@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.
- 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 +771 -144
- 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 +425 -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 +83 -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 +83 -2
- 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 +93 -8
- 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 +113 -16
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +45 -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/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +7 -2
- 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
|
@@ -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
|
-
//
|
|
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
|
|
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:
|
|
70
|
-
|
|
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 (
|
|
75
|
-
const event =
|
|
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
|
-
//
|
|
83
|
-
|
|
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:
|
|
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
|
|
131
|
-
.map((part
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
}
|