thevoidforge 21.0.11 → 21.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.claude/commands/ai.md +69 -0
- package/dist/.claude/commands/architect.md +121 -0
- package/dist/.claude/commands/assemble.md +201 -0
- package/dist/.claude/commands/assess.md +75 -0
- package/dist/.claude/commands/blueprint.md +135 -0
- package/dist/.claude/commands/build.md +116 -0
- package/dist/.claude/commands/campaign.md +201 -0
- package/dist/.claude/commands/cultivation.md +166 -0
- package/dist/.claude/commands/current.md +128 -0
- package/dist/.claude/commands/dangerroom.md +74 -0
- package/dist/.claude/commands/debrief.md +178 -0
- package/dist/.claude/commands/deploy.md +99 -0
- package/dist/.claude/commands/devops.md +143 -0
- package/dist/.claude/commands/gauntlet.md +140 -0
- package/dist/.claude/commands/git.md +104 -0
- package/dist/.claude/commands/grow.md +146 -0
- package/dist/.claude/commands/imagine.md +126 -0
- package/dist/.claude/commands/portfolio.md +50 -0
- package/dist/.claude/commands/prd.md +113 -0
- package/dist/.claude/commands/qa.md +107 -0
- package/dist/.claude/commands/review.md +151 -0
- package/dist/.claude/commands/security.md +100 -0
- package/dist/.claude/commands/test.md +96 -0
- package/dist/.claude/commands/thumper.md +116 -0
- package/dist/.claude/commands/treasury.md +100 -0
- package/dist/.claude/commands/ux.md +118 -0
- package/dist/.claude/commands/vault.md +189 -0
- package/dist/.claude/commands/void.md +108 -0
- package/dist/CHANGELOG.md +1918 -0
- package/dist/CLAUDE.md +250 -0
- package/dist/HOLOCRON.md +856 -0
- package/dist/VERSION.md +123 -0
- package/dist/docs/NAMING_REGISTRY.md +478 -0
- package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
- package/dist/docs/methods/ASSEMBLER.md +142 -0
- package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
- package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
- package/dist/docs/methods/CAMPAIGN.md +568 -0
- package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
- package/dist/docs/methods/DEEP_CURRENT.md +184 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
- package/dist/docs/methods/FIELD_MEDIC.md +261 -0
- package/dist/docs/methods/FORGE_ARTIST.md +108 -0
- package/dist/docs/methods/FORGE_KEEPER.md +268 -0
- package/dist/docs/methods/GAUNTLET.md +344 -0
- package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
- package/dist/docs/methods/HEARTBEAT.md +168 -0
- package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
- package/dist/docs/methods/MUSTER.md +148 -0
- package/dist/docs/methods/PRD_GENERATOR.md +186 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
- package/dist/docs/methods/QA_ENGINEER.md +337 -0
- package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
- package/dist/docs/methods/SUB_AGENTS.md +335 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
- package/dist/docs/methods/TESTING.md +359 -0
- package/dist/docs/methods/THUMPER.md +175 -0
- package/dist/docs/methods/TIME_VAULT.md +120 -0
- package/dist/docs/methods/TREASURY.md +184 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
- package/dist/docs/patterns/README.md +52 -0
- package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
- package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
- package/dist/docs/patterns/ai-classifier.ts +195 -0
- package/dist/docs/patterns/ai-eval.ts +272 -0
- package/dist/docs/patterns/ai-orchestrator.ts +341 -0
- package/dist/docs/patterns/ai-router.ts +194 -0
- package/dist/docs/patterns/ai-tool-schema.ts +237 -0
- package/dist/docs/patterns/api-route.ts +241 -0
- package/dist/docs/patterns/backtest-engine.ts +499 -0
- package/dist/docs/patterns/browser-review.ts +292 -0
- package/dist/docs/patterns/combobox.tsx +300 -0
- package/dist/docs/patterns/component.tsx +262 -0
- package/dist/docs/patterns/daemon-process.ts +338 -0
- package/dist/docs/patterns/data-pipeline.ts +297 -0
- package/dist/docs/patterns/database-migration.ts +466 -0
- package/dist/docs/patterns/e2e-test.ts +629 -0
- package/dist/docs/patterns/error-handling.ts +312 -0
- package/dist/docs/patterns/execution-safety.ts +601 -0
- package/dist/docs/patterns/financial-transaction.ts +342 -0
- package/dist/docs/patterns/funding-plan.ts +462 -0
- package/dist/docs/patterns/game-entity.ts +137 -0
- package/dist/docs/patterns/game-loop.ts +113 -0
- package/dist/docs/patterns/game-state.ts +143 -0
- package/dist/docs/patterns/job-queue.ts +225 -0
- package/dist/docs/patterns/kongo-integration.ts +164 -0
- package/dist/docs/patterns/middleware.ts +363 -0
- package/dist/docs/patterns/mobile-screen.tsx +139 -0
- package/dist/docs/patterns/mobile-service.ts +167 -0
- package/dist/docs/patterns/multi-tenant.ts +382 -0
- package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
- package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
- package/dist/docs/patterns/prompt-template.ts +195 -0
- package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
- package/dist/docs/patterns/service.ts +224 -0
- package/dist/docs/patterns/sse-endpoint.ts +118 -0
- package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
- package/dist/docs/patterns/third-party-script.ts +68 -0
- package/dist/scripts/thumper/gom-jabbar.sh +241 -0
- package/dist/scripts/thumper/relay.sh +610 -0
- package/dist/scripts/thumper/scan.sh +359 -0
- package/dist/scripts/thumper/thumper.sh +190 -0
- package/dist/scripts/thumper/water-rings.sh +76 -0
- package/dist/wizard/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: AI Router
|
|
3
|
+
*
|
|
4
|
+
* Maps natural language input to typed intents and dispatches to handlers.
|
|
5
|
+
* Think of this as a programmable switch statement where AI does the matching.
|
|
6
|
+
*
|
|
7
|
+
* Key principles:
|
|
8
|
+
* - Intents are a closed set — define them upfront, not discovered at runtime
|
|
9
|
+
* - Ambiguity is an explicit state — route to clarification, not a random handler
|
|
10
|
+
* - Default fallback prevents unhandled inputs from failing silently
|
|
11
|
+
* - Emit metrics on every route decision for observability
|
|
12
|
+
* - Handler functions are pure business logic — no AI inside handlers
|
|
13
|
+
*
|
|
14
|
+
* Agents: Picard (routing architecture), Stark (handler dispatch), Batman (edge cases)
|
|
15
|
+
*
|
|
16
|
+
* Provider note: Uses Anthropic for intent classification. Swap classify() for any provider.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
20
|
+
import { z } from 'zod'
|
|
21
|
+
|
|
22
|
+
// --- Intent definitions — closed set, exhaustive ---
|
|
23
|
+
|
|
24
|
+
export const INTENTS = [
|
|
25
|
+
'check_order_status',
|
|
26
|
+
'request_refund',
|
|
27
|
+
'update_account',
|
|
28
|
+
'product_question',
|
|
29
|
+
'speak_to_human',
|
|
30
|
+
'ambiguous',
|
|
31
|
+
] as const
|
|
32
|
+
|
|
33
|
+
export type Intent = (typeof INTENTS)[number]
|
|
34
|
+
|
|
35
|
+
// --- Router types ---
|
|
36
|
+
|
|
37
|
+
interface RouteResult {
|
|
38
|
+
intent: Intent
|
|
39
|
+
confidence: number
|
|
40
|
+
params: Record<string, string> // Extracted entities (orderId, productName, etc.)
|
|
41
|
+
handlerResponse: unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RouterMetrics {
|
|
45
|
+
intent: Intent
|
|
46
|
+
confidence: number
|
|
47
|
+
latencyMs: number
|
|
48
|
+
routed: boolean // false if fell through to default
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type MetricsEmitter = (metrics: RouterMetrics) => void
|
|
52
|
+
|
|
53
|
+
type IntentHandler = (
|
|
54
|
+
input: string,
|
|
55
|
+
params: Record<string, string>
|
|
56
|
+
) => Promise<unknown>
|
|
57
|
+
|
|
58
|
+
// --- Intent classification ---
|
|
59
|
+
|
|
60
|
+
const IntentOutputSchema = z.object({
|
|
61
|
+
intent: z.enum(INTENTS),
|
|
62
|
+
confidence: z.number().min(0).max(1),
|
|
63
|
+
params: z.record(z.string()),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
async function classifyIntent(
|
|
67
|
+
client: Anthropic,
|
|
68
|
+
input: string
|
|
69
|
+
): Promise<z.infer<typeof IntentOutputSchema>> {
|
|
70
|
+
const response = await client.messages.create({
|
|
71
|
+
model: 'claude-sonnet-4-20250514',
|
|
72
|
+
max_tokens: 256,
|
|
73
|
+
system: [
|
|
74
|
+
'You are an intent classifier. Classify the user message into exactly one intent.',
|
|
75
|
+
`Valid intents: ${INTENTS.join(', ')}`,
|
|
76
|
+
'If the intent is unclear, use "ambiguous".',
|
|
77
|
+
'Extract relevant parameters (orderId, productName, etc.) from the message.',
|
|
78
|
+
'Respond with JSON: { "intent": "<intent>", "confidence": <0-1>, "params": {} }',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
messages: [{ role: 'user', content: input }],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const content = response.content[0]
|
|
84
|
+
if (content.type !== 'text') throw new Error('Expected text response')
|
|
85
|
+
return IntentOutputSchema.parse(JSON.parse(content.text))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// OpenAI adaptation:
|
|
89
|
+
// Use openai.chat.completions.create() with response_format: { type: 'json_object' }.
|
|
90
|
+
// Same schema parsing. For very high throughput, consider fine-tuned gpt-4o-mini.
|
|
91
|
+
|
|
92
|
+
// --- IntentRouter class ---
|
|
93
|
+
|
|
94
|
+
const AMBIGUITY_THRESHOLD = 0.50 // Below this, treat as ambiguous regardless of model label
|
|
95
|
+
|
|
96
|
+
export class IntentRouter {
|
|
97
|
+
private handlers = new Map<Intent, IntentHandler>()
|
|
98
|
+
private defaultHandler: IntentHandler
|
|
99
|
+
private emitMetrics: MetricsEmitter
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
private client: Anthropic,
|
|
103
|
+
options: {
|
|
104
|
+
defaultHandler: IntentHandler
|
|
105
|
+
emitMetrics?: MetricsEmitter
|
|
106
|
+
}
|
|
107
|
+
) {
|
|
108
|
+
this.defaultHandler = options.defaultHandler
|
|
109
|
+
this.emitMetrics = options.emitMetrics ?? (() => {}) // No-op if not provided
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Register a handler for an intent. One handler per intent. */
|
|
113
|
+
on(intent: Intent, handler: IntentHandler): this {
|
|
114
|
+
this.handlers.set(intent, handler)
|
|
115
|
+
return this
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Classify intent from natural language and dispatch to the registered handler. */
|
|
119
|
+
async routeRequest(input: string): Promise<RouteResult> {
|
|
120
|
+
const start = Date.now()
|
|
121
|
+
const classification = await classifyIntent(this.client, input)
|
|
122
|
+
const latencyMs = Date.now() - start
|
|
123
|
+
|
|
124
|
+
// Override to ambiguous if confidence is too low, regardless of model's label
|
|
125
|
+
const effectiveIntent: Intent =
|
|
126
|
+
classification.confidence < AMBIGUITY_THRESHOLD ? 'ambiguous' : classification.intent
|
|
127
|
+
|
|
128
|
+
const handler = this.handlers.get(effectiveIntent) ?? this.defaultHandler
|
|
129
|
+
const routed = this.handlers.has(effectiveIntent)
|
|
130
|
+
|
|
131
|
+
// Emit metrics for observability — track routing decisions, not content
|
|
132
|
+
this.emitMetrics({
|
|
133
|
+
intent: effectiveIntent,
|
|
134
|
+
confidence: classification.confidence,
|
|
135
|
+
latencyMs,
|
|
136
|
+
routed,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const handlerResponse = await handler(input, classification.params)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
intent: effectiveIntent,
|
|
143
|
+
confidence: classification.confidence,
|
|
144
|
+
params: classification.params,
|
|
145
|
+
handlerResponse,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Usage example ---
|
|
151
|
+
|
|
152
|
+
// const router = new IntentRouter(anthropicClient, {
|
|
153
|
+
// defaultHandler: async (input) => ({
|
|
154
|
+
// message: "I'm not sure how to help with that. Let me connect you with someone.",
|
|
155
|
+
// }),
|
|
156
|
+
// emitMetrics: (m) => statsd.increment('ai.router.intent', { intent: m.intent }),
|
|
157
|
+
// })
|
|
158
|
+
//
|
|
159
|
+
// router
|
|
160
|
+
// .on('check_order_status', async (_input, params) => {
|
|
161
|
+
// return orderService.getStatus(params.orderId)
|
|
162
|
+
// })
|
|
163
|
+
// .on('request_refund', async (_input, params) => {
|
|
164
|
+
// return refundService.initiate(params.orderId)
|
|
165
|
+
// })
|
|
166
|
+
// .on('ambiguous', async (input) => {
|
|
167
|
+
// return { message: 'Could you clarify? I can help with orders, refunds, or account changes.' }
|
|
168
|
+
// })
|
|
169
|
+
//
|
|
170
|
+
// const result = await router.routeRequest("Where's my order #12345?")
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Framework adaptations:
|
|
174
|
+
*
|
|
175
|
+
* Express:
|
|
176
|
+
* - Mount router as middleware: app.post('/api/chat', async (req, res) => {
|
|
177
|
+
* const result = await router.routeRequest(req.body.message)
|
|
178
|
+
* res.json(result)
|
|
179
|
+
* })
|
|
180
|
+
* - Metrics via StatsD/Prometheus middleware
|
|
181
|
+
* - Rate limit the classification endpoint (express-rate-limit)
|
|
182
|
+
*
|
|
183
|
+
* FastAPI:
|
|
184
|
+
* - IntentRouter as a dependency: router = Depends(get_intent_router)
|
|
185
|
+
* - Pydantic models for RouteResult and RouterMetrics
|
|
186
|
+
* - Metrics via prometheus-fastapi-instrumentator or custom middleware
|
|
187
|
+
* - Async handlers: all handlers are async def by default in Python
|
|
188
|
+
*
|
|
189
|
+
* Django:
|
|
190
|
+
* - IntentRouter instantiated in services.py, called from views
|
|
191
|
+
* - Store routing decisions in a RoutingLog model for analytics
|
|
192
|
+
* - Metrics via django-prometheus or statsd
|
|
193
|
+
* - For real-time: combine with SSE pattern (sse-endpoint.ts) via Django Channels
|
|
194
|
+
*/
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: AI Tool Schema
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Define tools once, convert to any provider format (Anthropic, OpenAI)
|
|
6
|
+
* - Zod schemas for parameter validation — model output is untrusted input
|
|
7
|
+
* - Tool execution has error boundaries — one bad tool call can't crash the agent
|
|
8
|
+
* - Registry pattern for managing available tools per context/user/role
|
|
9
|
+
* - Tool results are always strings — structured data is JSON-serialized
|
|
10
|
+
*
|
|
11
|
+
* Agents: Stark (backend), Picard (architecture), Kenobi (input validation)
|
|
12
|
+
*
|
|
13
|
+
* Provider note: defineTool() creates a provider-agnostic definition.
|
|
14
|
+
* toAnthropicFormat() and toOpenAIFormat() convert for each SDK.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z, ZodType, ZodObject, ZodRawShape } from 'zod'
|
|
18
|
+
import type Anthropic from '@anthropic-ai/sdk'
|
|
19
|
+
|
|
20
|
+
// --- Core types ---
|
|
21
|
+
|
|
22
|
+
/** Provider-agnostic tool definition. */
|
|
23
|
+
export interface ToolDefinition<TInput extends ZodRawShape = ZodRawShape> {
|
|
24
|
+
name: string
|
|
25
|
+
description: string
|
|
26
|
+
parameters: ZodObject<TInput>
|
|
27
|
+
execute: (input: z.infer<ZodObject<TInput>>) => Promise<string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Result of a tool execution. */
|
|
31
|
+
export interface ToolResult {
|
|
32
|
+
toolName: string
|
|
33
|
+
success: boolean
|
|
34
|
+
output: string
|
|
35
|
+
durationMs: number
|
|
36
|
+
error?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- defineTool() — type-safe tool creation ---
|
|
40
|
+
|
|
41
|
+
/** Create a tool definition with Zod-validated parameters. */
|
|
42
|
+
export function defineTool<T extends ZodRawShape>(config: {
|
|
43
|
+
name: string
|
|
44
|
+
description: string
|
|
45
|
+
parameters: ZodObject<T>
|
|
46
|
+
execute: (input: z.infer<ZodObject<T>>) => Promise<string>
|
|
47
|
+
}): ToolDefinition<T> {
|
|
48
|
+
return config
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Example tool definitions:
|
|
52
|
+
//
|
|
53
|
+
// const getWeatherTool = defineTool({
|
|
54
|
+
// name: 'get_weather',
|
|
55
|
+
// description: 'Get current weather for a city',
|
|
56
|
+
// parameters: z.object({
|
|
57
|
+
// city: z.string().describe('City name'),
|
|
58
|
+
// units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Temperature units'),
|
|
59
|
+
// }),
|
|
60
|
+
// execute: async (input) => {
|
|
61
|
+
// const weather = await weatherService.getCurrent(input.city, input.units)
|
|
62
|
+
// return JSON.stringify(weather) // Always return string
|
|
63
|
+
// },
|
|
64
|
+
// })
|
|
65
|
+
|
|
66
|
+
// --- Tool execution with error boundary ---
|
|
67
|
+
|
|
68
|
+
/** Execute a tool safely. Validates input, catches errors, measures duration. */
|
|
69
|
+
export async function executeTool<T extends ZodRawShape>(
|
|
70
|
+
tool: ToolDefinition<T>,
|
|
71
|
+
rawInput: unknown
|
|
72
|
+
): Promise<ToolResult> {
|
|
73
|
+
const start = Date.now()
|
|
74
|
+
|
|
75
|
+
// 1. Validate input with Zod — model output is untrusted
|
|
76
|
+
const parseResult = tool.parameters.safeParse(rawInput)
|
|
77
|
+
if (!parseResult.success) {
|
|
78
|
+
return {
|
|
79
|
+
toolName: tool.name,
|
|
80
|
+
success: false,
|
|
81
|
+
output: '',
|
|
82
|
+
durationMs: Date.now() - start,
|
|
83
|
+
error: `Invalid parameters: ${parseResult.error.issues.map((i) => i.message).join(', ')}`,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Execute with error boundary
|
|
88
|
+
try {
|
|
89
|
+
const output = await tool.execute(parseResult.data)
|
|
90
|
+
return {
|
|
91
|
+
toolName: tool.name,
|
|
92
|
+
success: true,
|
|
93
|
+
output,
|
|
94
|
+
durationMs: Date.now() - start,
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
toolName: tool.name,
|
|
99
|
+
success: false,
|
|
100
|
+
output: '',
|
|
101
|
+
durationMs: Date.now() - start,
|
|
102
|
+
error: error instanceof Error ? error.message : 'Tool execution failed',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- ToolRegistry — manage available tools ---
|
|
108
|
+
|
|
109
|
+
export class ToolRegistry {
|
|
110
|
+
private tools = new Map<string, ToolDefinition>()
|
|
111
|
+
|
|
112
|
+
/** Register a tool. Throws if name already taken. */
|
|
113
|
+
register(tool: ToolDefinition): this {
|
|
114
|
+
if (this.tools.has(tool.name)) {
|
|
115
|
+
throw new Error(`Tool already registered: ${tool.name}`)
|
|
116
|
+
}
|
|
117
|
+
this.tools.set(tool.name, tool)
|
|
118
|
+
return this
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Get a tool by name. Returns undefined if not found. */
|
|
122
|
+
get(name: string): ToolDefinition | undefined {
|
|
123
|
+
return this.tools.get(name)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Execute a tool by name with raw input from the model. */
|
|
127
|
+
async execute(name: string, rawInput: unknown): Promise<ToolResult> {
|
|
128
|
+
const tool = this.tools.get(name)
|
|
129
|
+
if (!tool) {
|
|
130
|
+
return {
|
|
131
|
+
toolName: name,
|
|
132
|
+
success: false,
|
|
133
|
+
output: '',
|
|
134
|
+
durationMs: 0,
|
|
135
|
+
error: `Unknown tool: ${name}`,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return executeTool(tool, rawInput)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Convert all registered tools to Anthropic format. */
|
|
142
|
+
toAnthropicFormat(): Anthropic.Tool[] {
|
|
143
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
144
|
+
name: tool.name,
|
|
145
|
+
description: tool.description,
|
|
146
|
+
input_schema: zodToJsonSchema(tool.parameters),
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Convert all registered tools to OpenAI format. */
|
|
151
|
+
toOpenAIFormat(): Array<{
|
|
152
|
+
type: 'function'
|
|
153
|
+
function: { name: string; description: string; parameters: Record<string, unknown> }
|
|
154
|
+
}> {
|
|
155
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
156
|
+
type: 'function' as const,
|
|
157
|
+
function: {
|
|
158
|
+
name: tool.name,
|
|
159
|
+
description: tool.description,
|
|
160
|
+
parameters: zodToJsonSchema(tool.parameters),
|
|
161
|
+
},
|
|
162
|
+
}))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** List all registered tool names. */
|
|
166
|
+
listNames(): string[] {
|
|
167
|
+
return Array.from(this.tools.keys())
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Zod to JSON Schema conversion (simplified) ---
|
|
172
|
+
// In production, use zod-to-json-schema package. This is a minimal reference.
|
|
173
|
+
|
|
174
|
+
function zodToJsonSchema(schema: ZodType): Record<string, unknown> {
|
|
175
|
+
// Use zod-to-json-schema in production:
|
|
176
|
+
// import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
177
|
+
// return zodToJsonSchema(schema)
|
|
178
|
+
//
|
|
179
|
+
// Simplified version for the pattern reference:
|
|
180
|
+
if (schema instanceof ZodObject) {
|
|
181
|
+
const shape = schema.shape
|
|
182
|
+
const properties: Record<string, unknown> = {}
|
|
183
|
+
const required: string[] = []
|
|
184
|
+
|
|
185
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
186
|
+
const zodField = value as ZodType
|
|
187
|
+
properties[key] = { type: 'string', description: zodField.description ?? '' }
|
|
188
|
+
if (!zodField.isOptional()) required.push(key)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { type: 'object', properties, required }
|
|
192
|
+
}
|
|
193
|
+
return { type: 'object', properties: {} }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Usage example ---
|
|
197
|
+
|
|
198
|
+
// const registry = new ToolRegistry()
|
|
199
|
+
// .register(getWeatherTool)
|
|
200
|
+
// .register(lookupOrderTool)
|
|
201
|
+
// .register(createTicketTool)
|
|
202
|
+
//
|
|
203
|
+
// // Pass to Anthropic agent loop (see ai-orchestrator.ts):
|
|
204
|
+
// const response = await client.messages.create({
|
|
205
|
+
// model: 'claude-sonnet-4-20250514',
|
|
206
|
+
// tools: registry.toAnthropicFormat(),
|
|
207
|
+
// messages: [{ role: 'user', content: 'What is the weather in Tokyo?' }],
|
|
208
|
+
// })
|
|
209
|
+
//
|
|
210
|
+
// // When model returns tool_use, execute via registry:
|
|
211
|
+
// for (const block of response.content) {
|
|
212
|
+
// if (block.type === 'tool_use') {
|
|
213
|
+
// const result = await registry.execute(block.name, block.input)
|
|
214
|
+
// // Feed result back to model as tool_result
|
|
215
|
+
// }
|
|
216
|
+
// }
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Framework adaptations:
|
|
220
|
+
*
|
|
221
|
+
* Express:
|
|
222
|
+
* - ToolRegistry as singleton, initialized at startup
|
|
223
|
+
* - Register tools from service layer — tools call services, not DB directly
|
|
224
|
+
* - Admin endpoint to list available tools: GET /api/admin/tools
|
|
225
|
+
*
|
|
226
|
+
* FastAPI:
|
|
227
|
+
* - defineTool equivalent: dataclass with Pydantic model for parameters
|
|
228
|
+
* - ToolRegistry as dependency: registry = Depends(get_tool_registry)
|
|
229
|
+
* - Pydantic replaces Zod for parameter validation
|
|
230
|
+
* - Same provider adapter pattern: to_anthropic_format(), to_openai_format()
|
|
231
|
+
*
|
|
232
|
+
* Django:
|
|
233
|
+
* - Tools defined in tools.py, registered in apps.py ready() method
|
|
234
|
+
* - Tool permissions tied to Django auth: tool.requires_perm('orders.view')
|
|
235
|
+
* - ToolResult logged to ToolExecutionLog model for audit
|
|
236
|
+
* - Admin interface to enable/disable tools per tenant (multi-tenant.ts)
|
|
237
|
+
*/
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: API Route Handler
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Validate input at the boundary (Zod schema)
|
|
6
|
+
* - Auth check before business logic
|
|
7
|
+
* - Business logic in service layer, not the route
|
|
8
|
+
* - Consistent response shape
|
|
9
|
+
* - Structured error handling — never leak internals
|
|
10
|
+
* - Ownership verification for user-scoped resources
|
|
11
|
+
*
|
|
12
|
+
* Agents: Rogers (API design), Barton (error handling), Kenobi (security)
|
|
13
|
+
*
|
|
14
|
+
* Framework adaptations:
|
|
15
|
+
* Next.js: This file (NextRequest/NextResponse, App Router)
|
|
16
|
+
* Express: router.post('/api/projects', validate(schema), auth, async (req, res, next) => { ... })
|
|
17
|
+
* Django: DRF ViewSet with serializer validation, permission_classes, service call
|
|
18
|
+
* Rails: Controller action with strong_params, before_action :authenticate, service call
|
|
19
|
+
*
|
|
20
|
+
* === Django Deep Dive (DRF) ===
|
|
21
|
+
*
|
|
22
|
+
* from rest_framework import viewsets, serializers, permissions, status
|
|
23
|
+
* from rest_framework.response import Response
|
|
24
|
+
* from .services import ProjectService
|
|
25
|
+
*
|
|
26
|
+
* class ProjectSerializer(serializers.Serializer):
|
|
27
|
+
* name = serializers.CharField(max_length=200)
|
|
28
|
+
* description = serializers.CharField(required=False, allow_blank=True)
|
|
29
|
+
*
|
|
30
|
+
* class ProjectViewSet(viewsets.ViewSet):
|
|
31
|
+
* permission_classes = [permissions.IsAuthenticated]
|
|
32
|
+
*
|
|
33
|
+
* def create(self, request):
|
|
34
|
+
* serializer = ProjectSerializer(data=request.data)
|
|
35
|
+
* serializer.is_valid(raise_exception=True) # DRF handles 400 automatically
|
|
36
|
+
* project = ProjectService.create(
|
|
37
|
+
* user=request.user,
|
|
38
|
+
* **serializer.validated_data
|
|
39
|
+
* )
|
|
40
|
+
* return Response(ProjectSerializer(project).data, status=status.HTTP_201_CREATED)
|
|
41
|
+
*
|
|
42
|
+
* # Key differences from TypeScript pattern:
|
|
43
|
+
* # - Validation: DRF serializers replace Zod schemas
|
|
44
|
+
* # - Auth: permission_classes replaces getSession() — declarative, not imperative
|
|
45
|
+
* # - Ownership: service receives request.user, enforces ownership internally
|
|
46
|
+
* # - Errors: DRF exception handler maps service exceptions to HTTP responses
|
|
47
|
+
* # - SELECT control: Use serializer fields to control response shape, not Prisma select
|
|
48
|
+
* # For mutations, always SELECT only the fields you need (serializer.fields controls this)
|
|
49
|
+
*
|
|
50
|
+
* === FastAPI Deep Dive ===
|
|
51
|
+
*
|
|
52
|
+
* from fastapi import APIRouter, Depends, HTTPException
|
|
53
|
+
* from pydantic import BaseModel
|
|
54
|
+
* from .services import ProjectService
|
|
55
|
+
* from .auth import get_current_user, User
|
|
56
|
+
*
|
|
57
|
+
* router = APIRouter(prefix="/api/projects")
|
|
58
|
+
*
|
|
59
|
+
* class CreateProjectRequest(BaseModel):
|
|
60
|
+
* name: str
|
|
61
|
+
* description: str | None = None
|
|
62
|
+
*
|
|
63
|
+
* @router.post("/", status_code=201)
|
|
64
|
+
* async def create_project(
|
|
65
|
+
* body: CreateProjectRequest,
|
|
66
|
+
* user: User = Depends(get_current_user),
|
|
67
|
+
* ):
|
|
68
|
+
* project = await ProjectService.create(user=user, **body.model_dump())
|
|
69
|
+
* return project
|
|
70
|
+
*
|
|
71
|
+
* # Key differences from TypeScript pattern:
|
|
72
|
+
* # - Validation: Pydantic models replace Zod schemas (validated before handler runs)
|
|
73
|
+
* # - Auth: Depends(get_current_user) replaces getSession() — dependency injection
|
|
74
|
+
* # - Ownership: service receives user, enforces ownership internally
|
|
75
|
+
* # - Errors: HTTPException or custom exception handlers map to HTTP responses
|
|
76
|
+
* # - Fire-and-forget: Use BackgroundTasks for sendBeacon-style endpoints
|
|
77
|
+
*
|
|
78
|
+
* See /docs/patterns/error-handling.ts for the canonical error strategy.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
82
|
+
import { z } from 'zod'
|
|
83
|
+
import { getSession } from '@/lib/auth'
|
|
84
|
+
import { projectService } from '@/lib/services/projects'
|
|
85
|
+
import { ApiError, handleApiError } from '@/lib/errors'
|
|
86
|
+
|
|
87
|
+
// --- Input validation at the boundary ---
|
|
88
|
+
const createProjectSchema = z.object({
|
|
89
|
+
name: z.string().min(1).max(100).trim(),
|
|
90
|
+
description: z.string().max(500).optional(),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// --- POST /api/projects ---
|
|
94
|
+
export async function POST(req: NextRequest) {
|
|
95
|
+
try {
|
|
96
|
+
// 1. Authenticate
|
|
97
|
+
const session = await getSession(req)
|
|
98
|
+
if (!session) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
|
|
101
|
+
{ status: 401 }
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Validate input
|
|
106
|
+
const body = await req.json()
|
|
107
|
+
const parsed = createProjectSchema.safeParse(body)
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{
|
|
111
|
+
error: {
|
|
112
|
+
code: 'VALIDATION_ERROR',
|
|
113
|
+
message: 'Invalid input',
|
|
114
|
+
details: parsed.error.flatten().fieldErrors,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{ status: 400 }
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Business logic in service layer
|
|
122
|
+
const project = await projectService.create({
|
|
123
|
+
...parsed.data,
|
|
124
|
+
ownerId: session.userId,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 4. Consistent success response
|
|
128
|
+
return NextResponse.json({ project }, { status: 201 })
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return handleApiError(error)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- GET /api/projects ---
|
|
135
|
+
export async function GET(req: NextRequest) {
|
|
136
|
+
try {
|
|
137
|
+
const session = await getSession(req)
|
|
138
|
+
if (!session) {
|
|
139
|
+
return NextResponse.json(
|
|
140
|
+
{ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
|
|
141
|
+
{ status: 401 }
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { searchParams } = new URL(req.url)
|
|
146
|
+
const page = Math.max(1, Number(searchParams.get('page')) || 1)
|
|
147
|
+
const limit = Math.min(50, Math.max(1, Number(searchParams.get('limit')) || 20))
|
|
148
|
+
|
|
149
|
+
// Service handles pagination, filtering, ownership scoping
|
|
150
|
+
const result = await projectService.list({
|
|
151
|
+
ownerId: session.userId,
|
|
152
|
+
page,
|
|
153
|
+
limit,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
return NextResponse.json({
|
|
157
|
+
projects: result.items,
|
|
158
|
+
pagination: {
|
|
159
|
+
page: result.page,
|
|
160
|
+
limit: result.limit,
|
|
161
|
+
total: result.total,
|
|
162
|
+
hasMore: result.hasMore,
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return handleApiError(error)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Prisma Select on Mutations ---
|
|
171
|
+
// RULE: Always use `select` on Prisma create/update responses. Never return
|
|
172
|
+
// raw Prisma results from mutations — they include ALL columns by default,
|
|
173
|
+
// silently leaking sensitive fields (phoneHash, email, tokens, internal IDs).
|
|
174
|
+
//
|
|
175
|
+
// BAD: const user = await db.user.update({ where: { id }, data: { name } })
|
|
176
|
+
// return NextResponse.json({ user }) // leaks phoneHash, email, etc.
|
|
177
|
+
//
|
|
178
|
+
// GOOD: const user = await db.user.update({
|
|
179
|
+
// where: { id }, data: { name },
|
|
180
|
+
// select: { id: true, name: true, avatar: true }
|
|
181
|
+
// })
|
|
182
|
+
// return NextResponse.json({ user }) // only selected fields
|
|
183
|
+
//
|
|
184
|
+
// This applies to create(), update(), upsert(), and any mutation that returns data.
|
|
185
|
+
// findMany/findUnique already use select in the service pattern — mutations must too.
|
|
186
|
+
// (Field report #36: Prisma update() without select leaked phoneHash and whatsappId.)
|
|
187
|
+
|
|
188
|
+
// --- Fire-and-Forget Endpoints (sendBeacon) ---
|
|
189
|
+
// navigator.sendBeacon() cannot set custom headers — it sends a plain POST with
|
|
190
|
+
// Content-Type: text/plain. This makes it incompatible with header-based CSRF
|
|
191
|
+
// enforcement (X-CSRF-Token). For analytics/telemetry endpoints that use sendBeacon:
|
|
192
|
+
//
|
|
193
|
+
// 1. Exempt the endpoint from CSRF header checks
|
|
194
|
+
// 2. Add compensating controls: per-IP rate limiting, session cookie validation,
|
|
195
|
+
// origin check via Referer/Origin header (browsers always send these on sendBeacon)
|
|
196
|
+
// 3. Ensure the endpoint only WRITES (append-only) — never reads or mutates user data
|
|
197
|
+
// 4. Log the exemption in the security audit
|
|
198
|
+
//
|
|
199
|
+
// Pattern:
|
|
200
|
+
// POST /api/analytics/event — exempt from X-CSRF-Token
|
|
201
|
+
// Validation: session cookie + origin header + rate limit (100/min/IP)
|
|
202
|
+
// Data: append-only event log, no user data returned in response
|
|
203
|
+
//
|
|
204
|
+
// (Field report #36: analytics endpoint failed silently because sendBeacon
|
|
205
|
+
// couldn't attach the CSRF header.)
|
|
206
|
+
|
|
207
|
+
// ── Anti-pattern: Raw Prisma spread ──────────────────
|
|
208
|
+
// NEVER spread raw ORM/Prisma objects into API responses:
|
|
209
|
+
// ❌ res.json({ ...record }) — leaks internal fields (userId, rawMessageId, FK columns)
|
|
210
|
+
// ❌ res.json({ ...record, extra: 1 }) — still leaks everything from the spread
|
|
211
|
+
// ✅ res.json({ id: record.id, name: record.name, context: record.context })
|
|
212
|
+
//
|
|
213
|
+
// Prisma `include` without `select` returns ALL columns.
|
|
214
|
+
// Spreading passes them to the client, exposing internal schema.
|
|
215
|
+
// Always whitelist fields explicitly.
|
|
216
|
+
// (Field report #77: Dialog Travel leaked 12 internal fields via ...rec spread)
|
|
217
|
+
|
|
218
|
+
// --- Consistent error handler (shared across all routes) ---
|
|
219
|
+
// Located in /lib/errors.ts — shown here for reference
|
|
220
|
+
/*
|
|
221
|
+
export function handleApiError(error: unknown) {
|
|
222
|
+
if (error instanceof ApiError) {
|
|
223
|
+
return NextResponse.json(
|
|
224
|
+
{ error: { code: error.code, message: error.message } },
|
|
225
|
+
{ status: error.status }
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Log unexpected errors with context, never expose to client
|
|
230
|
+
console.error('[API Error]', {
|
|
231
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
232
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return NextResponse.json(
|
|
237
|
+
{ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } },
|
|
238
|
+
{ status: 500 }
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
*/
|