@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
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Auth Check API Route
3
+ *
4
+ * GET: Check if admin has been set up (first-run detection)
5
+ */
6
+
7
+ import { getCortex } from "@/lib/cortex";
8
+
9
+ const ADMIN_NAMESPACE = "quickstart-config";
10
+ const ADMIN_KEY = "admin_password_hash";
11
+
12
+ export async function GET() {
13
+ try {
14
+ const cortex = getCortex();
15
+
16
+ // Check if admin password hash exists in mutable store
17
+ const adminHash = await cortex.mutable.get(ADMIN_NAMESPACE, ADMIN_KEY);
18
+
19
+ return Response.json({
20
+ isSetup: adminHash !== null,
21
+ });
22
+ } catch (error) {
23
+ console.error("[Auth Check Error]", error);
24
+
25
+ return Response.json(
26
+ { error: "Failed to check admin setup status" },
27
+ { status: 500 }
28
+ );
29
+ }
30
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * User Login API Route
3
+ *
4
+ * POST: Authenticate user and return session
5
+ */
6
+
7
+ import { getCortex } from "@/lib/cortex";
8
+ import { verifyPassword, generateSessionToken } from "@/lib/password";
9
+
10
+ export async function POST(req: Request) {
11
+ try {
12
+ const body = await req.json();
13
+ const { username, password } = body;
14
+
15
+ // Validate input
16
+ if (!username || typeof username !== "string") {
17
+ return Response.json(
18
+ { error: "Username is required" },
19
+ { status: 400 }
20
+ );
21
+ }
22
+
23
+ if (!password || typeof password !== "string") {
24
+ return Response.json(
25
+ { error: "Password is required" },
26
+ { status: 400 }
27
+ );
28
+ }
29
+
30
+ const cortex = getCortex();
31
+ const sanitizedUsername = username.toLowerCase();
32
+
33
+ // Get user profile
34
+ const user = await cortex.users.get(sanitizedUsername);
35
+ if (!user) {
36
+ return Response.json(
37
+ { error: "Invalid username or password" },
38
+ { status: 401 }
39
+ );
40
+ }
41
+
42
+ // Verify password
43
+ const storedHash = user.data.passwordHash as string;
44
+ if (!storedHash) {
45
+ return Response.json(
46
+ { error: "Invalid username or password" },
47
+ { status: 401 }
48
+ );
49
+ }
50
+
51
+ const isValid = await verifyPassword(password, storedHash);
52
+ if (!isValid) {
53
+ return Response.json(
54
+ { error: "Invalid username or password" },
55
+ { status: 401 }
56
+ );
57
+ }
58
+
59
+ // Update last login time
60
+ await cortex.users.update(sanitizedUsername, {
61
+ lastLoginAt: Date.now(),
62
+ });
63
+
64
+ // Generate session token
65
+ const sessionToken = generateSessionToken();
66
+
67
+ return Response.json({
68
+ success: true,
69
+ user: {
70
+ id: sanitizedUsername,
71
+ displayName: (user.data.displayName as string) || sanitizedUsername,
72
+ },
73
+ sessionToken,
74
+ });
75
+ } catch (error) {
76
+ console.error("[Login Error]", error);
77
+
78
+ return Response.json(
79
+ { error: "Failed to authenticate" },
80
+ { status: 500 }
81
+ );
82
+ }
83
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * User Registration API Route
3
+ *
4
+ * POST: Register a new user account
5
+ */
6
+
7
+ import { getCortex } from "@/lib/cortex";
8
+ import { hashPassword, generateSessionToken } from "@/lib/password";
9
+
10
+ export async function POST(req: Request) {
11
+ try {
12
+ const body = await req.json();
13
+ const { username, password, displayName } = body;
14
+
15
+ // Validate input
16
+ if (!username || typeof username !== "string") {
17
+ return Response.json(
18
+ { error: "Username is required" },
19
+ { status: 400 }
20
+ );
21
+ }
22
+
23
+ if (!password || typeof password !== "string") {
24
+ return Response.json(
25
+ { error: "Password is required" },
26
+ { status: 400 }
27
+ );
28
+ }
29
+
30
+ if (username.length < 2) {
31
+ return Response.json(
32
+ { error: "Username must be at least 2 characters" },
33
+ { status: 400 }
34
+ );
35
+ }
36
+
37
+ if (password.length < 4) {
38
+ return Response.json(
39
+ { error: "Password must be at least 4 characters" },
40
+ { status: 400 }
41
+ );
42
+ }
43
+
44
+ // Sanitize username (alphanumeric, underscore, hyphen only)
45
+ const sanitizedUsername = username.toLowerCase().replace(/[^a-z0-9_-]/g, "");
46
+ if (sanitizedUsername !== username.toLowerCase()) {
47
+ return Response.json(
48
+ { error: "Username can only contain letters, numbers, underscores, and hyphens" },
49
+ { status: 400 }
50
+ );
51
+ }
52
+
53
+ const cortex = getCortex();
54
+
55
+ // Check if user already exists
56
+ const existingUser = await cortex.users.get(sanitizedUsername);
57
+ if (existingUser) {
58
+ return Response.json(
59
+ { error: "Username already taken" },
60
+ { status: 409 }
61
+ );
62
+ }
63
+
64
+ // Hash password and create user profile
65
+ const passwordHash = await hashPassword(password);
66
+ const now = Date.now();
67
+
68
+ await cortex.users.update(sanitizedUsername, {
69
+ displayName: displayName || sanitizedUsername,
70
+ passwordHash,
71
+ createdAt: now,
72
+ lastLoginAt: now,
73
+ });
74
+
75
+ // Generate session token
76
+ const sessionToken = generateSessionToken();
77
+
78
+ return Response.json({
79
+ success: true,
80
+ user: {
81
+ id: sanitizedUsername,
82
+ displayName: displayName || sanitizedUsername,
83
+ },
84
+ sessionToken,
85
+ });
86
+ } catch (error) {
87
+ console.error("[Register Error]", error);
88
+
89
+ return Response.json(
90
+ { error: "Failed to register user" },
91
+ { status: 500 }
92
+ );
93
+ }
94
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Admin Setup API Route
3
+ *
4
+ * POST: Set up initial admin password (first-run only)
5
+ */
6
+
7
+ import { getCortex } from "@/lib/cortex";
8
+ import { hashPassword } from "@/lib/password";
9
+
10
+ const ADMIN_NAMESPACE = "quickstart-config";
11
+ const ADMIN_KEY = "admin_password_hash";
12
+
13
+ export async function POST(req: Request) {
14
+ try {
15
+ const body = await req.json();
16
+ const { password } = body;
17
+
18
+ if (!password || typeof password !== "string") {
19
+ return Response.json(
20
+ { error: "Password is required" },
21
+ { status: 400 }
22
+ );
23
+ }
24
+
25
+ if (password.length < 4) {
26
+ return Response.json(
27
+ { error: "Password must be at least 4 characters" },
28
+ { status: 400 }
29
+ );
30
+ }
31
+
32
+ const cortex = getCortex();
33
+
34
+ // Check if admin already exists
35
+ const existingHash = await cortex.mutable.get(ADMIN_NAMESPACE, ADMIN_KEY);
36
+ if (existingHash !== null) {
37
+ return Response.json(
38
+ { error: "Admin already configured" },
39
+ { status: 409 }
40
+ );
41
+ }
42
+
43
+ // Hash and store admin password
44
+ const passwordHash = await hashPassword(password);
45
+ await cortex.mutable.set(ADMIN_NAMESPACE, ADMIN_KEY, passwordHash);
46
+
47
+ return Response.json({
48
+ success: true,
49
+ message: "Admin password configured successfully",
50
+ });
51
+ } catch (error) {
52
+ console.error("[Admin Setup Error]", error);
53
+
54
+ return Response.json(
55
+ { error: "Failed to configure admin password" },
56
+ { status: 500 }
57
+ );
58
+ }
59
+ }
@@ -11,6 +11,7 @@ import {
11
11
  createUIMessageStream,
12
12
  createUIMessageStreamResponse,
13
13
  } from "ai";
14
+ import { getCortex } from "@/lib/cortex";
14
15
 
15
16
  // Create OpenAI client for embeddings
16
17
  const openaiClient = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
@@ -40,6 +41,7 @@ Example interactions:
40
41
  function getCortexMemoryConfig(
41
42
  memorySpaceId: string,
42
43
  userId: string,
44
+ conversationId: string,
43
45
  layerObserver?: LayerObserver,
44
46
  ): CortexMemoryConfig {
45
47
  return {
@@ -54,6 +56,9 @@ function getCortexMemoryConfig(
54
56
  agentId: "quickstart-assistant",
55
57
  agentName: "Cortex Demo Assistant",
56
58
 
59
+ // Conversation ID for chat history isolation
60
+ conversationId,
61
+
57
62
  // Enable graph memory sync (auto-configured via env vars)
58
63
  // When true, uses CypherGraphAdapter to sync to Neo4j/Memgraph
59
64
  enableGraphMemory: process.env.CORTEX_GRAPH_SYNC === "true",
@@ -101,10 +106,31 @@ function getCortexMemoryConfig(
101
106
  };
102
107
  }
103
108
 
109
+ /**
110
+ * Generate a title from the first user message
111
+ */
112
+ function generateTitle(message: string): string {
113
+ // Take first 50 chars, cut at word boundary
114
+ let title = message.slice(0, 50);
115
+ if (message.length > 50) {
116
+ const lastSpace = title.lastIndexOf(" ");
117
+ if (lastSpace > 20) {
118
+ title = title.slice(0, lastSpace);
119
+ }
120
+ title += "...";
121
+ }
122
+ return title;
123
+ }
124
+
104
125
  export async function POST(req: Request) {
105
126
  try {
106
127
  const body = await req.json();
107
- const { messages, memorySpaceId, userId } = body;
128
+ const { messages, memorySpaceId, userId, conversationId: providedConversationId } = body;
129
+
130
+ // Generate conversation ID if not provided (new chat)
131
+ const conversationId = providedConversationId ||
132
+ `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
133
+ const isNewConversation = !providedConversationId;
108
134
 
109
135
  // Convert UIMessage[] from useChat to ModelMessage[] for streamText
110
136
  // Note: In AI SDK v6+, convertToModelMessages may return a Promise
@@ -114,6 +140,28 @@ export async function POST(req: Request) {
114
140
  ? await modelMessagesResult
115
141
  : modelMessagesResult;
116
142
 
143
+ // Get the first user message for title generation
144
+ // AI SDK v5+ uses `parts` array instead of `content` string
145
+ const firstUserMessage = messages.find((m: { role: string }) => m.role === "user") as {
146
+ role: string;
147
+ content?: string;
148
+ parts?: Array<{ type: string; text?: string }>;
149
+ } | undefined;
150
+
151
+ let messageText = "";
152
+ if (firstUserMessage) {
153
+ if (typeof firstUserMessage.content === "string") {
154
+ // Legacy format: content is a string
155
+ messageText = firstUserMessage.content;
156
+ } else if (firstUserMessage.parts && Array.isArray(firstUserMessage.parts)) {
157
+ // AI SDK v5+ format: extract text from parts array
158
+ messageText = firstUserMessage.parts
159
+ .filter((part) => part.type === "text" && part.text)
160
+ .map((part) => part.text)
161
+ .join("");
162
+ }
163
+ }
164
+
117
165
  // Use createUIMessageStream to send both LLM text and layer events
118
166
  return createUIMessageStreamResponse({
119
167
  stream: createUIMessageStream({
@@ -157,10 +205,11 @@ export async function POST(req: Request) {
157
205
  },
158
206
  };
159
207
 
160
- // Build config with the observer
208
+ // Build config with the observer and conversation ID
161
209
  const config = getCortexMemoryConfig(
162
210
  memorySpaceId || "quickstart-demo",
163
211
  userId || "demo-user",
212
+ conversationId,
164
213
  layerObserver,
165
214
  );
166
215
 
@@ -177,6 +226,38 @@ export async function POST(req: Request) {
177
226
 
178
227
  // Merge LLM stream into the UI message stream
179
228
  writer.merge(result.toUIMessageStream());
229
+
230
+ // If this is a new conversation, create it in the SDK and update the title
231
+ if (isNewConversation && messageText) {
232
+ try {
233
+ const cortex = getCortex();
234
+ const title = generateTitle(messageText);
235
+
236
+ // Create the conversation with the SDK
237
+ await cortex.conversations.create({
238
+ memorySpaceId: memorySpaceId || "quickstart-demo",
239
+ conversationId,
240
+ type: "user-agent",
241
+ participants: {
242
+ userId: userId || "demo-user",
243
+ agentId: "quickstart-assistant",
244
+ },
245
+ metadata: { title },
246
+ });
247
+
248
+ // Send conversation update to the client
249
+ writer.write({
250
+ type: "data-conversation-update",
251
+ data: {
252
+ conversationId,
253
+ title,
254
+ },
255
+ transient: true,
256
+ });
257
+ } catch (error) {
258
+ console.error("Failed to create conversation:", error);
259
+ }
260
+ }
180
261
  },
181
262
  }),
182
263
  });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Conversations API Route
3
+ *
4
+ * GET: List conversations for a user (chat history)
5
+ * POST: Create a new conversation
6
+ * DELETE: Delete a conversation
7
+ */
8
+
9
+ import { getCortex } from "@/lib/cortex";
10
+
11
+ export async function GET(req: Request) {
12
+ try {
13
+ const { searchParams } = new URL(req.url);
14
+ const conversationId = searchParams.get("conversationId");
15
+ const userId = searchParams.get("userId");
16
+ const memorySpaceId = searchParams.get("memorySpaceId") || "quickstart-demo";
17
+
18
+ const cortex = getCortex();
19
+
20
+ // If conversationId is provided, fetch single conversation with messages
21
+ if (conversationId) {
22
+ const conversation = await cortex.conversations.get(conversationId, {
23
+ includeMessages: true,
24
+ messageLimit: 100,
25
+ });
26
+
27
+ if (!conversation) {
28
+ return Response.json(
29
+ { error: "Conversation not found" },
30
+ { status: 404 }
31
+ );
32
+ }
33
+
34
+ // Transform messages to the format expected by AI SDK useChat
35
+ const messages = (conversation.messages || []).map((msg) => ({
36
+ id: msg.id,
37
+ role: msg.role as "user" | "assistant",
38
+ content: msg.content,
39
+ createdAt: new Date(msg.timestamp),
40
+ }));
41
+
42
+ return Response.json({
43
+ conversation: {
44
+ id: conversation.conversationId,
45
+ title: (conversation.metadata?.title as string) || getDefaultTitle(conversation),
46
+ createdAt: conversation.createdAt,
47
+ updatedAt: conversation.updatedAt,
48
+ messageCount: conversation.messageCount || 0,
49
+ },
50
+ messages,
51
+ });
52
+ }
53
+
54
+ // List conversations for user (requires userId)
55
+ if (!userId) {
56
+ return Response.json(
57
+ { error: "userId is required" },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ // Get conversations for the user
63
+ const result = await cortex.conversations.list({
64
+ memorySpaceId,
65
+ userId,
66
+ limit: 50,
67
+ });
68
+
69
+ // Map conversations to a simpler format for the UI
70
+ const conversations = result.conversations.map((conv) => ({
71
+ id: conv.conversationId,
72
+ title: (conv.metadata?.title as string) || getDefaultTitle(conv),
73
+ createdAt: conv.createdAt,
74
+ updatedAt: conv.updatedAt,
75
+ messageCount: conv.messageCount || 0,
76
+ }));
77
+
78
+ // Sort by updatedAt descending (most recent first)
79
+ conversations.sort((a, b) => b.updatedAt - a.updatedAt);
80
+
81
+ return Response.json({ conversations });
82
+ } catch (error) {
83
+ console.error("[Conversations Error]", error);
84
+
85
+ return Response.json(
86
+ { error: "Failed to fetch conversations" },
87
+ { status: 500 }
88
+ );
89
+ }
90
+ }
91
+
92
+ export async function POST(req: Request) {
93
+ try {
94
+ const body = await req.json();
95
+ const { userId, memorySpaceId = "quickstart-demo", title } = body;
96
+
97
+ if (!userId) {
98
+ return Response.json(
99
+ { error: "userId is required" },
100
+ { status: 400 }
101
+ );
102
+ }
103
+
104
+ const cortex = getCortex();
105
+
106
+ // Create a new conversation
107
+ const conversationId = `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
108
+
109
+ const conversation = await cortex.conversations.create({
110
+ memorySpaceId,
111
+ conversationId,
112
+ type: "user-agent",
113
+ participants: {
114
+ userId,
115
+ agentId: "quickstart-assistant",
116
+ },
117
+ metadata: {
118
+ title: title || "New Chat",
119
+ },
120
+ });
121
+
122
+ return Response.json({
123
+ success: true,
124
+ conversation: {
125
+ id: conversation.conversationId,
126
+ title: (conversation.metadata?.title as string) || "New Chat",
127
+ createdAt: conversation.createdAt,
128
+ updatedAt: conversation.updatedAt,
129
+ messageCount: 0,
130
+ },
131
+ });
132
+ } catch (error) {
133
+ console.error("[Conversation Create Error]", error);
134
+
135
+ return Response.json(
136
+ { error: "Failed to create conversation" },
137
+ { status: 500 }
138
+ );
139
+ }
140
+ }
141
+
142
+ export async function DELETE(req: Request) {
143
+ try {
144
+ const { searchParams } = new URL(req.url);
145
+ const conversationId = searchParams.get("conversationId");
146
+
147
+ if (!conversationId) {
148
+ return Response.json(
149
+ { error: "conversationId is required" },
150
+ { status: 400 }
151
+ );
152
+ }
153
+
154
+ const cortex = getCortex();
155
+
156
+ await cortex.conversations.delete(conversationId);
157
+
158
+ return Response.json({ success: true });
159
+ } catch (error) {
160
+ console.error("[Conversation Delete Error]", error);
161
+
162
+ return Response.json(
163
+ { error: "Failed to delete conversation" },
164
+ { status: 500 }
165
+ );
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Generate a default title from conversation data
171
+ */
172
+ function getDefaultTitle(conv: { createdAt: number; messageCount?: number }): string {
173
+ const date = new Date(conv.createdAt);
174
+ const timeStr = date.toLocaleTimeString("en-US", {
175
+ hour: "numeric",
176
+ minute: "2-digit",
177
+ });
178
+ return `Chat at ${timeStr}`;
179
+ }