@cortexmemory/cli 0.27.1 → 0.27.3

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 (46) hide show
  1. package/dist/commands/convex.js +1 -1
  2. package/dist/commands/convex.js.map +1 -1
  3. package/dist/commands/deploy.d.ts +1 -1
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +771 -144
  6. package/dist/commands/deploy.js.map +1 -1
  7. package/dist/commands/dev.d.ts.map +1 -1
  8. package/dist/commands/dev.js +89 -26
  9. package/dist/commands/dev.js.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/utils/app-template-sync.d.ts +95 -0
  12. package/dist/utils/app-template-sync.d.ts.map +1 -0
  13. package/dist/utils/app-template-sync.js +425 -0
  14. package/dist/utils/app-template-sync.js.map +1 -0
  15. package/dist/utils/deployment-selector.d.ts +21 -0
  16. package/dist/utils/deployment-selector.d.ts.map +1 -1
  17. package/dist/utils/deployment-selector.js +32 -0
  18. package/dist/utils/deployment-selector.js.map +1 -1
  19. package/dist/utils/init/graph-setup.d.ts.map +1 -1
  20. package/dist/utils/init/graph-setup.js +13 -2
  21. package/dist/utils/init/graph-setup.js.map +1 -1
  22. package/package.json +1 -1
  23. package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
  24. package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +83 -0
  25. package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
  26. package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
  27. package/templates/vercel-ai-quickstart/app/api/chat/route.ts +83 -2
  28. package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
  29. package/templates/vercel-ai-quickstart/app/globals.css +161 -0
  30. package/templates/vercel-ai-quickstart/app/page.tsx +93 -8
  31. package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
  32. package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
  33. package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
  34. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +113 -16
  35. package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
  36. package/templates/vercel-ai-quickstart/jest.config.js +45 -0
  37. package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
  38. package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
  39. package/templates/vercel-ai-quickstart/next.config.js +20 -0
  40. package/templates/vercel-ai-quickstart/package.json +7 -2
  41. package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
  42. package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
  43. package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
  44. package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
  45. package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
  46. package/templates/vercel-ai-quickstart/tsconfig.json +1 -1
