@theihtisham/ai-agent-starter-kit 1.0.0

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.
Files changed (130) hide show
  1. package/.env.example +33 -0
  2. package/Dockerfile +35 -0
  3. package/LICENSE +21 -0
  4. package/README.md +73 -0
  5. package/docker-compose.yml +28 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +17 -0
  8. package/package.json +85 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +157 -0
  11. package/prisma/seed.ts +46 -0
  12. package/src/app/(auth)/forgot-password/page.tsx +56 -0
  13. package/src/app/(auth)/layout.tsx +7 -0
  14. package/src/app/(auth)/login/page.tsx +83 -0
  15. package/src/app/(auth)/signup/page.tsx +108 -0
  16. package/src/app/(dashboard)/agents/[id]/edit/page.tsx +68 -0
  17. package/src/app/(dashboard)/agents/[id]/page.tsx +114 -0
  18. package/src/app/(dashboard)/agents/new/page.tsx +43 -0
  19. package/src/app/(dashboard)/agents/page.tsx +63 -0
  20. package/src/app/(dashboard)/api-keys/page.tsx +139 -0
  21. package/src/app/(dashboard)/dashboard/page.tsx +79 -0
  22. package/src/app/(dashboard)/layout.tsx +16 -0
  23. package/src/app/(dashboard)/settings/billing/page.tsx +59 -0
  24. package/src/app/(dashboard)/settings/page.tsx +45 -0
  25. package/src/app/(dashboard)/usage/page.tsx +46 -0
  26. package/src/app/api/agents/[id]/chat/route.ts +100 -0
  27. package/src/app/api/agents/[id]/chats/route.ts +36 -0
  28. package/src/app/api/agents/[id]/route.ts +97 -0
  29. package/src/app/api/agents/route.ts +84 -0
  30. package/src/app/api/api-keys/[id]/route.ts +25 -0
  31. package/src/app/api/api-keys/route.ts +72 -0
  32. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  33. package/src/app/api/auth/register/route.ts +53 -0
  34. package/src/app/api/health/route.ts +26 -0
  35. package/src/app/api/stripe/checkout/route.ts +37 -0
  36. package/src/app/api/stripe/plans/route.ts +16 -0
  37. package/src/app/api/stripe/portal/route.ts +29 -0
  38. package/src/app/api/stripe/webhook/route.ts +45 -0
  39. package/src/app/api/usage/route.ts +43 -0
  40. package/src/app/globals.css +59 -0
  41. package/src/app/layout.tsx +22 -0
  42. package/src/app/page.tsx +32 -0
  43. package/src/app/pricing/page.tsx +25 -0
  44. package/src/components/agents/agent-form.tsx +137 -0
  45. package/src/components/agents/model-selector.tsx +35 -0
  46. package/src/components/agents/tool-selector.tsx +48 -0
  47. package/src/components/auth-provider.tsx +17 -0
  48. package/src/components/billing/plan-badge.tsx +23 -0
  49. package/src/components/billing/pricing-table.tsx +95 -0
  50. package/src/components/billing/usage-meter.tsx +39 -0
  51. package/src/components/chat/chat-input.tsx +68 -0
  52. package/src/components/chat/chat-interface.tsx +152 -0
  53. package/src/components/chat/chat-message.tsx +50 -0
  54. package/src/components/chat/chat-sidebar.tsx +49 -0
  55. package/src/components/chat/code-block.tsx +38 -0
  56. package/src/components/chat/markdown-renderer.tsx +56 -0
  57. package/src/components/chat/streaming-text.tsx +46 -0
  58. package/src/components/dashboard/agent-card.tsx +52 -0
  59. package/src/components/dashboard/header.tsx +75 -0
  60. package/src/components/dashboard/sidebar.tsx +52 -0
  61. package/src/components/dashboard/stat-card.tsx +42 -0
  62. package/src/components/dashboard/usage-chart.tsx +42 -0
  63. package/src/components/landing/cta.tsx +30 -0
  64. package/src/components/landing/features.tsx +75 -0
  65. package/src/components/landing/hero.tsx +42 -0
  66. package/src/components/landing/pricing.tsx +28 -0
  67. package/src/components/ui/avatar.tsx +24 -0
  68. package/src/components/ui/badge.tsx +24 -0
  69. package/src/components/ui/button.tsx +39 -0
  70. package/src/components/ui/card.tsx +50 -0
  71. package/src/components/ui/dialog.tsx +73 -0
  72. package/src/components/ui/dropdown.tsx +77 -0
  73. package/src/components/ui/input.tsx +23 -0
  74. package/src/components/ui/skeleton.tsx +7 -0
  75. package/src/components/ui/switch.tsx +31 -0
  76. package/src/components/ui/table.tsx +48 -0
  77. package/src/components/ui/tabs.tsx +66 -0
  78. package/src/components/ui/textarea.tsx +20 -0
  79. package/src/hooks/use-agent.ts +44 -0
  80. package/src/hooks/use-streaming.ts +82 -0
  81. package/src/hooks/use-subscription.ts +40 -0
  82. package/src/hooks/use-usage.ts +43 -0
  83. package/src/hooks/use-user.ts +13 -0
  84. package/src/lib/agents/index.ts +60 -0
  85. package/src/lib/agents/memory/long-term.ts +241 -0
  86. package/src/lib/agents/memory/manager.ts +154 -0
  87. package/src/lib/agents/memory/short-term.ts +155 -0
  88. package/src/lib/agents/memory/types.ts +68 -0
  89. package/src/lib/agents/orchestration/debate.ts +170 -0
  90. package/src/lib/agents/orchestration/index.ts +103 -0
  91. package/src/lib/agents/orchestration/parallel.ts +143 -0
  92. package/src/lib/agents/orchestration/router.ts +199 -0
  93. package/src/lib/agents/orchestration/sequential.ts +127 -0
  94. package/src/lib/agents/orchestration/types.ts +68 -0
  95. package/src/lib/agents/tools/calculator.ts +131 -0
  96. package/src/lib/agents/tools/code-executor.ts +191 -0
  97. package/src/lib/agents/tools/file-reader.ts +129 -0
  98. package/src/lib/agents/tools/index.ts +48 -0
  99. package/src/lib/agents/tools/registry.ts +182 -0
  100. package/src/lib/agents/tools/web-search.ts +83 -0
  101. package/src/lib/ai/agent.ts +275 -0
  102. package/src/lib/ai/context.ts +68 -0
  103. package/src/lib/ai/memory.ts +98 -0
  104. package/src/lib/ai/models.ts +80 -0
  105. package/src/lib/ai/streaming.ts +80 -0
  106. package/src/lib/ai/tools.ts +149 -0
  107. package/src/lib/auth/middleware.ts +41 -0
  108. package/src/lib/auth/nextauth.ts +69 -0
  109. package/src/lib/db/client.ts +15 -0
  110. package/src/lib/rate-limit/limiter.ts +93 -0
  111. package/src/lib/rate-limit/rules.ts +38 -0
  112. package/src/lib/stripe/client.ts +25 -0
  113. package/src/lib/stripe/plans.ts +75 -0
  114. package/src/lib/stripe/usage.ts +123 -0
  115. package/src/lib/stripe/webhooks.ts +96 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/errors.ts +73 -0
  118. package/src/lib/utils/helpers.ts +50 -0
  119. package/src/lib/utils/id.ts +21 -0
  120. package/src/lib/utils/logger.ts +38 -0
  121. package/src/lib/utils/validation.ts +44 -0
  122. package/src/middleware.ts +13 -0
  123. package/src/types/agent.ts +31 -0
  124. package/src/types/api.ts +38 -0
  125. package/src/types/billing.ts +35 -0
  126. package/src/types/chat.ts +30 -0
  127. package/src/types/next-auth.d.ts +19 -0
  128. package/tailwind.config.ts +72 -0
  129. package/tsconfig.json +28 -0
  130. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,80 @@
