chore: tighten deploy checklist and lint setup
This commit is contained in:
parent
1849dde46e
commit
ee254fc5c3
20
.cursorrules
20
.cursorrules
@ -35,12 +35,14 @@ pnpm test:ui # Vitest UI dashboard
|
||||
pnpm test:coverage # Generate coverage report
|
||||
|
||||
# Deployment to Cloudflare
|
||||
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 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
|
||||
|
||||
@ -322,6 +324,7 @@ return { json: { output: actual } };
|
||||
### Deploying to Cloudflare
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm lint
|
||||
npx @opennextjs/cloudflare build
|
||||
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`).
|
||||
|
||||
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
|
||||
npx @opennextjs/cloudflare build
|
||||
```
|
||||
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
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
184
CLAUDE.md
184
CLAUDE.md
@ -25,24 +25,61 @@ npx wrangler deploy # Deploy to Cloudflare (requires OpenNext bui
|
||||
npx wrangler tail # View live logs from deployed worker
|
||||
```
|
||||
|
||||
**Critical**:
|
||||
- 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`.
|
||||
**Critical Deployment Checklist (must be followed exactly):**
|
||||
|
||||
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—it’s 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 don’t 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
|
||||
|
||||
### Agent Configuration
|
||||
- Agents are 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
|
||||
- **Standard agents** defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
|
||||
- **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
|
||||
- Agent selection is persisted to localStorage using agent-specific keys
|
||||
- Custom agents (created via Morgan) have `agentId` format: `custom-{uuid}`
|
||||
|
||||
### Message Flow
|
||||
1. User submits message through `ChatInterface` component
|
||||
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`)
|
||||
4. Message forwarded to n8n webhook with all metadata
|
||||
5. Response parsed from n8n (supports streaming chunks and diff tool calls)
|
||||
3. Route extracts webhook URL:
|
||||
- Standard agents (`agent-N`): Uses `AGENT_N_URL` from environment variables
|
||||
- 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}`)
|
||||
|
||||
### 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
|
||||
- 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
|
||||
|
||||
### API Routes
|
||||
- `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/flags/route.ts` - Returns feature flags from environment variables
|
||||
|
||||
### Core Components
|
||||
- `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/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-display.tsx` - Side-by-side diff visualization
|
||||
|
||||
@ -82,24 +148,70 @@ npx wrangler tail # View live logs from deployed worker
|
||||
## Important Patterns
|
||||
|
||||
### Environment Variable Access
|
||||
Always extract agent index from agentId before accessing env vars:
|
||||
Always check agent type and extract appropriate webhook URL:
|
||||
```typescript
|
||||
// For standard agents
|
||||
const match = agentId.match(/agent-(\d+)/)
|
||||
const agentIndex = match[1]
|
||||
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
|
||||
if (match) {
|
||||
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
|
||||
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
|
||||
- Per-agent session: `chat-session-{agentId}`
|
||||
- Per-agent messages: `chat-messages-{agentId}`
|
||||
- Custom agents: `pinned-agents` (array of PinnedAgent objects)
|
||||
|
||||
### Response Parsing in /api/chat
|
||||
The route handles multiple n8n response formats:
|
||||
1. Streaming chunks (newline-delimited JSON with `type: "item"`)
|
||||
2. Tool calls (`type: "tool_call", name: "show_diff"`)
|
||||
3. Regular JSON with various fields (`response`, `message`, `output`, `text`)
|
||||
4. Plain text fallback
|
||||
2. Tool calls (`type: "tool_call"`)
|
||||
3. Code node output (`[{ output: { messageType, content, toolCall? } }]`)
|
||||
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
|
||||
- Images converted to base64 via FileReader in browser
|
||||
@ -184,6 +296,7 @@ The route handles multiple n8n response formats:
|
||||
### Available Flags
|
||||
- `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation
|
||||
- `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
|
||||
|
||||
@ -298,3 +411,46 @@ expect(response.status).toBe(403)
|
||||
- No database - all state in localStorage and n8n workflows
|
||||
- Mobile-first responsive design with specific mobile breakpoint styling
|
||||
- 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
|
||||
|
||||
18
README.md
18
README.md
@ -206,21 +206,33 @@ Where N = 1, 2, 3, etc. The application will discover all configured agents auto
|
||||
|
||||
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
|
||||
npx @opennextjs/cloudflare build
|
||||
```
|
||||
|
||||
**Important:** Always run this before deploying. Standard `next build` alone is insufficient.
|
||||
|
||||
2. **Deploy**
|
||||
4. **Deploy**
|
||||
```bash
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
**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
|
||||
npx wrangler tail
|
||||
```
|
||||
|
||||
@ -1,16 +1,3 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc/dist/eslintrc.cjs";
|
||||
import nextConfig from "eslint-config-next";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
export default nextConfig;
|
||||
|
||||
@ -80,6 +80,7 @@
|
||||
"@types/react-dom": "^18",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.3",
|
||||
"jsdom": "^27.2.0",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^4.1.9",
|
||||
|
||||
1902
pnpm-lock.yaml
generated
1902
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -10,9 +10,10 @@ interface PinnedAgentsDrawerProps {
|
||||
isOpen: boolean
|
||||
onClose: () => 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 [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null)
|
||||
@ -37,7 +38,9 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored)
|
||||
setTimeout(() => {
|
||||
setAgents(parsed)
|
||||
}, 0)
|
||||
} catch (error) {
|
||||
console.error("Failed to parse pinned agents:", error)
|
||||
}
|
||||
@ -182,10 +185,10 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
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" />
|
||||
Start chat
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user