@@ -25,6 +25,7 @@ interface LayerUpdateData {
25
25
  interface ChatInterfaceProps {
26
26
  memorySpaceId: string;
27
27
  userId: string;
28
+ conversationId: string | null;
28
29
  onOrchestrationStart?: () => void;
29
30
  onLayerUpdate?: (
30
31
  layer: MemoryLayer,
@@ -36,14 +37,17 @@ interface ChatInterfaceProps {
36
37
  },
37
38
  ) => void;
38
39
  onReset?: () => void;
40
+ onConversationUpdate?: (conversationId: string, title: string) => void;
39
41
  }
40
42
 
41
43
  export function ChatInterface({
42
44
  memorySpaceId,
43
45
  userId,
46
+ conversationId,
44
47
  onOrchestrationStart,
45
48
  onLayerUpdate,
46
49
  onReset,
50
+ onConversationUpdate,
47
51
  }: ChatInterfaceProps) {
48
52
  const messagesEndRef = useRef<HTMLDivElement>(null);
49
53
  const [input, setInput] = useState("");
@@ -54,38 +58,56 @@ export function ChatInterface({
54
58
  "What do you remember about me?",
55
59
  ]);
56
60
 
57
- // Create transport with body parameters - memoized to prevent recreation
61
+ // Track conversation ID in a ref for immediate access in callbacks
62
+ // This ensures the latest conversationId is always used when sending messages,
63
+ // even before React re-renders and recreates the transport
64
+ const conversationIdRef = useRef<string | null>(conversationId);
65
+ useEffect(() => {
66
+ conversationIdRef.current = conversationId;
67
+ }, [conversationId]);
68
+
69
+ // Create transport with a function that reads from ref for conversationId
70
+ // This ensures we always send the latest conversationId
58
71
  const transport = useMemo(
59
72
  () =>
60
73
  new DefaultChatTransport({
61
74
  api: "/api/chat",
62
- body: { memorySpaceId, userId },
75
+ // Use a function to get body so it reads latest conversationId from ref
76
+ body: () => ({
77
+ memorySpaceId,
78
+ userId,
79
+ conversationId: conversationIdRef.current,
80
+ }),
63
81
  }),
64
- [memorySpaceId, userId],
82
+ [memorySpaceId, userId], // Note: conversationId removed - ref handles updates
65
83
  );
66
84
 
67
85
  // Handle layer data parts from the stream
68
86
  const handleDataPart = useCallback(
69
- (dataPart: any) => {
70
- if (dataPart.type === "data-orchestration-start") {
87
+ (dataPart: unknown) => {
88
+ const part = dataPart as { type: string; data?: unknown };
89
+ if (part.type === "data-orchestration-start") {
71
90
  onOrchestrationStart?.();
72
91
  }
73
92
 
74
- if (dataPart.type === "data-layer-update") {
75
- const event = dataPart.data as LayerUpdateData;
93
+ if (part.type === "data-layer-update") {
94
+ const event = part.data as LayerUpdateData;
76
95
  onLayerUpdate?.(event.layer, event.status, event.data, {
77
96
  action: event.revisionAction,
78
97
  supersededFacts: event.supersededFacts,
79
98
  });
80
99
  }
81
100
 
82
- // orchestration-complete is informational - layer diagram already updated
83
- // via individual layer events
101
+ // Handle conversation title update
102
+ if (part.type === "data-conversation-update") {
103
+ const update = part.data as { conversationId: string; title: string };
104
+ onConversationUpdate?.(update.conversationId, update.title);
105
+ }
84
106
  },
85
- [onOrchestrationStart, onLayerUpdate],
107
+ [onOrchestrationStart, onLayerUpdate, onConversationUpdate],
86
108
  );
87
109
 
88
- const { messages, sendMessage, status } = useChat({
110
+ const { messages, sendMessage, status, setMessages } = useChat({
89
111
  transport,
90
112
  onData: handleDataPart,
91
113
  onError: (error) => {
@@ -93,6 +115,53 @@ export function ChatInterface({
93
115
  },
94
116
  });
95
117
 
118
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
119
+
120
+ // Load messages when conversation changes
121
+ useEffect(() => {
122
+ // Clear messages first
123
+ setMessages([]);
124
+
125
+ // If no conversation selected, nothing more to do
126
+ if (!conversationId) {
127
+ return;
128
+ }
129
+
130
+ // Fetch conversation history
131
+ const loadConversationHistory = async () => {
132
+ setIsLoadingHistory(true);
133
+ try {
134
+ const response = await fetch(
135
+ `/api/conversations?conversationId=${encodeURIComponent(conversationId)}`
136
+ );
137
+
138
+ if (!response.ok) {
139
+ console.error("Failed to load conversation history");
140
+ return;
141
+ }
142
+
143
+ const data = await response.json();
144
+
145
+ if (data.messages && data.messages.length > 0) {
146
+ // Transform to the format expected by useChat
147
+ const loadedMessages = data.messages.map((msg: { id: string; role: string; content: string; createdAt: string }) => ({
148
+ id: msg.id,
149
+ role: msg.role,
150
+ content: msg.content,
151
+ createdAt: new Date(msg.createdAt),
152
+ }));
153
+ setMessages(loadedMessages);
154
+ }
155
+ } catch (error) {
156
+ console.error("Error loading conversation history:", error);
157
+ } finally {
158
+ setIsLoadingHistory(false);
159
+ }
160
+ };
161
+
162
+ loadConversationHistory();
163
+ }, [conversationId, setMessages]);
164
+
96
165
  // Determine if we're actively streaming (only time to show typing indicator)
97
166
  const isStreaming = status === "streaming";
98
167
 
@@ -121,14 +190,14 @@ export function ChatInterface({
121
190
  };
122
191
 
123
192
  // Extract text content from message parts (AI SDK v5 format)
124
- const getMessageContent = (message: any): string => {
193
+ const getMessageContent = (message: { content?: string; parts?: Array<{ type: string; text?: string }> }): string => {
125
194
  if (typeof message.content === "string") {
126
195
  return message.content;
127
196
  }
128
197
  if (message.parts) {
129
198
  return message.parts
130
- .filter((part: any) => part.type === "text")
131
- .map((part: any) => part.text)
199
+ .filter((part) => part.type === "text")
200
+ .map((part) => part.text)
132
201
  .join("");
133
202
  }
134
203
  return "";
@@ -138,13 +207,41 @@ export function ChatInterface({
138
207
  <div className="flex flex-col h-full">
139
208
  {/* Messages */}
140
209
  <div className="flex-1 overflow-y-auto p-4 space-y-4">
141
- {messages.length === 0 && (
210
+ {isLoadingHistory && (
211
+ <div className="flex items-center justify-center py-12">
212
+ <div className="flex flex-col items-center gap-3">
213
+ <svg
214
+ className="animate-spin h-8 w-8 text-cortex-500"
215
+ xmlns="http://www.w3.org/2000/svg"
216
+ fill="none"
217
+ viewBox="0 0 24 24"
218
+ >
219
+ <circle
220
+ className="opacity-25"
221
+ cx="12"
222
+ cy="12"
223
+ r="10"
224
+ stroke="currentColor"
225
+ strokeWidth="4"
226
+ />
227
+ <path
228
+ className="opacity-75"
229
+ fill="currentColor"
230
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
231
+ />
232
+ </svg>
233
+ <span className="text-gray-400 text-sm">Loading conversation...</span>
234
+ </div>
235
+ </div>
236
+ )}
237
+
238
+ {!isLoadingHistory && messages.length === 0 && (
142
239
  <div className="text-center py-12">
143
240
  <div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-cortex-500/20 to-cortex-700/20 flex items-center justify-center">
144
241
  <span className="text-3xl">🧠</span>
145
242
  </div>
146
243
  <h2 className="text-xl font-semibold mb-2">
147
- Welcome to Cortex Memory Demo
244
+ {conversationId ? "Continue your conversation" : "Start a new conversation"}
148
245
  </h2>
149
246
  <p className="text-gray-400 max-w-md mx-auto mb-6">
150
247
  This demo shows how Cortex orchestrates memory across multiple
@@ -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,45 @@
1
+ /**
2
+ * Jest Configuration for Vercel AI Quickstart
3
+ *
4
+ * Two test projects:
5
+ * - unit: Fast unit tests with mocked dependencies
6
+ * - integration: Integration tests for API routes with mocked SDK
7
+ */
8
+
9
+ const baseConfig = {
10
+ preset: "ts-jest",
11
+ testEnvironment: "node",
12
+ moduleNameMapper: {
13
+ // Handle path aliases from tsconfig
14
+ "^@/(.*)$": "<rootDir>/$1",
15
+ },
16
+ transform: {
17
+ "^.+\\.tsx?$": [
18
+ "ts-jest",
19
+ {
20
+ tsconfig: "tsconfig.json",
21
+ },
22
+ ],
23
+ },
24
+ setupFilesAfterEnv: ["<rootDir>/tests/helpers/setup.ts"],
25
+ // Ignore Next.js build output
26
+ testPathIgnorePatterns: ["/node_modules/", "/.next/"],
27
+ };
28
+
29
+ module.exports = {
30
+ ...baseConfig,
31
+ projects: [
32
+ {
33
+ ...baseConfig,
34
+ displayName: "unit",
35
+ testMatch: ["<rootDir>/tests/unit/**/*.test.ts"],
36
+ testTimeout: 10000,
37
+ },
38
+ {
39
+ ...baseConfig,
40
+ displayName: "integration",
41
+ testMatch: ["<rootDir>/tests/integration/**/*.test.ts"],
42
+ testTimeout: 30000,
43
+ },
44
+ ],
45
+ };
@@ -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
+ }
@@ -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,12 @@
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",
13
+ "test:watch": "jest --watch",
12
14
  "convex:dev": "convex dev",
13
15
  "convex:deploy": "convex deploy"
14
16
  },
@@ -30,12 +32,15 @@
30
32
  },
31
33
  "devDependencies": {
32
34
  "@tailwindcss/postcss": "^4.1.18",
35
+ "@types/jest": "^29.5.14",
33
36
  "@types/node": "^25.0.3",
34
37
  "@types/react": "^19.2.7",
35
38
  "@types/react-dom": "^19.2.3",
36
39
  "autoprefixer": "^10.4.23",
40
+ "jest": "^29.7.0",
37
41
  "postcss": "^8.5.6",
38
42
  "tailwindcss": "^4.1.18",
43
+ "ts-jest": "^29.2.5",
39
44
  "typescript": "^5.9.3"
40
45
  }
41
46
  }