@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.
Files changed (51) 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 +839 -141
  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 +445 -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 +128 -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 +139 -3
  28. package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +333 -0
  29. package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
  30. package/templates/vercel-ai-quickstart/app/globals.css +161 -0
  31. package/templates/vercel-ai-quickstart/app/page.tsx +110 -11
  32. package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
  33. package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
  34. package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
  35. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +117 -17
  36. package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
  37. package/templates/vercel-ai-quickstart/jest.config.js +52 -0
  38. package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
  39. package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
  40. package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
  41. package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
  42. package/templates/vercel-ai-quickstart/next.config.js +20 -0
  43. package/templates/vercel-ai-quickstart/package.json +11 -2
  44. package/templates/vercel-ai-quickstart/test-api.mjs +272 -0
  45. package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +454 -0
  46. package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
  47. package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
  48. package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
  49. package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
  50. package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
  51. package/templates/vercel-ai-quickstart/tsconfig.json +1 -1
@@ -0,0 +1,323 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { useAuth } from "./AuthProvider";
5
+
6
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7
+ // Types
8
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9
+
10
+ interface Conversation {
11
+ id: string;
12
+ title: string;
13
+ createdAt: number;
14
+ updatedAt: number;
15
+ messageCount: number;
16
+ }
17
+
18
+ interface ChatHistorySidebarProps {
19
+ memorySpaceId: string;
20
+ currentConversationId: string | null;
21
+ onSelectConversation: (conversationId: string) => void;
22
+ onNewChat: () => void;
23
+ }
24
+
25
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26
+ // Helpers
27
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28
+
29
+ function groupByDate(conversations: Conversation[]): Record<string, Conversation[]> {
30
+ const now = new Date();
31
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
32
+ const yesterday = today - 86400000;
33
+ const weekAgo = today - 7 * 86400000;
34
+
35
+ const groups: Record<string, Conversation[]> = {
36
+ Today: [],
37
+ Yesterday: [],
38
+ "Last 7 Days": [],
39
+ Older: [],
40
+ };
41
+
42
+ for (const conv of conversations) {
43
+ const convDate = new Date(conv.updatedAt);
44
+ const convDay = new Date(
45
+ convDate.getFullYear(),
46
+ convDate.getMonth(),
47
+ convDate.getDate()
48
+ ).getTime();
49
+
50
+ if (convDay >= today) {
51
+ groups.Today.push(conv);
52
+ } else if (convDay >= yesterday) {
53
+ groups.Yesterday.push(conv);
54
+ } else if (convDay >= weekAgo) {
55
+ groups["Last 7 Days"].push(conv);
56
+ } else {
57
+ groups.Older.push(conv);
58
+ }
59
+ }
60
+
61
+ return groups;
62
+ }
63
+
64
+ function formatTime(timestamp: number): string {
65
+ return new Date(timestamp).toLocaleTimeString("en-US", {
66
+ hour: "numeric",
67
+ minute: "2-digit",
68
+ });
69
+ }
70
+
71
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72
+ // Component
73
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
+
75
+ export function ChatHistorySidebar({
76
+ memorySpaceId,
77
+ currentConversationId,
78
+ onSelectConversation,
79
+ onNewChat,
80
+ }: ChatHistorySidebarProps) {
81
+ const { user, logout } = useAuth();
82
+ const [conversations, setConversations] = useState<Conversation[]>([]);
83
+ const [isLoading, setIsLoading] = useState(true);
84
+ const [deletingId, setDeletingId] = useState<string | null>(null);
85
+
86
+ // Fetch conversations
87
+ const fetchConversations = useCallback(async () => {
88
+ if (!user) return;
89
+
90
+ try {
91
+ const response = await fetch(
92
+ `/api/conversations?userId=${encodeURIComponent(user.id)}&memorySpaceId=${encodeURIComponent(memorySpaceId)}`
93
+ );
94
+ const data = await response.json();
95
+ if (data.conversations) {
96
+ setConversations(data.conversations);
97
+ }
98
+ } catch (error) {
99
+ console.error("Failed to fetch conversations:", error);
100
+ } finally {
101
+ setIsLoading(false);
102
+ }
103
+ }, [user, memorySpaceId]);
104
+
105
+ // Fetch on mount and when dependencies change
106
+ useEffect(() => {
107
+ fetchConversations();
108
+ }, [fetchConversations]);
109
+
110
+ // Refresh conversations periodically (every 10 seconds)
111
+ useEffect(() => {
112
+ const interval = setInterval(fetchConversations, 10000);
113
+ return () => clearInterval(interval);
114
+ }, [fetchConversations]);
115
+
116
+ // Delete conversation
117
+ const handleDelete = async (e: React.MouseEvent, conversationId: string) => {
118
+ e.stopPropagation();
119
+ if (deletingId) return;
120
+
121
+ setDeletingId(conversationId);
122
+
123
+ try {
124
+ const response = await fetch(`/api/conversations?conversationId=${encodeURIComponent(conversationId)}`, {
125
+ method: "DELETE",
126
+ });
127
+
128
+ if (!response.ok) {
129
+ const data = await response.json().catch(() => ({}));
130
+ throw new Error(data.error || `Delete failed with status ${response.status}`);
131
+ }
132
+
133
+ // Remove from local state only after successful deletion
134
+ setConversations((prev) => prev.filter((c) => c.id !== conversationId));
135
+
136
+ // If deleted conversation was selected, trigger new chat
137
+ if (conversationId === currentConversationId) {
138
+ onNewChat();
139
+ }
140
+ } catch (error) {
141
+ console.error("Failed to delete conversation:", error);
142
+ } finally {
143
+ setDeletingId(null);
144
+ }
145
+ };
146
+
147
+ const groupedConversations = groupByDate(conversations);
148
+ const groups = ["Today", "Yesterday", "Last 7 Days", "Older"];
149
+
150
+ return (
151
+ <div className="w-64 h-full flex flex-col bg-black/40 border-r border-white/10">
152
+ {/* Header */}
153
+ <div className="p-4 border-b border-white/10">
154
+ <div className="flex items-center gap-2 mb-4">
155
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center">
156
+ <span className="text-sm">🧠</span>
157
+ </div>
158
+ <span className="font-semibold text-sm">Cortex Demo</span>
159
+ </div>
160
+
161
+ <button
162
+ onClick={onNewChat}
163
+ className="w-full py-2.5 px-4 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-sm font-medium transition-colors flex items-center gap-2"
164
+ >
165
+ <svg
166
+ className="w-4 h-4"
167
+ fill="none"
168
+ viewBox="0 0 24 24"
169
+ stroke="currentColor"
170
+ >
171
+ <path
172
+ strokeLinecap="round"
173
+ strokeLinejoin="round"
174
+ strokeWidth={2}
175
+ d="M12 4v16m8-8H4"
176
+ />
177
+ </svg>
178
+ New Chat
179
+ </button>
180
+ </div>
181
+
182
+ {/* Conversation List */}
183
+ <div className="flex-1 overflow-y-auto p-2">
184
+ {isLoading ? (
185
+ <div className="flex items-center justify-center py-8">
186
+ <svg
187
+ className="animate-spin h-5 w-5 text-gray-400"
188
+ xmlns="http://www.w3.org/2000/svg"
189
+ fill="none"
190
+ viewBox="0 0 24 24"
191
+ >
192
+ <circle
193
+ className="opacity-25"
194
+ cx="12"
195
+ cy="12"
196
+ r="10"
197
+ stroke="currentColor"
198
+ strokeWidth="4"
199
+ />
200
+ <path
201
+ className="opacity-75"
202
+ fill="currentColor"
203
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
204
+ />
205
+ </svg>
206
+ </div>
207
+ ) : conversations.length === 0 ? (
208
+ <div className="text-center py-8 text-gray-500 text-sm">
209
+ <p>No conversations yet</p>
210
+ <p className="mt-1 text-xs">Start a new chat to begin</p>
211
+ </div>
212
+ ) : (
213
+ <div className="space-y-4">
214
+ {groups.map((group) => {
215
+ const groupConvos = groupedConversations[group];
216
+ if (groupConvos.length === 0) return null;
217
+
218
+ return (
219
+ <div key={group}>
220
+ <div className="px-2 py-1 text-xs font-medium text-gray-500 uppercase tracking-wider">
221
+ {group}
222
+ </div>
223
+ <div className="space-y-1">
224
+ {groupConvos.map((conv) => (
225
+ <div
226
+ key={conv.id}
227
+ onClick={() => onSelectConversation(conv.id)}
228
+ className={`group relative px-3 py-2.5 rounded-xl cursor-pointer transition-colors ${
229
+ conv.id === currentConversationId
230
+ ? "bg-cortex-600/20 text-white"
231
+ : "hover:bg-white/5 text-gray-300"
232
+ }`}
233
+ >
234
+ <div className="pr-6">
235
+ <div className="text-sm font-medium truncate">
236
+ {conv.title}
237
+ </div>
238
+ <div className="text-xs text-gray-500 mt-0.5">
239
+ {formatTime(conv.updatedAt)}
240
+ {conv.messageCount > 0 && (
241
+ <span className="ml-2">
242
+ {conv.messageCount} message
243
+ {conv.messageCount !== 1 ? "s" : ""}
244
+ </span>
245
+ )}
246
+ </div>
247
+ </div>
248
+
249
+ {/* Delete button */}
250
+ <button
251
+ onClick={(e) => handleDelete(e, conv.id)}
252
+ disabled={deletingId === conv.id}
253
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 opacity-0 group-hover:opacity-100 hover:bg-white/10 rounded transition-all"
254
+ title="Delete conversation"
255
+ >
256
+ {deletingId === conv.id ? (
257
+ <svg
258
+ className="w-4 h-4 animate-spin text-gray-400"
259
+ fill="none"
260
+ viewBox="0 0 24 24"
261
+ >
262
+ <circle
263
+ className="opacity-25"
264
+ cx="12"
265
+ cy="12"
266
+ r="10"
267
+ stroke="currentColor"
268
+ strokeWidth="4"
269
+ />
270
+ <path
271
+ className="opacity-75"
272
+ fill="currentColor"
273
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
274
+ />
275
+ </svg>
276
+ ) : (
277
+ <svg
278
+ className="w-4 h-4 text-gray-400 hover:text-red-400"
279
+ fill="none"
280
+ viewBox="0 0 24 24"
281
+ stroke="currentColor"
282
+ >
283
+ <path
284
+ strokeLinecap="round"
285
+ strokeLinejoin="round"
286
+ strokeWidth={2}
287
+ d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
288
+ />
289
+ </svg>
290
+ )}
291
+ </button>
292
+ </div>
293
+ ))}
294
+ </div>
295
+ </div>
296
+ );
297
+ })}
298
+ </div>
299
+ )}
300
+ </div>
301
+
302
+ {/* User Section */}
303
+ <div className="p-4 border-t border-white/10">
304
+ <div className="flex items-center gap-3">
305
+ <div className="w-9 h-9 rounded-full bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center text-sm font-medium">
306
+ {user?.displayName?.charAt(0).toUpperCase() || "U"}
307
+ </div>
308
+ <div className="flex-1 min-w-0">
309
+ <div className="text-sm font-medium truncate">
310
+ {user?.displayName || user?.id}
311
+ </div>
312
+ <button
313
+ onClick={logout}
314
+ className="text-xs text-gray-500 hover:text-white transition-colors"
315
+ >
316
+ Sign out
317
+ </button>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ );
323
+ }
@@ -25,6 +25,9 @@ interface LayerUpdateData {
25
25
  interface ChatInterfaceProps {
26
26
  memorySpaceId: string;
27
27
  userId: string;
28
+ conversationId: string | null;
29
+ /** API endpoint for chat - defaults to /api/chat, use /api/chat-v6 for AI SDK v6 */
30
+ apiEndpoint?: string;
28
31
  onOrchestrationStart?: () => void;
29
32
  onLayerUpdate?: (
30
33
  layer: MemoryLayer,
@@ -36,14 +39,18 @@ interface ChatInterfaceProps {
36
39
  },
37
40
  ) => void;
38
41
  onReset?: () => void;
42
+ onConversationUpdate?: (conversationId: string, title: string) => void;
39
43
  }
40
44
 
41
45
  export function ChatInterface({
42
46
  memorySpaceId,
43
47
  userId,
48
+ conversationId,
49
+ apiEndpoint = "/api/chat",
44
50
  onOrchestrationStart,
45
51
  onLayerUpdate,
46
52
  onReset,
53
+ onConversationUpdate,
47
54
  }: ChatInterfaceProps) {
48
55
  const messagesEndRef = useRef<HTMLDivElement>(null);
49
56
  const [input, setInput] = useState("");
@@ -54,38 +61,56 @@ export function ChatInterface({
54
61
  "What do you remember about me?",
55
62
  ]);
56
63
 
57
- // Create transport with body parameters - memoized to prevent recreation
64
+ // Track conversation ID in a ref for immediate access in callbacks
65
+ // This ensures the latest conversationId is always used when sending messages,
66
+ // even before React re-renders and recreates the transport
67
+ const conversationIdRef = useRef<string | null>(conversationId);
68
+ useEffect(() => {
69
+ conversationIdRef.current = conversationId;
70
+ }, [conversationId]);
71
+
72
+ // Create transport with a function that reads from ref for conversationId
73
+ // This ensures we always send the latest conversationId
58
74
  const transport = useMemo(
59
75
  () =>
60
76
  new DefaultChatTransport({
61
- api: "/api/chat",
62
- body: { memorySpaceId, userId },
77
+ api: apiEndpoint,
78
+ // Use a function to get body so it reads latest conversationId from ref
79
+ body: () => ({
80
+ memorySpaceId,
81
+ userId,
82
+ conversationId: conversationIdRef.current,
83
+ }),
63
84
  }),
64
- [memorySpaceId, userId],
85
+ [apiEndpoint, memorySpaceId, userId], // Note: conversationId removed - ref handles updates
65
86
  );
66
87
 
67
88
  // Handle layer data parts from the stream
68
89
  const handleDataPart = useCallback(
69
- (dataPart: any) => {
70
- if (dataPart.type === "data-orchestration-start") {
90
+ (dataPart: unknown) => {
91
+ const part = dataPart as { type: string; data?: unknown };
92
+ if (part.type === "data-orchestration-start") {
71
93
  onOrchestrationStart?.();
72
94
  }
73
95
 
74
- if (dataPart.type === "data-layer-update") {
75
- const event = dataPart.data as LayerUpdateData;
96
+ if (part.type === "data-layer-update") {
97
+ const event = part.data as LayerUpdateData;
76
98
  onLayerUpdate?.(event.layer, event.status, event.data, {
77
99
  action: event.revisionAction,
78
100
  supersededFacts: event.supersededFacts,
79
101
  });
80
102
  }
81
103
 
82
- // orchestration-complete is informational - layer diagram already updated
83
- // via individual layer events
104
+ // Handle conversation title update
105
+ if (part.type === "data-conversation-update") {
106
+ const update = part.data as { conversationId: string; title: string };
107
+ onConversationUpdate?.(update.conversationId, update.title);
108
+ }
84
109
  },
85
- [onOrchestrationStart, onLayerUpdate],
110
+ [onOrchestrationStart, onLayerUpdate, onConversationUpdate],
86
111
  );
87
112
 
88
- const { messages, sendMessage, status } = useChat({
113
+ const { messages, sendMessage, status, setMessages } = useChat({
89
114
  transport,
90
115
  onData: handleDataPart,
91
116
  onError: (error) => {
@@ -93,6 +118,53 @@ export function ChatInterface({
93
118
  },
94
119
  });
95
120
 
121
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
122
+
123
+ // Load messages when conversation changes
124
+ useEffect(() => {
125
+ // Clear messages first
126
+ setMessages([]);
127
+
128
+ // If no conversation selected, nothing more to do
129
+ if (!conversationId) {
130
+ return;
131
+ }
132
+
133
+ // Fetch conversation history
134
+ const loadConversationHistory = async () => {
135
+ setIsLoadingHistory(true);
136
+ try {
137
+ const response = await fetch(
138
+ `/api/conversations?conversationId=${encodeURIComponent(conversationId)}`
139
+ );
140
+
141
+ if (!response.ok) {
142
+ console.error("Failed to load conversation history");
143
+ return;
144
+ }
145
+
146
+ const data = await response.json();
147
+
148
+ if (data.messages && data.messages.length > 0) {
149
+ // Transform to the format expected by useChat
150
+ const loadedMessages = data.messages.map((msg: { id: string; role: string; content: string; createdAt: string }) => ({
151
+ id: msg.id,
152
+ role: msg.role,
153
+ content: msg.content,
154
+ createdAt: new Date(msg.createdAt),
155
+ }));
156
+ setMessages(loadedMessages);
157
+ }
158
+ } catch (error) {
159
+ console.error("Error loading conversation history:", error);
160
+ } finally {
161
+ setIsLoadingHistory(false);
162
+ }
163
+ };
164
+
165
+ loadConversationHistory();
166
+ }, [conversationId, setMessages]);
167
+
96
168
  // Determine if we're actively streaming (only time to show typing indicator)
97
169
  const isStreaming = status === "streaming";
98
170
 
@@ -121,14 +193,14 @@ export function ChatInterface({
121
193
  };
122
194
 
123
195
  // Extract text content from message parts (AI SDK v5 format)
124
- const getMessageContent = (message: any): string => {
196
+ const getMessageContent = (message: { content?: string; parts?: Array<{ type: string; text?: string }> }): string => {
125
197
  if (typeof message.content === "string") {
126
198
  return message.content;
127
199
  }
128
200
  if (message.parts) {
129
201
  return message.parts
130
- .filter((part: any) => part.type === "text")
131
- .map((part: any) => part.text)
202
+ .filter((part) => part.type === "text")
203
+ .map((part) => part.text)
132
204
  .join("");
133
205
  }
134
206
  return "";
@@ -138,13 +210,41 @@ export function ChatInterface({
138
210
  <div className="flex flex-col h-full">
139
211
  {/* Messages */}
140
212
  <div className="flex-1 overflow-y-auto p-4 space-y-4">
141
- {messages.length === 0 && (
213
+ {isLoadingHistory && (
214
+ <div className="flex items-center justify-center py-12">
215
+ <div className="flex flex-col items-center gap-3">
216
+ <svg
217
+ className="animate-spin h-8 w-8 text-cortex-500"
218
+ xmlns="http://www.w3.org/2000/svg"
219
+ fill="none"
220
+ viewBox="0 0 24 24"
221
+ >
222
+ <circle
223
+ className="opacity-25"
224
+ cx="12"
225
+ cy="12"
226
+ r="10"
227
+ stroke="currentColor"
228
+ strokeWidth="4"
229
+ />
230
+ <path
231
+ className="opacity-75"
232
+ fill="currentColor"
233
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
234
+ />
235
+ </svg>
236
+ <span className="text-gray-400 text-sm">Loading conversation...</span>
237
+ </div>
238
+ </div>
239
+ )}
240
+
241
+ {!isLoadingHistory && messages.length === 0 && (
142
242
  <div className="text-center py-12">
143
243
  <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
244
  <span className="text-3xl">🧠</span>
145
245
  </div>
146
246
  <h2 className="text-xl font-semibold mb-2">
147
- Welcome to Cortex Memory Demo
247
+ {conversationId ? "Continue your conversation" : "Start a new conversation"}
148
248
  </h2>
149
249
  <p className="text-gray-400 max-w-md mx-auto mb-6">
150
250
  This demo shows how Cortex orchestrates memory across multiple