@cortexmemory/cli 0.27.1 → 0.27.4
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/commands/convex.js +1 -1
- package/dist/commands/convex.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +839 -141
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +89 -26
- package/dist/commands/dev.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/app-template-sync.d.ts +95 -0
- package/dist/utils/app-template-sync.d.ts.map +1 -0
- package/dist/utils/app-template-sync.js +445 -0
- package/dist/utils/app-template-sync.js.map +1 -0
- package/dist/utils/deployment-selector.d.ts +21 -0
- package/dist/utils/deployment-selector.d.ts.map +1 -1
- package/dist/utils/deployment-selector.js +32 -0
- package/dist/utils/deployment-selector.js.map +1 -1
- package/dist/utils/init/graph-setup.d.ts.map +1 -1
- package/dist/utils/init/graph-setup.js +13 -2
- package/dist/utils/init/graph-setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +128 -0
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +139 -3
- package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +333 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
- package/templates/vercel-ai-quickstart/app/globals.css +161 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +110 -11
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +117 -17
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +52 -0
- package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
- package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
- package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
- package/templates/vercel-ai-quickstart/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +11 -2
- package/templates/vercel-ai-quickstart/test-api.mjs +272 -0
- package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +454 -0
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
- package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
- package/templates/vercel-ai-quickstart/tsconfig.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useAuth } from "./AuthProvider";
|
|
5
|
+
|
|
6
|
+
type Tab = "login" | "register";
|
|
7
|
+
|
|
8
|
+
export function LoginScreen() {
|
|
9
|
+
const { login, register, error, clearError } = useAuth();
|
|
10
|
+
const [activeTab, setActiveTab] = useState<Tab>("login");
|
|
11
|
+
const [username, setUsername] = useState("");
|
|
12
|
+
const [password, setPassword] = useState("");
|
|
13
|
+
const [displayName, setDisplayName] = useState("");
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setLocalError(null);
|
|
20
|
+
clearError();
|
|
21
|
+
|
|
22
|
+
if (!username.trim()) {
|
|
23
|
+
setLocalError("Username is required");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!password) {
|
|
28
|
+
setLocalError("Password is required");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
|
|
34
|
+
if (activeTab === "login") {
|
|
35
|
+
await login(username, password);
|
|
36
|
+
} else {
|
|
37
|
+
await register(username, password, displayName || undefined);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setIsLoading(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleTabChange = (tab: Tab) => {
|
|
44
|
+
setActiveTab(tab);
|
|
45
|
+
setLocalError(null);
|
|
46
|
+
clearError();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const displayError = localError || error;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
53
|
+
<div className="w-full max-w-md">
|
|
54
|
+
{/* Logo and Title */}
|
|
55
|
+
<div className="text-center mb-8">
|
|
56
|
+
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center shadow-lg shadow-cortex-500/20">
|
|
57
|
+
<span className="text-4xl">🧠</span>
|
|
58
|
+
</div>
|
|
59
|
+
<h1 className="text-3xl font-bold mb-2">Cortex Memory Demo</h1>
|
|
60
|
+
<p className="text-gray-400">Sign in to explore AI with long-term memory</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Login/Register Card */}
|
|
64
|
+
<div className="glass rounded-2xl overflow-hidden">
|
|
65
|
+
{/* Tabs */}
|
|
66
|
+
<div className="flex border-b border-white/10">
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => handleTabChange("login")}
|
|
69
|
+
className={`flex-1 py-4 text-sm font-medium transition-colors ${
|
|
70
|
+
activeTab === "login"
|
|
71
|
+
? "text-white border-b-2 border-cortex-500"
|
|
72
|
+
: "text-gray-400 hover:text-white"
|
|
73
|
+
}`}
|
|
74
|
+
>
|
|
75
|
+
Sign In
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => handleTabChange("register")}
|
|
79
|
+
className={`flex-1 py-4 text-sm font-medium transition-colors ${
|
|
80
|
+
activeTab === "register"
|
|
81
|
+
? "text-white border-b-2 border-cortex-500"
|
|
82
|
+
: "text-gray-400 hover:text-white"
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
Create Account
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Form */}
|
|
90
|
+
<form onSubmit={handleSubmit} className="p-8 space-y-5">
|
|
91
|
+
<div>
|
|
92
|
+
<label
|
|
93
|
+
htmlFor="username"
|
|
94
|
+
className="block text-sm font-medium text-gray-300 mb-2"
|
|
95
|
+
>
|
|
96
|
+
Username
|
|
97
|
+
</label>
|
|
98
|
+
<input
|
|
99
|
+
id="username"
|
|
100
|
+
type="text"
|
|
101
|
+
value={username}
|
|
102
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
103
|
+
placeholder="Enter your username"
|
|
104
|
+
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
105
|
+
disabled={isLoading}
|
|
106
|
+
autoFocus
|
|
107
|
+
autoComplete="username"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{activeTab === "register" && (
|
|
112
|
+
<div>
|
|
113
|
+
<label
|
|
114
|
+
htmlFor="displayName"
|
|
115
|
+
className="block text-sm font-medium text-gray-300 mb-2"
|
|
116
|
+
>
|
|
117
|
+
Display Name{" "}
|
|
118
|
+
<span className="text-gray-500">(optional)</span>
|
|
119
|
+
</label>
|
|
120
|
+
<input
|
|
121
|
+
id="displayName"
|
|
122
|
+
type="text"
|
|
123
|
+
value={displayName}
|
|
124
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
125
|
+
placeholder="How should we call you?"
|
|
126
|
+
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
127
|
+
disabled={isLoading}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<div>
|
|
133
|
+
<label
|
|
134
|
+
htmlFor="password"
|
|
135
|
+
className="block text-sm font-medium text-gray-300 mb-2"
|
|
136
|
+
>
|
|
137
|
+
Password
|
|
138
|
+
</label>
|
|
139
|
+
<input
|
|
140
|
+
id="password"
|
|
141
|
+
type="password"
|
|
142
|
+
value={password}
|
|
143
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
144
|
+
placeholder="Enter your password"
|
|
145
|
+
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
146
|
+
disabled={isLoading}
|
|
147
|
+
autoComplete={activeTab === "login" ? "current-password" : "new-password"}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{displayError && (
|
|
152
|
+
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 text-sm">
|
|
153
|
+
{displayError}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
<button
|
|
158
|
+
type="submit"
|
|
159
|
+
disabled={isLoading || !username || !password}
|
|
160
|
+
className="w-full py-3 bg-cortex-600 hover:bg-cortex-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl font-medium transition-colors flex items-center justify-center gap-2"
|
|
161
|
+
>
|
|
162
|
+
{isLoading ? (
|
|
163
|
+
<>
|
|
164
|
+
<svg
|
|
165
|
+
className="animate-spin h-5 w-5"
|
|
166
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
167
|
+
fill="none"
|
|
168
|
+
viewBox="0 0 24 24"
|
|
169
|
+
>
|
|
170
|
+
<circle
|
|
171
|
+
className="opacity-25"
|
|
172
|
+
cx="12"
|
|
173
|
+
cy="12"
|
|
174
|
+
r="10"
|
|
175
|
+
stroke="currentColor"
|
|
176
|
+
strokeWidth="4"
|
|
177
|
+
/>
|
|
178
|
+
<path
|
|
179
|
+
className="opacity-75"
|
|
180
|
+
fill="currentColor"
|
|
181
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
182
|
+
/>
|
|
183
|
+
</svg>
|
|
184
|
+
{activeTab === "login" ? "Signing in..." : "Creating account..."}
|
|
185
|
+
</>
|
|
186
|
+
) : activeTab === "login" ? (
|
|
187
|
+
"Sign In"
|
|
188
|
+
) : (
|
|
189
|
+
"Create Account"
|
|
190
|
+
)}
|
|
191
|
+
</button>
|
|
192
|
+
</form>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Demo hint */}
|
|
196
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
197
|
+
Create an account to start chatting with memory-enabled AI
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Configuration for Vercel AI Quickstart
|
|
3
|
+
*
|
|
4
|
+
* Three test projects:
|
|
5
|
+
* - unit: Fast unit tests with mocked dependencies
|
|
6
|
+
* - integration: Integration tests for API routes with mocked SDK
|
|
7
|
+
* - e2e: End-to-end tests with real Cortex backend (requires CONVEX_URL, OPENAI_API_KEY)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const baseConfig = {
|
|
11
|
+
preset: "ts-jest",
|
|
12
|
+
testEnvironment: "node",
|
|
13
|
+
moduleNameMapper: {
|
|
14
|
+
// Handle path aliases from tsconfig
|
|
15
|
+
"^@/(.*)$": "<rootDir>/$1",
|
|
16
|
+
},
|
|
17
|
+
transform: {
|
|
18
|
+
"^.+\\.tsx?$": [
|
|
19
|
+
"ts-jest",
|
|
20
|
+
{
|
|
21
|
+
tsconfig: "tsconfig.json",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
setupFilesAfterEnv: ["<rootDir>/tests/helpers/setup.ts"],
|
|
26
|
+
// Ignore Next.js build output
|
|
27
|
+
testPathIgnorePatterns: ["/node_modules/", "/.next/"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
...baseConfig,
|
|
32
|
+
projects: [
|
|
33
|
+
{
|
|
34
|
+
...baseConfig,
|
|
35
|
+
displayName: "unit",
|
|
36
|
+
testMatch: ["<rootDir>/tests/unit/**/*.test.ts"],
|
|
37
|
+
testTimeout: 10000,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
...baseConfig,
|
|
41
|
+
displayName: "integration",
|
|
42
|
+
testMatch: ["<rootDir>/tests/integration/**/*.test.ts"],
|
|
43
|
+
testTimeout: 30000,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
...baseConfig,
|
|
47
|
+
displayName: "e2e",
|
|
48
|
+
testMatch: ["<rootDir>/tests/e2e/**/*.test.ts"],
|
|
49
|
+
testTimeout: 120000, // 2 minutes for real network calls
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-Enabled Agent (AI SDK v6)
|
|
3
|
+
*
|
|
4
|
+
* This file demonstrates how to create a reusable agent with
|
|
5
|
+
* Cortex Memory integration using AI SDK v6's ToolLoopAgent.
|
|
6
|
+
*
|
|
7
|
+
* The agent:
|
|
8
|
+
* - Automatically injects relevant memories into context
|
|
9
|
+
* - Can be used with both generate() and stream()
|
|
10
|
+
* - Supports type-safe call options for userId, memorySpaceId, etc.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const result = await memoryAgent.generate({
|
|
15
|
+
* prompt: 'What do you remember about me?',
|
|
16
|
+
* options: {
|
|
17
|
+
* userId: 'user_123',
|
|
18
|
+
* memorySpaceId: 'my-app',
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { ToolLoopAgent, tool, stepCountIs } from "ai";
|
|
25
|
+
import { openai } from "@ai-sdk/openai";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import {
|
|
28
|
+
createCortexCallOptionsSchema,
|
|
29
|
+
createMemoryPrepareCall,
|
|
30
|
+
type CortexCallOptions,
|
|
31
|
+
} from "@cortexmemory/vercel-ai-provider";
|
|
32
|
+
|
|
33
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
+
// Agent Configuration
|
|
35
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
|
|
37
|
+
const SYSTEM_PROMPT = `You are a helpful AI assistant with long-term memory powered by Cortex.
|
|
38
|
+
|
|
39
|
+
Your capabilities:
|
|
40
|
+
- You remember everything users tell you across conversations
|
|
41
|
+
- You can recall facts, preferences, and context from past interactions
|
|
42
|
+
- You naturally reference what you've learned about the user
|
|
43
|
+
|
|
44
|
+
Behavior guidelines:
|
|
45
|
+
- When you remember something from a previous conversation, mention it naturally
|
|
46
|
+
- If asked about something you learned, reference it specifically
|
|
47
|
+
- Be conversational and friendly
|
|
48
|
+
- Help demonstrate the memory system by showing what you remember`;
|
|
49
|
+
|
|
50
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
// Memory Agent Definition
|
|
52
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A memory-enabled agent using AI SDK v6's ToolLoopAgent.
|
|
56
|
+
*
|
|
57
|
+
* This agent demonstrates:
|
|
58
|
+
* - callOptionsSchema for type-safe runtime config (userId, memorySpaceId, etc.)
|
|
59
|
+
* - prepareCall for automatic memory context injection via Cortex's recall() API
|
|
60
|
+
* - Built-in tools for memory operations (optional)
|
|
61
|
+
*
|
|
62
|
+
* The callOptionsSchema ensures TypeScript type safety when calling the agent:
|
|
63
|
+
* - userId: required for memory isolation per user
|
|
64
|
+
* - memorySpaceId: required for data partitioning
|
|
65
|
+
* - conversationId: optional for session continuity
|
|
66
|
+
* - agentId: optional agent identifier
|
|
67
|
+
*/
|
|
68
|
+
export const memoryAgent = new ToolLoopAgent({
|
|
69
|
+
id: "cortex-memory-agent",
|
|
70
|
+
model: openai("gpt-4o-mini"),
|
|
71
|
+
instructions: SYSTEM_PROMPT,
|
|
72
|
+
|
|
73
|
+
// ┌─────────────────────────────────────────────────────────────────┐
|
|
74
|
+
// │ callOptionsSchema: Type-Safe Runtime Configuration │
|
|
75
|
+
// │ │
|
|
76
|
+
// │ This Zod schema defines what options must/can be passed when │
|
|
77
|
+
// │ calling the agent. AI SDK v6 validates these at runtime. │
|
|
78
|
+
// │ │
|
|
79
|
+
// │ Example usage: │
|
|
80
|
+
// │ await memoryAgent.generate({ │
|
|
81
|
+
// │ prompt: 'Hello!', │
|
|
82
|
+
// │ options: { userId: 'u1', memorySpaceId: 'app1' }, // typed!│
|
|
83
|
+
// │ }); │
|
|
84
|
+
// └─────────────────────────────────────────────────────────────────┘
|
|
85
|
+
callOptionsSchema: createCortexCallOptionsSchema(),
|
|
86
|
+
|
|
87
|
+
// ┌─────────────────────────────────────────────────────────────────┐
|
|
88
|
+
// │ prepareCall: Memory Context Injection │
|
|
89
|
+
// │ │
|
|
90
|
+
// │ Called before each agent invocation. This hook: │
|
|
91
|
+
// │ 1. Extracts the user's query from messages │
|
|
92
|
+
// │ 2. Calls Cortex memory.recall() with userId + memorySpaceId │
|
|
93
|
+
// │ 3. Injects the returned context into instructions │
|
|
94
|
+
// │ │
|
|
95
|
+
// │ The recall() API orchestrates all memory layers: │
|
|
96
|
+
// │ - Vector memories (semantic search) │
|
|
97
|
+
// │ - Facts (extracted knowledge) │
|
|
98
|
+
// │ - Graph relationships (if configured) │
|
|
99
|
+
// └─────────────────────────────────────────────────────────────────┘
|
|
100
|
+
prepareCall: createMemoryPrepareCall({
|
|
101
|
+
convexUrl: process.env.CONVEX_URL!,
|
|
102
|
+
maxMemories: 20, // Max items to inject from recall
|
|
103
|
+
includeFacts: true, // Include Layer 3 facts
|
|
104
|
+
includeVector: true, // Include Layer 2 vector memories
|
|
105
|
+
includeGraph: true, // Expand through graph relationships
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
// Default to 5 steps (sufficient for most chat interactions)
|
|
109
|
+
stopWhen: stepCountIs(5),
|
|
110
|
+
|
|
111
|
+
// Optional: Add memory-specific tools for explicit memory operations
|
|
112
|
+
// Uncomment to let the agent actively search/store memories
|
|
113
|
+
/*
|
|
114
|
+
tools: {
|
|
115
|
+
searchMemory: tool({
|
|
116
|
+
description: 'Search for specific memories about the user',
|
|
117
|
+
inputSchema: z.object({
|
|
118
|
+
query: z.string().describe('What to search for in memory'),
|
|
119
|
+
}),
|
|
120
|
+
execute: async ({ query }, { options }) => {
|
|
121
|
+
const { Cortex } = await import('@cortexmemory/sdk');
|
|
122
|
+
const cortex = new Cortex({ convexUrl: process.env.CONVEX_URL! });
|
|
123
|
+
const result = await cortex.memory.recall({
|
|
124
|
+
memorySpaceId: options.memorySpaceId,
|
|
125
|
+
query,
|
|
126
|
+
userId: options.userId,
|
|
127
|
+
limit: 5,
|
|
128
|
+
});
|
|
129
|
+
return result.context || 'No memories found.';
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
*/
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
137
|
+
// Type Exports for Client Components
|
|
138
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Inferred UIMessage type for this agent.
|
|
142
|
+
*
|
|
143
|
+
* Use this in your client components for full type safety:
|
|
144
|
+
*
|
|
145
|
+
* ```typescript
|
|
146
|
+
* import { useChat } from '@ai-sdk/react';
|
|
147
|
+
* import type { MemoryAgentUIMessage } from '@/lib/agents/memory-agent';
|
|
148
|
+
*
|
|
149
|
+
* const { messages } = useChat<MemoryAgentUIMessage>();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export type MemoryAgentUIMessage = {
|
|
153
|
+
id: string;
|
|
154
|
+
role: "user" | "assistant" | "system";
|
|
155
|
+
createdAt?: Date;
|
|
156
|
+
parts?: Array<
|
|
157
|
+
| { type: "text"; text: string }
|
|
158
|
+
| { type: "tool-invocation"; toolCallId: string; state: string }
|
|
159
|
+
>;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Re-export call options type for convenience.
|
|
164
|
+
*/
|
|
165
|
+
export type { CortexCallOptions };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cortex SDK Client
|
|
3
|
+
*
|
|
4
|
+
* Shared Cortex client instance for API routes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Cortex } from "@cortexmemory/sdk";
|
|
8
|
+
|
|
9
|
+
let cortexClient: Cortex | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get or create a Cortex SDK client
|
|
13
|
+
*/
|
|
14
|
+
export function getCortex(): Cortex {
|
|
15
|
+
if (!cortexClient) {
|
|
16
|
+
const convexUrl = process.env.CONVEX_URL;
|
|
17
|
+
if (!convexUrl) {
|
|
18
|
+
throw new Error("CONVEX_URL environment variable is required");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
cortexClient = new Cortex({
|
|
22
|
+
convexUrl,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return cortexClient;
|
|
27
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Hashing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Crypto API (PBKDF2) for password hashing.
|
|
5
|
+
* Works in Edge runtime - no external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ITERATIONS = 100000;
|
|
9
|
+
const KEY_LENGTH = 256;
|
|
10
|
+
const SALT_LENGTH = 16;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hash a password using PBKDF2
|
|
14
|
+
*
|
|
15
|
+
* @param password - Plain text password to hash
|
|
16
|
+
* @returns Hashed password as base64 string (format: salt:hash)
|
|
17
|
+
*/
|
|
18
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
const passwordBuffer = encoder.encode(password);
|
|
21
|
+
|
|
22
|
+
// Generate random salt
|
|
23
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
24
|
+
|
|
25
|
+
// Import password as key material
|
|
26
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
27
|
+
"raw",
|
|
28
|
+
passwordBuffer,
|
|
29
|
+
"PBKDF2",
|
|
30
|
+
false,
|
|
31
|
+
["deriveBits"]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Derive key using PBKDF2
|
|
35
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
36
|
+
{
|
|
37
|
+
name: "PBKDF2",
|
|
38
|
+
salt,
|
|
39
|
+
iterations: ITERATIONS,
|
|
40
|
+
hash: "SHA-256",
|
|
41
|
+
},
|
|
42
|
+
keyMaterial,
|
|
43
|
+
KEY_LENGTH
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Convert to base64 strings
|
|
47
|
+
const saltB64 = btoa(String.fromCharCode(...salt));
|
|
48
|
+
const hashB64 = btoa(String.fromCharCode(...new Uint8Array(derivedBits)));
|
|
49
|
+
|
|
50
|
+
// Return combined format: salt:hash
|
|
51
|
+
return `${saltB64}:${hashB64}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Verify a password against a stored hash
|
|
56
|
+
*
|
|
57
|
+
* @param password - Plain text password to verify
|
|
58
|
+
* @param storedHash - Previously hashed password (format: salt:hash)
|
|
59
|
+
* @returns True if password matches
|
|
60
|
+
*/
|
|
61
|
+
export async function verifyPassword(
|
|
62
|
+
password: string,
|
|
63
|
+
storedHash: string
|
|
64
|
+
): Promise<boolean> {
|
|
65
|
+
try {
|
|
66
|
+
const [saltB64, expectedHashB64] = storedHash.split(":");
|
|
67
|
+
if (!saltB64 || !expectedHashB64) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const passwordBuffer = encoder.encode(password);
|
|
73
|
+
|
|
74
|
+
// Decode salt from base64
|
|
75
|
+
const saltStr = atob(saltB64);
|
|
76
|
+
const salt = new Uint8Array(saltStr.length);
|
|
77
|
+
for (let i = 0; i < saltStr.length; i++) {
|
|
78
|
+
salt[i] = saltStr.charCodeAt(i);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Import password as key material
|
|
82
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
83
|
+
"raw",
|
|
84
|
+
passwordBuffer,
|
|
85
|
+
"PBKDF2",
|
|
86
|
+
false,
|
|
87
|
+
["deriveBits"]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Derive key using same parameters
|
|
91
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
92
|
+
{
|
|
93
|
+
name: "PBKDF2",
|
|
94
|
+
salt,
|
|
95
|
+
iterations: ITERATIONS,
|
|
96
|
+
hash: "SHA-256",
|
|
97
|
+
},
|
|
98
|
+
keyMaterial,
|
|
99
|
+
KEY_LENGTH
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Compare hashes
|
|
103
|
+
const hashB64 = btoa(String.fromCharCode(...new Uint8Array(derivedBits)));
|
|
104
|
+
return hashB64 === expectedHashB64;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate a secure random session token
|
|
112
|
+
*
|
|
113
|
+
* @returns Random token as hex string
|
|
114
|
+
*/
|
|
115
|
+
export function generateSessionToken(): string {
|
|
116
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
117
|
+
return Array.from(bytes)
|
|
118
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
119
|
+
.join("");
|
|
120
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version detection utilities for displaying SDK versions in the UI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface VersionInfo {
|
|
6
|
+
cortexSdk: string;
|
|
7
|
+
aiSdk: string;
|
|
8
|
+
aiSdkMajor: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect installed SDK versions at runtime.
|
|
13
|
+
* Returns version strings for display in the UI.
|
|
14
|
+
*/
|
|
15
|
+
export async function detectVersions(): Promise<VersionInfo> {
|
|
16
|
+
let cortexSdk = "unknown";
|
|
17
|
+
let aiSdk = "unknown";
|
|
18
|
+
let aiSdkMajor = 5;
|
|
19
|
+
|
|
20
|
+
// Detect Cortex SDK version
|
|
21
|
+
try {
|
|
22
|
+
const cortexModule = await import("@cortexmemory/sdk");
|
|
23
|
+
// Check for version export or use package version
|
|
24
|
+
if ("VERSION" in cortexModule) {
|
|
25
|
+
cortexSdk = cortexModule.VERSION as string;
|
|
26
|
+
} else {
|
|
27
|
+
// Fallback: try to detect from package
|
|
28
|
+
cortexSdk = "0.24.0";
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
cortexSdk = "0.24.0";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Detect AI SDK version by checking for v6-specific exports
|
|
35
|
+
try {
|
|
36
|
+
const aiModule = await import("ai");
|
|
37
|
+
|
|
38
|
+
// v6 has these specific exports
|
|
39
|
+
const hasV6Features =
|
|
40
|
+
"ToolLoopAgent" in aiModule ||
|
|
41
|
+
"createAgentUIStreamResponse" in aiModule ||
|
|
42
|
+
"Output" in aiModule;
|
|
43
|
+
|
|
44
|
+
if (hasV6Features) {
|
|
45
|
+
aiSdkMajor = 6;
|
|
46
|
+
aiSdk = "v6";
|
|
47
|
+
} else {
|
|
48
|
+
aiSdkMajor = 5;
|
|
49
|
+
aiSdk = "v5";
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
aiSdk = "v5";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
cortexSdk,
|
|
57
|
+
aiSdk,
|
|
58
|
+
aiSdkMajor,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
1
3
|
/** @type {import('next').NextConfig} */
|
|
2
4
|
const nextConfig = {
|
|
3
5
|
transpilePackages: ["@cortexmemory/sdk", "@cortexmemory/vercel-ai-provider"],
|
|
4
6
|
serverExternalPackages: ["convex"],
|
|
7
|
+
experimental: {
|
|
8
|
+
// Ensure linked packages resolve dependencies from this project's node_modules
|
|
9
|
+
externalDir: true,
|
|
10
|
+
},
|
|
11
|
+
// Empty turbopack config to silence the warning about missing turbopack config
|
|
12
|
+
turbopack: {},
|
|
13
|
+
// Webpack configuration for module resolution when SDK is file-linked
|
|
14
|
+
// This is needed because the SDK uses dynamic imports that don't resolve
|
|
15
|
+
// correctly from a linked package's location during local development
|
|
16
|
+
webpack: (config) => {
|
|
17
|
+
config.resolve.alias = {
|
|
18
|
+
...config.resolve.alias,
|
|
19
|
+
"@anthropic-ai/sdk": path.resolve(__dirname, "node_modules/@anthropic-ai/sdk"),
|
|
20
|
+
"openai": path.resolve(__dirname, "node_modules/openai"),
|
|
21
|
+
"neo4j-driver": path.resolve(__dirname, "node_modules/neo4j-driver"),
|
|
22
|
+
};
|
|
23
|
+
return config;
|
|
24
|
+
},
|
|
5
25
|
};
|
|
6
26
|
|
|
7
27
|
module.exports = nextConfig;
|
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"description": "Cortex Memory + Vercel AI SDK Quickstart Demo",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "next dev",
|
|
9
|
-
"build": "next build",
|
|
8
|
+
"dev": "next dev --webpack",
|
|
9
|
+
"build": "next build --webpack",
|
|
10
10
|
"start": "next start",
|
|
11
11
|
"lint": "next lint",
|
|
12
|
+
"test": "jest --selectProjects unit integration",
|
|
13
|
+
"test:unit": "jest --selectProjects unit",
|
|
14
|
+
"test:integration": "jest --selectProjects integration",
|
|
15
|
+
"test:e2e": "jest --selectProjects e2e",
|
|
16
|
+
"test:all": "jest",
|
|
17
|
+
"test:watch": "jest --watch",
|
|
12
18
|
"convex:dev": "convex dev",
|
|
13
19
|
"convex:deploy": "convex deploy"
|
|
14
20
|
},
|
|
@@ -30,12 +36,15 @@
|
|
|
30
36
|
},
|
|
31
37
|
"devDependencies": {
|
|
32
38
|
"@tailwindcss/postcss": "^4.1.18",
|
|
39
|
+
"@types/jest": "^29.5.14",
|
|
33
40
|
"@types/node": "^25.0.3",
|
|
34
41
|
"@types/react": "^19.2.7",
|
|
35
42
|
"@types/react-dom": "^19.2.3",
|
|
36
43
|
"autoprefixer": "^10.4.23",
|
|
44
|
+
"jest": "^29.7.0",
|
|
37
45
|
"postcss": "^8.5.6",
|
|
38
46
|
"tailwindcss": "^4.1.18",
|
|
47
|
+
"ts-jest": "^29.2.5",
|
|
39
48
|
"typescript": "^5.9.3"
|
|
40
49
|
}
|
|
41
50
|
}
|