1
+ export type AIProvider = 'openai' | 'anthropic' | 'google' | 'custom';
2
+
3
+ export interface ModelConfig {
4
+ id: string;
5
+ name: string;
6
+ provider: AIProvider;
7
+ maxTokens: number;
8
+ supportsTools: boolean;
9
+ supportsStreaming: boolean;
10
+ pricing: {
11
+ inputPer1k: number;
12
+ outputPer1k: number;
13
+ };
14
+ }
15
+
16
+ export const AVAILABLE_MODELS: ModelConfig[] = [
17
+ {
18
+ id: 'gpt-4o',
19
+ name: 'GPT-4o',
20
+ provider: 'openai',
21
+ maxTokens: 128000,
22
+ supportsTools: true,
23
+ supportsStreaming: true,
24
+ pricing: { inputPer1k: 0.0025, outputPer1k: 0.01 },
25
+ },
26
+ {
27
+ id: 'gpt-4o-mini',
28
+ name: 'GPT-4o Mini',
29
+ provider: 'openai',
30
+ maxTokens: 128000,
31
+ supportsTools: true,
32
+ supportsStreaming: true,
33
+ pricing: { inputPer1k: 0.00015, outputPer1k: 0.0006 },
34
+ },
35
+ {
36
+ id: 'gpt-4-turbo',
37
+ name: 'GPT-4 Turbo',
38
+ provider: 'openai',
39
+ maxTokens: 128000,
40
+ supportsTools: true,
41
+ supportsStreaming: true,
42
+ pricing: { inputPer1k: 0.01, outputPer1k: 0.03 },
43
+ },
44
+ {
45
+ id: 'claude-sonnet-4-6',
46
+ name: 'Claude Sonnet 4.6',
47
+ provider: 'anthropic',
48
+ maxTokens: 200000,
49
+ supportsTools: true,
50
+ supportsStreaming: true,
51
+ pricing: { inputPer1k: 0.003, outputPer1k: 0.015 },
52
+ },
53
+ {
54
+ id: 'claude-haiku-4-5',
55
+ name: 'Claude Haiku 4.5',
56
+ provider: 'anthropic',
57
+ maxTokens: 200000,
58
+ supportsTools: true,
59
+ supportsStreaming: true,
60
+ pricing: { inputPer1k: 0.0008, outputPer1k: 0.004 },
61
+ },
62
+ {
63
+ id: 'gemini-2.5-flash',
64
+ name: 'Gemini 2.5 Flash',
65
+ provider: 'google',
66
+ maxTokens: 1000000,
67
+ supportsTools: true,
68
+ supportsStreaming: true,
69
+ pricing: { inputPer1k: 0.000075, outputPer1k: 0.0003 },
70
+ },
71
+ ];
72
+
73
+ export function getModel(modelId: string): ModelConfig | undefined {
74
+ return AVAILABLE_MODELS.find((m) => m.id === modelId);
75
+ }
76
+
77
+ export function getProviderForModel(modelId: string): AIProvider {
78
+ const model = getModel(modelId);
79
+ return model?.provider ?? 'openai';
80
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Server-Sent Events (SSE) streaming helper for chat responses.
3
+ */
4
+
5
+ export interface StreamChunk {
6
+ type: 'text' | 'tool_call' | 'tool_result' | 'error' | 'done';
7
+ content: string;
8
+ metadata?: Record<string, unknown>;
9
+ }
10
+
11
+ /**
12
+ * Create a ReadableStream that emits SSE-formatted chunks
13
+ */
14
+ export function createSSEStream(): {
15
+ stream: ReadableStream<Uint8Array>;
16
+ controller: ReadableStreamDefaultController<Uint8Array>;
17
+ send: (chunk: StreamChunk) => void;
18
+ close: () => void;
19
+ } {
20
+ const encoder = new TextEncoder();
21
+ let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
22
+
23
+ const stream = new ReadableStream<Uint8Array>({
24
+ start(c) {
25
+ controller = c;
26
+ },
27
+ });
28
+
29
+ const send = (chunk: StreamChunk) => {
30
+ if (!controller) return;
31
+ const data = `data: ${JSON.stringify(chunk)}\n\n`;
32
+ controller.enqueue(encoder.encode(data));
33
+ };
34
+
35
+ const close = () => {
36
+ if (!controller) return;
37
+ send({ type: 'done', content: '' });
38
+ controller.close();
39
+ };
40
+
41
+ return { stream, controller: controller!, send, close };
42
+ }
43
+
44
+ /**
45
+ * Parse an SSE stream from a fetch Response into async generator of chunks
46
+ */
47
+ export async function* parseSSEStream(
48
+ response: Response,
49
+ ): AsyncGenerator<StreamChunk> {
50
+ const reader = response.body?.getReader();
51
+ if (!reader) throw new Error('No response body');
52
+
53
+ const decoder = new TextDecoder();
54
+ let buffer = '';
55
+
56
+ try {
57
+ while (true) {
58
+ const { done, value } = await reader.read();
59
+ if (done) break;
60
+
61
+ buffer += decoder.decode(value, { stream: true });
62
+ const lines = buffer.split('\n');
63
+ buffer = lines.pop() ?? '';
64
+
65
+ for (const line of lines) {
66
+ if (line.startsWith('data: ')) {
67
+ const data = line.slice(6).trim();
68
+ if (data === '[DONE]') return;
69
+ try {
70
+ yield JSON.parse(data) as StreamChunk;
71
+ } catch {
72
+ // Skip malformed chunks
73
+ }
74
+ }
75
+ }
76
+ }
77
+ } finally {
78
+ reader.releaseLock();
79
+ }
80
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Built-in tools registry for AI agents.
3
+ * Each tool defines its schema and handler function.
4
+ */
5
+
6
+ export interface ToolDefinition {
7
+ id: string;
8
+ name: string;
9
+ description: string;
10
+ parameters: Record<string, {
11
+ type: string;
12
+ description: string;
13
+ required?: boolean;
14
+ enum?: string[];
15
+ }>;
16
+ handler: (params: Record<string, unknown>) => Promise<string>;
17
+ }
18
+
19
+ export const builtInTools: Record<string, ToolDefinition> = {};
20
+
21
+ export function registerTool(tool: ToolDefinition): void {
22
+ builtInTools[tool.id] = tool;
23
+ }
24
+
25
+ export function getTool(toolId: string): ToolDefinition | undefined {
26
+ return builtInTools[toolId];
27
+ }
28
+
29
+ export function getAllTools(): ToolDefinition[] {
30
+ return Object.values(builtInTools);
31
+ }
32
+
33
+ export function getToolsByIds(ids: string[]): ToolDefinition[] {
34
+ return ids.map((id) => builtInTools[id]).filter((t): t is ToolDefinition => t != null);
35
+ }
36
+
37
+ // --- Web Search Tool ---
38
+ registerTool({
39
+ id: 'web_search',
40
+ name: 'Web Search',
41
+ description: 'Search the web for information. Returns top results with titles, URLs, and snippets.',
42
+ parameters: {
43
+ query: { type: 'string', description: 'Search query', required: true },
44
+ max_results: { type: 'number', description: 'Max results to return (1-10)', required: false },
45
+ },
46
+ handler: async (params) => {
47
+ const query = params.query as string;
48
+ const maxResults = Math.min((params.max_results as number) ?? 5, 10);
49
+ // Placeholder: in production, use SearXNG, SerpAPI, or Google Custom Search
50
+ return `Search results for "${query}" (top ${maxResults}):\n[Web search not configured — set SEARXNG_URL or SERP_API_KEY env var]`;
51
+ },
52
+ });
53
+
54
+ // --- Calculator Tool ---
55
+ registerTool({
56
+ id: 'calculator',
57
+ name: 'Calculator',
58
+ description: 'Evaluate mathematical expressions safely.',
59
+ parameters: {
60
+ expression: { type: 'string', description: 'Math expression to evaluate (e.g., "2 + 2", "Math.sqrt(16)")', required: true },
61
+ },
62
+ handler: async (params) => {
63
+ const expression = params.expression as string;
64
+ try {
65
+ // Safe evaluation using Function constructor with restricted scope
66
+ const allowedMath = {
67
+ abs: Math.abs, ceil: Math.ceil, floor: Math.floor, round: Math.round,
68
+ sqrt: Math.sqrt, pow: Math.pow, log: Math.log, log2: Math.log2,
69
+ sin: Math.sin, cos: Math.cos, tan: Math.tan, PI: Math.PI, E: Math.E,
70
+ min: Math.min, max: Math.max, random: Math.random,
71
+ };
72
+ const keys = Object.keys(allowedMath);
73
+ const values = Object.values(allowedMath);
74
+ const fn = new Function(...keys, `"use strict"; return (${expression});`);
75
+ const result = fn(...values);
76
+ return `Result: ${result}`;
77
+ } catch (err) {
78
+ return `Error evaluating expression: ${(err as Error).message}`;
79
+ }
80
+ },
81
+ });
82
+
83
+ // --- JSON Parser Tool ---
84
+ registerTool({
85
+ id: 'json_parser',
86
+ name: 'JSON Parser',
87
+ description: 'Parse and query JSON data. Supports JSONPath-like queries.',
88
+ parameters: {
89
+ data: { type: 'string', description: 'JSON string to parse', required: true },
90
+ query: { type: 'string', description: 'Optional dot-notation path (e.g., "users[0].name")', required: false },
91
+ },
92
+ handler: async (params) => {
93
+ const data = params.data as string;
94
+ const query = params.query as string | undefined;
95
+ try {
96
+ let parsed = JSON.parse(data);
97
+ if (query) {
98
+ const parts = query.replace(/\[(\d+)]/g, '.$1').split('.');
99
+ for (const part of parts) {
100
+ if (part === '') continue;
101
+ parsed = parsed[part];
102
+ if (parsed === undefined) return `Path "${query}" not found in JSON`;
103
+ }
104
+ }
105
+ return JSON.stringify(parsed, null, 2);
106
+ } catch (err) {
107
+ return `JSON parse error: ${(err as Error).message}`;
108
+ }
109
+ },
110
+ });
111
+
112
+ // --- Code Executor Tool (safe sandbox) ---
113
+ registerTool({
114
+ id: 'code_executor',
115
+ name: 'Code Executor',
116
+ description: 'Execute JavaScript code in a safe sandbox. Read-only access, no filesystem.',
117
+ parameters: {
118
+ code: { type: 'string', description: 'JavaScript code to execute', required: true },
119
+ language: { type: 'string', description: 'Language (currently only "javascript")', required: false, enum: ['javascript'] },
120
+ },
121
+ handler: async (params) => {
122
+ const code = params.code as string;
123
+ try {
124
+ const fn = new Function('"use strict"; ' + code);
125
+ const result = fn();
126
+ return `Output: ${JSON.stringify(result, null, 2)}`;
127
+ } catch (err) {
128
+ return `Execution error: ${(err as Error).message}`;
129
+ }
130
+ },
131
+ });
132
+
133
+ // --- File Reader Tool ---
134
+ registerTool({
135
+ id: 'file_reader',
136
+ name: 'File Reader',
137
+ description: 'Read file contents. Supports text files, JSON, CSV.',
138
+ parameters: {
139
+ file_content: { type: 'string', description: 'The file content to analyze', required: true },
140
+ filename: { type: 'string', description: 'Name of the file', required: true },
141
+ },
142
+ handler: async (params) => {
143
+ const content = params.file_content as string;
144
+ const filename = params.filename as string;
145
+ const lines = content.split('\n').length;
146
+ const chars = content.length;
147
+ return `File: ${filename}\nLines: ${lines}, Characters: ${chars}\n\nContent preview:\n${content.slice(0, 2000)}${content.length > 2000 ? '\n...[truncated]' : ''}`;
148
+ },
149
+ });
@@ -0,0 +1,41 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getToken } from 'next-auth/jwt';
3
+
4
+ const PUBLIC_PATHS = ['/', '/login', '/signup', '/forgot-password', '/pricing', '/api/auth', '/api/health'];
5
+ const PUBLIC_API_PREFIXES = ['/api/stripe/webhook'];
6
+
7
+ export async function authMiddleware(req: NextRequest) {
8
+ const { pathname } = req.nextUrl;
9
+
10
+ // Allow public paths
11
+ if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith(p + '/'))) {
12
+ return NextResponse.next();
13
+ }
14
+
15
+ // Allow webhook endpoints (verified by signature)
16
+ if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) {
17
+ return NextResponse.next();
18
+ }
19
+
20
+ // Allow static files and Next.js internals
21
+ if (pathname.startsWith('/_next') || pathname.startsWith('/favicon') || pathname.includes('.')) {
22
+ return NextResponse.next();
23
+ }
24
+
25
+ // Check JWT token
26
+ const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
27
+
28
+ if (!token) {
29
+ // API routes return 401
30
+ if (pathname.startsWith('/api/')) {
31
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
32
+ }
33
+ // Page routes redirect to login
34
+ const loginUrl = req.nextUrl.clone();
35
+ loginUrl.pathname = '/login';
36
+ loginUrl.searchParams.set('callbackUrl', pathname);
37
+ return NextResponse.redirect(loginUrl);
38
+ }
39
+
40
+ return NextResponse.next();
41
+ }
@@ -0,0 +1,69 @@
1
+ import type { NextAuthOptions } from 'next-auth';
2
+ import GoogleProvider from 'next-auth/providers/google';
3
+ import GitHubProvider from 'next-auth/providers/github';
4
+ import EmailProvider from 'next-auth/providers/email';
5
+ import { PrismaAdapter } from '@next-auth/prisma-adapter';
6
+ import prisma from '../db/client';
7
+
8
+ export const authOptions: NextAuthOptions = {
9
+ adapter: PrismaAdapter(prisma),
10
+ providers: [
11
+ ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
12
+ ? [
13
+ GoogleProvider({
14
+ clientId: process.env.GOOGLE_CLIENT_ID,
15
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
16
+ }),
17
+ ]
18
+ : []),
19
+ ...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
20
+ ? [
21
+ GitHubProvider({
22
+ clientId: process.env.GITHUB_CLIENT_ID,
23
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
24
+ }),
25
+ ]
26
+ : []),
27
+ ...(process.env.RESEND_API_KEY
28
+ ? [
29
+ EmailProvider({
30
+ server: {
31
+ host: 'smtp.resend.com',
32
+ port: 465,
33
+ auth: {
34
+ user: 'resend',
35
+ pass: process.env.RESEND_API_KEY,
36
+ },
37
+ },
38
+ from: process.env.EMAIL_FROM ?? 'noreply@yourdomain.com',
39
+ }),
40
+ ]
41
+ : []),
42
+ ],
43
+ session: {
44
+ strategy: 'jwt',
45
+ maxAge: 30 * 24 * 60 * 60, // 30 days
46
+ },
47
+ pages: {
48
+ signIn: '/login',
49
+ error: '/login',
50
+ },
51
+ callbacks: {
52
+ async jwt({ token, user }) {
53
+ if (user) {
54
+ token.id = user.id;
55
+ token.email = user.email;
56
+ }
57
+ return token;
58
+ },
59
+ async session({ session, token }) {
60
+ if (session.user && token.id) {
61
+ session.user.id = token.id as string;
62
+ session.user.email = token.email as string;
63
+ }
64
+ return session;
65
+ },
66
+ },
67
+ secret: process.env.NEXTAUTH_SECRET,
68
+ debug: process.env.NODE_ENV === 'development',
69
+ };
@@ -0,0 +1,15 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const globalForPrisma = globalThis as unknown as {
4
+ prisma: PrismaClient | undefined;
5
+ };
6
+
7
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({
8
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
9
+ });
10
+
11
+ if (process.env.NODE_ENV !== 'production') {
12
+ globalForPrisma.prisma = prisma;
13
+ }
14
+
15
+ export default prisma;
@@ -0,0 +1,93 @@
1
+ import { logger } from '../utils/logger';
2
+
3
+ interface RateLimitEntry {
4
+ count: number;
5
+ resetAt: number;
6
+ }
7
+
8
+ const store = new Map<string, RateLimitEntry>();
9
+
10
+ // Cleanup old entries every 5 minutes
11
+ setInterval(() => {
12
+ const now = Date.now();
13
+ const keysToDelete: string[] = [];
14
+ store.forEach((entry, key) => {
15
+ if (entry.resetAt < now) {
16
+ keysToDelete.push(key);
17
+ }
18
+ });
19
+ keysToDelete.forEach((key) => store.delete(key));
20
+ }, 5 * 60 * 1000);
21
+
22
+ export interface RateLimitResult {
23
+ allowed: boolean;
24
+ remaining: number;
25
+ limit: number;
26
+ resetAt: number;
27
+ }
28
+
29
+ /**
30
+ * Sliding window rate limiter.
31
+ * Uses in-memory store by default, Redis when REDIS_URL is set.
32
+ */
33
+ export async function rateLimit(
34
+ key: string,
35
+ limit: number,
36
+ windowMs: number,
37
+ ): Promise<RateLimitResult> {
38
+ // If Redis is available, use it for distributed rate limiting
39
+ if (process.env.REDIS_URL) {
40
+ return redisRateLimit(key, limit, windowMs);
41
+ }
42
+
43
+ return memoryRateLimit(key, limit, windowMs);
44
+ }
45
+
46
+ function memoryRateLimit(key: string, limit: number, windowMs: number): RateLimitResult {
47
+ const now = Date.now();
48
+ const entry = store.get(key);
49
+
50
+ if (!entry || entry.resetAt < now) {
51
+ store.set(key, { count: 1, resetAt: now + windowMs });
52
+ return { allowed: true, remaining: limit - 1, limit, resetAt: now + windowMs };
53
+ }
54
+
55
+ if (entry.count >= limit) {
56
+ return { allowed: false, remaining: 0, limit, resetAt: entry.resetAt };
57
+ }
58
+
59
+ entry.count++;
60
+ return { allowed: true, remaining: limit - entry.count, limit, resetAt: entry.resetAt };
61
+ }
62
+
63
+ async function redisRateLimit(key: string, limit: number, windowMs: number): Promise<RateLimitResult> {
64
+ const redisKey = `ratelimit:${key}`;
65
+ const resetAt = Date.now() + windowMs;
66
+
67
+ try {
68
+ // Dynamic import for optional redis dependency
69
+ const { createClient } = await import('redis');
70
+ const client = createClient({ url: process.env.REDIS_URL });
71
+ await client.connect();
72
+
73
+ const count = await client.incr(redisKey);
74
+ if (count === 1) {
75
+ await client.pExpire(redisKey, windowMs);
76
+ }
77
+
78
+ const ttl = await client.pTTL(redisKey);
79
+ const remaining = Math.max(0, limit - count);
80
+
81
+ await client.disconnect();
82
+
83
+ return {
84
+ allowed: count <= limit,
85
+ remaining,
86
+ limit,
87
+ resetAt: Date.now() + ttl,
88
+ };
89
+ } catch (err) {
90
+ logger.warn('Redis rate limit failed, falling back to memory', { error: (err as Error).message });
91
+ return memoryRateLimit(key, limit, windowMs);
92
+ }
93
+ }
@@ -0,0 +1,38 @@
1
+ import { getPlan } from '../stripe/plans';
2
+ import { rateLimit, type RateLimitResult } from './limiter';
3
+
4
+ interface RateLimitRule {
5
+ windowMs: number;
6
+ limit: number;
7
+ }
8
+
9
+ const RULES: Record<string, RateLimitRule> = {
10
+ // Per-endpoint rate limits
11
+ 'chat:message': { windowMs: 60 * 1000, limit: 20 }, // 20 messages/minute
12
+ 'chat:stream': { windowMs: 60 * 1000, limit: 20 }, // 20 streams/minute
13
+ 'agent:create': { windowMs: 60 * 1000, limit: 10 }, // 10 agent creates/minute
14
+ 'agent:update': { windowMs: 60 * 1000, limit: 30 }, // 30 updates/minute
15
+ 'api-key:create': { windowMs: 60 * 60 * 1000, limit: 5 }, // 5 API keys/hour
16
+ 'auth:login': { windowMs: 15 * 60 * 1000, limit: 10 }, // 10 login attempts/15min
17
+ 'auth:signup': { windowMs: 60 * 60 * 1000, limit: 5 }, // 5 signups/hour
18
+ 'api:general': { windowMs: 60 * 1000, limit: 60 }, // 60 requests/minute
19
+ };
20
+
21
+ /**
22
+ * Apply rate limiting based on plan and action type.
23
+ * Pro/Team plans get 2x/5x the limits.
24
+ */
25
+ export async function applyRateLimit(
26
+ userId: string,
27
+ action: string,
28
+ planId: string = 'free',
29
+ ): Promise<RateLimitResult> {
30
+ const rule = RULES[action] ?? RULES['api:general']!;
31
+ const plan = getPlan(planId);
32
+
33
+ // Scale limits by plan tier
34
+ const multiplier = plan.id === 'team' ? 5 : plan.id === 'pro' ? 2 : 1;
35
+ const limit = rule.limit * multiplier;
36
+
37
+ return rateLimit(`rl:${userId}:${action}`, limit, rule.windowMs);
38
+ }
@@ -0,0 +1,25 @@
1
+ import Stripe from 'stripe';
2
+
3
+ let _stripe: Stripe | null = null;
4
+
5
+ export function getStripeClient(): Stripe {
6
+ if (!_stripe) {
7
+ const key = process.env.STRIPE_SECRET_KEY;
8
+ if (!key) {
9
+ throw new Error('STRIPE_SECRET_KEY is not configured');
10
+ }
11
+ _stripe = new Stripe(key, {
12
+ typescript: true,
13
+ } as Stripe.StripeConfig);
14
+ }
15
+ return _stripe;
16
+ }
17
+
18
+ // Lazy getter — only creates client when accessed
19
+ export const stripe = new Proxy({} as Stripe, {
20
+ get(_target, prop) {
21
+ const client = getStripeClient();
22
+ const value = (client as unknown as Record<string | symbol, unknown>)[prop];
23
+ return typeof value === 'function' ? value.bind(client) : value;
24
+ },
25
+ });
@@ -0,0 +1,75 @@
1
+ export interface Plan {
2
+ id: string;
3
+ name: string;
4
+ price: number;
5
+ interval: 'month' | 'year';
6
+ stripePriceId: string;
7
+ features: {
8
+ maxAgents: number;
9
+ maxMessages: number;
10
+ maxTokens: number;
11
+ tools: boolean;
12
+ apiAccess: boolean;
13
+ support: string;
14
+ customTools: boolean;
15
+ };
16
+ }
17
+
18
+ export const PLANS: Plan[] = [
19
+ {
20
+ id: 'free',
21
+ name: 'Free',
22
+ price: 0,
23
+ interval: 'month',
24
+ stripePriceId: '',
25
+ features: {
26
+ maxAgents: 2,
27
+ maxMessages: 50,
28
+ maxTokens: 100000,
29
+ tools: false,
30
+ apiAccess: false,
31
+ support: 'Community',
32
+ customTools: false,
33
+ },
34
+ },
35
+ {
36
+ id: 'pro',
37
+ name: 'Pro',
38
+ price: 19,
39
+ interval: 'month',
40
+ stripePriceId: process.env.STRIPE_PRO_PRICE_ID ?? '',
41
+ features: {
42
+ maxAgents: 10,
43
+ maxMessages: 2000,
44
+ maxTokens: 2000000,
45
+ tools: true,
46
+ apiAccess: true,
47
+ support: 'Email',
48
+ customTools: false,
49
+ },
50
+ },
51
+ {
52
+ id: 'team',
53
+ name: 'Team',
54
+ price: 49,
55
+ interval: 'month',
56
+ stripePriceId: process.env.STRIPE_TEAM_PRICE_ID ?? '',
57
+ features: {
58
+ maxAgents: 50,
59
+ maxMessages: 10000,
60
+ maxTokens: 10000000,
61
+ tools: true,
62
+ apiAccess: true,
63
+ support: 'Priority',
64
+ customTools: true,
65
+ },
66
+ },
67
+ ];
68
+
69
+ export function getPlan(planId: string): Plan {
70
+ return PLANS.find((p) => p.id === planId) ?? PLANS[0]!;
71
+ }
72
+
73
+ export function getPlanByPriceId(stripePriceId: string): Plan | undefined {
74
+ return PLANS.find((p) => p.stripePriceId === stripePriceId);
75
+ }