@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.
- package/.env.example +33 -0
- package/Dockerfile +35 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +28 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +17 -0
- package/package.json +85 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +157 -0
- package/prisma/seed.ts +46 -0
- package/src/app/(auth)/forgot-password/page.tsx +56 -0
- package/src/app/(auth)/layout.tsx +7 -0
- package/src/app/(auth)/login/page.tsx +83 -0
- package/src/app/(auth)/signup/page.tsx +108 -0
- package/src/app/(dashboard)/agents/[id]/edit/page.tsx +68 -0
- package/src/app/(dashboard)/agents/[id]/page.tsx +114 -0
- package/src/app/(dashboard)/agents/new/page.tsx +43 -0
- package/src/app/(dashboard)/agents/page.tsx +63 -0
- package/src/app/(dashboard)/api-keys/page.tsx +139 -0
- package/src/app/(dashboard)/dashboard/page.tsx +79 -0
- package/src/app/(dashboard)/layout.tsx +16 -0
- package/src/app/(dashboard)/settings/billing/page.tsx +59 -0
- package/src/app/(dashboard)/settings/page.tsx +45 -0
- package/src/app/(dashboard)/usage/page.tsx +46 -0
- package/src/app/api/agents/[id]/chat/route.ts +100 -0
- package/src/app/api/agents/[id]/chats/route.ts +36 -0
- package/src/app/api/agents/[id]/route.ts +97 -0
- package/src/app/api/agents/route.ts +84 -0
- package/src/app/api/api-keys/[id]/route.ts +25 -0
- package/src/app/api/api-keys/route.ts +72 -0
- package/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/src/app/api/auth/register/route.ts +53 -0
- package/src/app/api/health/route.ts +26 -0
- package/src/app/api/stripe/checkout/route.ts +37 -0
- package/src/app/api/stripe/plans/route.ts +16 -0
- package/src/app/api/stripe/portal/route.ts +29 -0
- package/src/app/api/stripe/webhook/route.ts +45 -0
- package/src/app/api/usage/route.ts +43 -0
- package/src/app/globals.css +59 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/page.tsx +32 -0
- package/src/app/pricing/page.tsx +25 -0
- package/src/components/agents/agent-form.tsx +137 -0
- package/src/components/agents/model-selector.tsx +35 -0
- package/src/components/agents/tool-selector.tsx +48 -0
- package/src/components/auth-provider.tsx +17 -0
- package/src/components/billing/plan-badge.tsx +23 -0
- package/src/components/billing/pricing-table.tsx +95 -0
- package/src/components/billing/usage-meter.tsx +39 -0
- package/src/components/chat/chat-input.tsx +68 -0
- package/src/components/chat/chat-interface.tsx +152 -0
- package/src/components/chat/chat-message.tsx +50 -0
- package/src/components/chat/chat-sidebar.tsx +49 -0
- package/src/components/chat/code-block.tsx +38 -0
- package/src/components/chat/markdown-renderer.tsx +56 -0
- package/src/components/chat/streaming-text.tsx +46 -0
- package/src/components/dashboard/agent-card.tsx +52 -0
- package/src/components/dashboard/header.tsx +75 -0
- package/src/components/dashboard/sidebar.tsx +52 -0
- package/src/components/dashboard/stat-card.tsx +42 -0
- package/src/components/dashboard/usage-chart.tsx +42 -0
- package/src/components/landing/cta.tsx +30 -0
- package/src/components/landing/features.tsx +75 -0
- package/src/components/landing/hero.tsx +42 -0
- package/src/components/landing/pricing.tsx +28 -0
- package/src/components/ui/avatar.tsx +24 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +39 -0
- package/src/components/ui/card.tsx +50 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/dropdown.tsx +77 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +48 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +20 -0
- package/src/hooks/use-agent.ts +44 -0
- package/src/hooks/use-streaming.ts +82 -0
- package/src/hooks/use-subscription.ts +40 -0
- package/src/hooks/use-usage.ts +43 -0
- package/src/hooks/use-user.ts +13 -0
- package/src/lib/agents/index.ts +60 -0
- package/src/lib/agents/memory/long-term.ts +241 -0
- package/src/lib/agents/memory/manager.ts +154 -0
- package/src/lib/agents/memory/short-term.ts +155 -0
- package/src/lib/agents/memory/types.ts +68 -0
- package/src/lib/agents/orchestration/debate.ts +170 -0
- package/src/lib/agents/orchestration/index.ts +103 -0
- package/src/lib/agents/orchestration/parallel.ts +143 -0
- package/src/lib/agents/orchestration/router.ts +199 -0
- package/src/lib/agents/orchestration/sequential.ts +127 -0
- package/src/lib/agents/orchestration/types.ts +68 -0
- package/src/lib/agents/tools/calculator.ts +131 -0
- package/src/lib/agents/tools/code-executor.ts +191 -0
- package/src/lib/agents/tools/file-reader.ts +129 -0
- package/src/lib/agents/tools/index.ts +48 -0
- package/src/lib/agents/tools/registry.ts +182 -0
- package/src/lib/agents/tools/web-search.ts +83 -0
- package/src/lib/ai/agent.ts +275 -0
- package/src/lib/ai/context.ts +68 -0
- package/src/lib/ai/memory.ts +98 -0
- package/src/lib/ai/models.ts +80 -0
- package/src/lib/ai/streaming.ts +80 -0
- package/src/lib/ai/tools.ts +149 -0
- package/src/lib/auth/middleware.ts +41 -0
- package/src/lib/auth/nextauth.ts +69 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/rate-limit/limiter.ts +93 -0
- package/src/lib/rate-limit/rules.ts +38 -0
- package/src/lib/stripe/client.ts +25 -0
- package/src/lib/stripe/plans.ts +75 -0
- package/src/lib/stripe/usage.ts +123 -0
- package/src/lib/stripe/webhooks.ts +96 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/errors.ts +73 -0
- package/src/lib/utils/helpers.ts +50 -0
- package/src/lib/utils/id.ts +21 -0
- package/src/lib/utils/logger.ts +38 -0
- package/src/lib/utils/validation.ts +44 -0
- package/src/middleware.ts +13 -0
- package/src/types/agent.ts +31 -0
- package/src/types/api.ts +38 -0
- package/src/types/billing.ts +35 -0
- package/src/types/chat.ts +30 -0
- package/src/types/next-auth.d.ts +19 -0
- package/tailwind.config.ts +72 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { PlanBadge } from '@/components/billing/plan-badge';
|
|
7
|
+
import { UsageMeter } from '@/components/billing/usage-meter';
|
|
8
|
+
|
|
9
|
+
export default function BillingPage() {
|
|
10
|
+
const [planId, setPlanId] = useState('free');
|
|
11
|
+
const [usage, setUsage] = useState({ messages: 0, tokens: 0, agents: 0 });
|
|
12
|
+
const [limits, setLimits] = useState({ maxMessages: 50, maxTokens: 100000, maxAgents: 2 });
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
fetch('/api/usage')
|
|
16
|
+
.then((r) => r.json())
|
|
17
|
+
.then((res) => {
|
|
18
|
+
if (res.success) {
|
|
19
|
+
setPlanId(res.data?.planId ?? 'free');
|
|
20
|
+
setUsage(res.data?.usage ?? usage);
|
|
21
|
+
setLimits(res.data?.limits ?? limits);
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.catch(() => {});
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const handleManageBilling = async () => {
|
|
28
|
+
const res = await fetch('/api/stripe/portal', { method: 'POST' });
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
const { url } = await res.json();
|
|
31
|
+
window.location.href = url;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="max-w-2xl mx-auto space-y-6">
|
|
37
|
+
<h1 className="text-2xl font-bold">Billing</h1>
|
|
38
|
+
|
|
39
|
+
<Card>
|
|
40
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
41
|
+
<CardTitle>Current Plan</CardTitle>
|
|
42
|
+
<PlanBadge planId={planId} />
|
|
43
|
+
</CardHeader>
|
|
44
|
+
<CardContent className="space-y-6">
|
|
45
|
+
<UsageMeter label="Messages" used={usage.messages} limit={limits.maxMessages} />
|
|
46
|
+
<UsageMeter label="Tokens" used={usage.tokens} limit={limits.maxTokens} />
|
|
47
|
+
<UsageMeter label="Agents" used={usage.agents} limit={limits.maxAgents} />
|
|
48
|
+
|
|
49
|
+
<div className="flex gap-3 pt-4">
|
|
50
|
+
<Button onClick={() => window.location.href = '/pricing'}>Change Plan</Button>
|
|
51
|
+
{planId !== 'free' && (
|
|
52
|
+
<Button variant="outline" onClick={handleManageBilling}>Manage Billing</Button>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</CardContent>
|
|
56
|
+
</Card>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSession } from 'next-auth/react';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Input } from '@/components/ui/input';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
|
|
8
|
+
export default function SettingsPage() {
|
|
9
|
+
const { data: session } = useSession();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="max-w-2xl mx-auto space-y-6">
|
|
13
|
+
<h1 className="text-2xl font-bold">Settings</h1>
|
|
14
|
+
|
|
15
|
+
<Card>
|
|
16
|
+
<CardHeader>
|
|
17
|
+
<CardTitle>Profile</CardTitle>
|
|
18
|
+
</CardHeader>
|
|
19
|
+
<CardContent className="space-y-4">
|
|
20
|
+
<div className="space-y-2">
|
|
21
|
+
<label className="text-sm font-medium">Name</label>
|
|
22
|
+
<Input defaultValue={session?.user?.name ?? ''} />
|
|
23
|
+
</div>
|
|
24
|
+
<div className="space-y-2">
|
|
25
|
+
<label className="text-sm font-medium">Email</label>
|
|
26
|
+
<Input defaultValue={session?.user?.email ?? ''} disabled />
|
|
27
|
+
</div>
|
|
28
|
+
<Button>Save Changes</Button>
|
|
29
|
+
</CardContent>
|
|
30
|
+
</Card>
|
|
31
|
+
|
|
32
|
+
<Card>
|
|
33
|
+
<CardHeader>
|
|
34
|
+
<CardTitle>Danger Zone</CardTitle>
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<CardContent>
|
|
37
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
38
|
+
Permanently delete your account and all associated data.
|
|
39
|
+
</p>
|
|
40
|
+
<Button variant="destructive">Delete Account</Button>
|
|
41
|
+
</CardContent>
|
|
42
|
+
</Card>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { StatCard } from '@/components/dashboard/stat-card';
|
|
6
|
+
|
|
7
|
+
interface UsageData {
|
|
8
|
+
messages: number;
|
|
9
|
+
toolCalls: number;
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function UsagePage() {
|
|
15
|
+
const [usage, setUsage] = useState<UsageData>({ messages: 0, toolCalls: 0, inputTokens: 0, outputTokens: 0 });
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
fetch('/api/usage')
|
|
20
|
+
.then((r) => r.json())
|
|
21
|
+
.then((res) => {
|
|
22
|
+
if (res.success) setUsage(res.data?.usage ?? usage);
|
|
23
|
+
})
|
|
24
|
+
.catch(() => {})
|
|
25
|
+
.finally(() => setLoading(false));
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-6">
|
|
30
|
+
<h1 className="text-2xl font-bold">Usage</h1>
|
|
31
|
+
<Card>
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<CardTitle>Current Billing Period</CardTitle>
|
|
34
|
+
</CardHeader>
|
|
35
|
+
<CardContent>
|
|
36
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
37
|
+
<StatCard title="Messages" value={loading ? '...' : usage.messages} />
|
|
38
|
+
<StatCard title="Tool Calls" value={loading ? '...' : usage.toolCalls} />
|
|
39
|
+
<StatCard title="Input Tokens" value={loading ? '...' : usage.inputTokens.toLocaleString()} />
|
|
40
|
+
<StatCard title="Output Tokens" value={loading ? '...' : usage.outputTokens.toLocaleString()} />
|
|
41
|
+
</div>
|
|
42
|
+
</CardContent>
|
|
43
|
+
</Card>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import prisma from '@/lib/db/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
import { chatMessageSchema } from '@/lib/utils/validation';
|
|
7
|
+
import { AgentEngine, type AgentConfig } from '@/lib/ai/agent';
|
|
8
|
+
import { loadHistory, saveMessage, createChat, getOrCreateChat } from '@/lib/ai/memory';
|
|
9
|
+
import { generateChatTitle } from '@/lib/utils/id';
|
|
10
|
+
import { checkUsageLimit, recordUsage } from '@/lib/stripe/usage';
|
|
11
|
+
import { applyRateLimit } from '@/lib/rate-limit/rules';
|
|
12
|
+
|
|
13
|
+
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
14
|
+
try {
|
|
15
|
+
const { id: agentId } = await params;
|
|
16
|
+
const session = await getServerSession(authOptions);
|
|
17
|
+
if (!session?.user?.id) {
|
|
18
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rl = await applyRateLimit(session.user.id, 'chat:message');
|
|
22
|
+
if (!rl.allowed) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ success: false, error: { code: 'RATE_LIMITED', message: 'Too many messages' } },
|
|
25
|
+
{ status: 429, headers: { 'Retry-After': String(Math.ceil((rl.resetAt - Date.now()) / 1000)) } },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const usageCheck = await checkUsageLimit(session.user.id, 'messages');
|
|
30
|
+
if (!usageCheck.allowed) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ success: false, error: { code: 'LIMIT_EXCEEDED', message: 'Message limit reached. Upgrade your plan.' } },
|
|
33
|
+
{ status: 402 },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const body = await req.json();
|
|
38
|
+
const { message, chatId } = chatMessageSchema.parse(body);
|
|
39
|
+
|
|
40
|
+
// Get agent
|
|
41
|
+
const agent = await prisma.agent.findFirst({
|
|
42
|
+
where: { id: agentId, userId: session.user.id },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!agent) {
|
|
46
|
+
return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: 'Agent not found' } }, { status: 404 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get or create chat
|
|
50
|
+
const chat = chatId
|
|
51
|
+
? await prisma.chat.findUnique({ where: { id: chatId } })
|
|
52
|
+
: await createChat(agentId, session.user.id, generateChatTitle(message));
|
|
53
|
+
|
|
54
|
+
if (!chat) {
|
|
55
|
+
return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: 'Chat not found' } }, { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Save user message
|
|
59
|
+
await saveMessage(chat.id, 'user', message);
|
|
60
|
+
await recordUsage(session.user.id, 'message', 1);
|
|
61
|
+
|
|
62
|
+
// Load history and build agent engine
|
|
63
|
+
const history = await loadHistory(chat.id);
|
|
64
|
+
const config: AgentConfig = {
|
|
65
|
+
model: agent.model,
|
|
66
|
+
systemPrompt: agent.systemPrompt,
|
|
67
|
+
temperature: agent.temperature,
|
|
68
|
+
maxTokens: agent.maxTokens,
|
|
69
|
+
toolIds: agent.tools as string[],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const engine = new AgentEngine(config);
|
|
73
|
+
const { stream, getResult } = await engine.chatStream(history, message);
|
|
74
|
+
|
|
75
|
+
// Start processing in background and save results
|
|
76
|
+
const resultPromise = getResult().then(async (result) => {
|
|
77
|
+
await saveMessage(chat.id, 'assistant', result.content);
|
|
78
|
+
await recordUsage(session.user.id, 'token_input', result.inputTokens);
|
|
79
|
+
await recordUsage(session.user.id, 'token_output', result.outputTokens);
|
|
80
|
+
if (result.toolCalls > 0) {
|
|
81
|
+
await recordUsage(session.user.id, 'tool_call', result.toolCalls);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Fire-and-forget result saving
|
|
87
|
+
resultPromise.catch(() => {});
|
|
88
|
+
|
|
89
|
+
return new Response(stream, {
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'text/event-stream',
|
|
92
|
+
'Cache-Control': 'no-cache',
|
|
93
|
+
Connection: 'keep-alive',
|
|
94
|
+
'X-Chat-Id': chat.id,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return handleApiError(err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import prisma from '@/lib/db/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
import { getChatsForAgent, createChat } from '@/lib/ai/memory';
|
|
7
|
+
|
|
8
|
+
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
try {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const session = await getServerSession(authOptions);
|
|
12
|
+
if (!session?.user?.id) {
|
|
13
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const chats = await getChatsForAgent(id, session.user.id);
|
|
17
|
+
return NextResponse.json({ success: true, data: chats });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return handleApiError(err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
24
|
+
try {
|
|
25
|
+
const { id } = await params;
|
|
26
|
+
const session = await getServerSession(authOptions);
|
|
27
|
+
if (!session?.user?.id) {
|
|
28
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const chat = await createChat(id, session.user.id, 'New Chat');
|
|
32
|
+
return NextResponse.json({ success: true, data: chat }, { status: 201 });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return handleApiError(err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import prisma from '@/lib/db/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
import { updateAgentSchema } from '@/lib/utils/validation';
|
|
7
|
+
|
|
8
|
+
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
try {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const session = await getServerSession(authOptions);
|
|
12
|
+
if (!session?.user?.id) {
|
|
13
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const agent = await prisma.agent.findFirst({
|
|
17
|
+
where: { id, userId: session.user.id },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!agent) {
|
|
21
|
+
return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: 'Agent not found' } }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
success: true,
|
|
26
|
+
data: {
|
|
27
|
+
...agent,
|
|
28
|
+
tools: agent.tools as string[],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return handleApiError(err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
37
|
+
try {
|
|
38
|
+
const { id } = await params;
|
|
39
|
+
const session = await getServerSession(authOptions);
|
|
40
|
+
if (!session?.user?.id) {
|
|
41
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const existing = await prisma.agent.findFirst({ where: { id, userId: session.user.id } });
|
|
45
|
+
if (!existing) {
|
|
46
|
+
return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: 'Agent not found' } }, { status: 404 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const body = await req.json();
|
|
50
|
+
const data = updateAgentSchema.parse(body);
|
|
51
|
+
|
|
52
|
+
const agent = await prisma.agent.update({
|
|
53
|
+
where: { id },
|
|
54
|
+
data: {
|
|
55
|
+
...(data.name !== undefined && { name: data.name }),
|
|
56
|
+
...(data.description !== undefined && { description: data.description }),
|
|
57
|
+
...(data.systemPrompt !== undefined && { systemPrompt: data.systemPrompt }),
|
|
58
|
+
...(data.model !== undefined && { model: data.model }),
|
|
59
|
+
...(data.temperature !== undefined && { temperature: data.temperature }),
|
|
60
|
+
...(data.maxTokens !== undefined && { maxTokens: data.maxTokens }),
|
|
61
|
+
...(data.tools !== undefined && { tools: data.tools }),
|
|
62
|
+
...(data.isPublic !== undefined && { isPublic: data.isPublic }),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return NextResponse.json({ success: true, data: agent });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return handleApiError(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
73
|
+
try {
|
|
74
|
+
const { id } = await params;
|
|
75
|
+
const session = await getServerSession(authOptions);
|
|
76
|
+
if (!session?.user?.id) {
|
|
77
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const existing = await prisma.agent.findFirst({ where: { id, userId: session.user.id } });
|
|
81
|
+
if (!existing) {
|
|
82
|
+
return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: 'Agent not found' } }, { status: 404 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Delete associated chats and messages
|
|
86
|
+
const chats = await prisma.chat.findMany({ where: { agentId: id }, select: { id: true } });
|
|
87
|
+
if (chats.length > 0) {
|
|
88
|
+
await prisma.message.deleteMany({ where: { chatId: { in: chats.map((c) => c.id) } } });
|
|
89
|
+
await prisma.chat.deleteMany({ where: { agentId: id } });
|
|
90
|
+
}
|
|
91
|
+
await prisma.agent.delete({ where: { id } });
|
|
92
|
+
|
|
93
|
+
return NextResponse.json({ success: true });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return handleApiError(err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import prisma from '@/lib/db/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
import { createAgentSchema } from '@/lib/utils/validation';
|
|
7
|
+
import { applyRateLimit } from '@/lib/rate-limit/rules';
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
try {
|
|
11
|
+
const session = await getServerSession(authOptions);
|
|
12
|
+
if (!session?.user?.id) {
|
|
13
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const agents = await prisma.agent.findMany({
|
|
17
|
+
where: { userId: session.user.id },
|
|
18
|
+
orderBy: { updatedAt: 'desc' },
|
|
19
|
+
include: {
|
|
20
|
+
_count: { select: { chats: true } },
|
|
21
|
+
chats: {
|
|
22
|
+
orderBy: { updatedAt: 'desc' },
|
|
23
|
+
take: 1,
|
|
24
|
+
include: { _count: { select: { messages: true } } },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = agents.map((agent) => ({
|
|
30
|
+
id: agent.id,
|
|
31
|
+
name: agent.name,
|
|
32
|
+
description: agent.description,
|
|
33
|
+
model: agent.model,
|
|
34
|
+
isPublic: agent.isPublic,
|
|
35
|
+
tools: agent.tools as string[],
|
|
36
|
+
temperature: agent.temperature,
|
|
37
|
+
maxTokens: agent.maxTokens,
|
|
38
|
+
messageCount: agent.chats.reduce((sum, c) => sum + c._count.messages, 0),
|
|
39
|
+
lastUsed: agent.chats[0]?.updatedAt?.toISOString() ?? null,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({ success: true, data });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return handleApiError(err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function POST(req: NextRequest) {
|
|
49
|
+
try {
|
|
50
|
+
const session = await getServerSession(authOptions);
|
|
51
|
+
if (!session?.user?.id) {
|
|
52
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rl = await applyRateLimit(session.user.id, 'agent:create');
|
|
56
|
+
if (!rl.allowed) {
|
|
57
|
+
return NextResponse.json(
|
|
58
|
+
{ success: false, error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
|
|
59
|
+
{ status: 429, headers: { 'Retry-After': String(Math.ceil((rl.resetAt - Date.now()) / 1000)) } },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const body = await req.json();
|
|
64
|
+
const data = createAgentSchema.parse(body);
|
|
65
|
+
|
|
66
|
+
const agent = await prisma.agent.create({
|
|
67
|
+
data: {
|
|
68
|
+
userId: session.user.id,
|
|
69
|
+
name: data.name,
|
|
70
|
+
description: data.description ?? '',
|
|
71
|
+
systemPrompt: data.systemPrompt,
|
|
72
|
+
model: data.model,
|
|
73
|
+
temperature: data.temperature ?? 0.7,
|
|
74
|
+
maxTokens: data.maxTokens ?? 4096,
|
|
75
|
+
tools: data.tools ?? [],
|
|
76
|
+
isPublic: data.isPublic ?? false,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return NextResponse.json({ success: true, data: agent }, { status: 201 });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return handleApiError(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import prisma from '@/lib/db/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
|
|
7
|
+
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
try {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const session = await getServerSession(authOptions);
|
|
11
|
+
if (!session?.user?.id) {
|
|
12
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const key = await prisma.apiKey.findFirst({ where: { id, userId: session.user.id } });
|
|
16
|
+
if (!key) {
|
|
17
|
+
return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: 'API key not found' } }, { status: 404 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await prisma.apiKey.delete({ where: { id } });
|
|
21
|
+
return NextResponse.json({ success: true });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return handleApiError(err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getServerSession } from 'next-auth';
|
|
3
|
+
import { authOptions } from '@/lib/auth/nextauth';
|
|
4
|
+
import prisma from '@/lib/db/client';
|
|
5
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
6
|
+
import { createApiKeySchema } from '@/lib/utils/validation';
|
|
7
|
+
import { generateApiKey } from '@/lib/utils/id';
|
|
8
|
+
import { applyRateLimit } from '@/lib/rate-limit/rules';
|
|
9
|
+
import { hash } from 'bcryptjs';
|
|
10
|
+
|
|
11
|
+
export async function GET() {
|
|
12
|
+
try {
|
|
13
|
+
const session = await getServerSession(authOptions);
|
|
14
|
+
if (!session?.user?.id) {
|
|
15
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const keys = await prisma.apiKey.findMany({
|
|
19
|
+
where: { userId: session.user.id },
|
|
20
|
+
select: { id: true, name: true, prefix: true, lastUsed: true, createdAt: true },
|
|
21
|
+
orderBy: { createdAt: 'desc' },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({ success: true, data: keys });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return handleApiError(err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function POST(req: NextRequest) {
|
|
31
|
+
try {
|
|
32
|
+
const session = await getServerSession(authOptions);
|
|
33
|
+
if (!session?.user?.id) {
|
|
34
|
+
return NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } }, { status: 401 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rl = await applyRateLimit(session.user.id, 'api-key:create');
|
|
38
|
+
if (!rl.allowed) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ success: false, error: { code: 'RATE_LIMITED', message: 'Too many API key creation attempts' } },
|
|
41
|
+
{ status: 429 },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const body = await req.json();
|
|
46
|
+
const data = createApiKeySchema.parse(body);
|
|
47
|
+
|
|
48
|
+
const { key, prefix, hashed } = generateApiKey();
|
|
49
|
+
|
|
50
|
+
const apiKey = await prisma.apiKey.create({
|
|
51
|
+
data: {
|
|
52
|
+
userId: session.user.id,
|
|
53
|
+
name: data.name,
|
|
54
|
+
key: hashed,
|
|
55
|
+
prefix,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return NextResponse.json({
|
|
60
|
+
success: true,
|
|
61
|
+
data: {
|
|
62
|
+
id: apiKey.id,
|
|
63
|
+
name: apiKey.name,
|
|
64
|
+
prefix: apiKey.prefix,
|
|
65
|
+
key, // Only returned on creation
|
|
66
|
+
createdAt: apiKey.createdAt,
|
|
67
|
+
},
|
|
68
|
+
}, { status: 201 });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return handleApiError(err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { hash } from 'bcryptjs';
|
|
3
|
+
import prisma from '@/lib/db/client';
|
|
4
|
+
import { handleApiError } from '@/lib/utils/api-response';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const registerSchema = z.object({
|
|
8
|
+
name: z.string().min(2).max(50),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
password: z.string().min(8),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const data = registerSchema.parse(body);
|
|
17
|
+
|
|
18
|
+
const existing = await prisma.user.findUnique({ where: { email: data.email } });
|
|
19
|
+
if (existing) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ success: false, error: { code: 'EMAIL_EXISTS', message: 'Email already registered' } },
|
|
22
|
+
{ status: 409 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hashedPassword = await hash(data.password, 12);
|
|
27
|
+
|
|
28
|
+
const user = await prisma.user.create({
|
|
29
|
+
data: {
|
|
30
|
+
name: data.name,
|
|
31
|
+
email: data.email,
|
|
32
|
+
// NextAuth PrismaAdapter expects the password on the Account model
|
|
33
|
+
// For credentials provider, store in a custom field or handle in callback
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Create a credentials account for the user
|
|
38
|
+
await prisma.account.create({
|
|
39
|
+
data: {
|
|
40
|
+
userId: user.id,
|
|
41
|
+
type: 'credentials',
|
|
42
|
+
provider: 'credentials',
|
|
43
|
+
providerAccountId: user.id,
|
|
44
|
+
// Store hashed password in a separate mechanism
|
|
45
|
+
// In production, use a proper credentials provider setup
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return NextResponse.json({ success: true, data: { id: user.id, email: user.email } }, { status: 201 });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return handleApiError(err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import prisma from '@/lib/db/client';
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const checks: Record<string, 'ok' | 'down' | 'not_configured'> = {
|
|
6
|
+
database: 'down',
|
|
7
|
+
redis: process.env.REDIS_URL ? 'down' : 'not_configured',
|
|
8
|
+
stripe: process.env.STRIPE_SECRET_KEY ? 'ok' : 'not_configured',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
13
|
+
checks.database = 'ok';
|
|
14
|
+
} catch {
|
|
15
|
+
checks.database = 'down';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const status = checks.database === 'ok' ? 'ok' : 'down';
|
|
19
|
+
|
|
20
|
+
return NextResponse.json({
|
|
21
|
+
status,
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
version: process.env.npm_package_version ?? '0.1.0',
|
|
24
|
+
services: checks,
|
|
25
|
+
}, { status: status === 'ok' ? 200 : 503 });
|
|
26
|
+
}
|