@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexmemory/cli",
3
- "version": "0.27.1",
3
+ "version": "0.27.4",
4
4
  "description": "CLI tool for managing Cortex Memory deployments, performing administrative tasks, and streamlining development workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,128 @@
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
+ /**
11
+ * Validates login request body structure.
12
+ * Returns validated credentials or null if invalid.
13
+ */
14
+ function validateLoginBody(
15
+ body: unknown
16
+ ): { username: string; password: string } | null {
17
+ if (typeof body !== "object" || body === null) {
18
+ return null;
19
+ }
20
+
21
+ const record = body as Record<string, unknown>;
22
+
23
+ // Validate username field exists and is a non-empty string
24
+ const hasValidUsername =
25
+ "username" in record &&
26
+ typeof record.username === "string" &&
27
+ record.username.length > 0 &&
28
+ record.username.length <= 256;
29
+
30
+ // Validate password field exists and is a non-empty string
31
+ const hasValidPassword =
32
+ "password" in record &&
33
+ typeof record.password === "string" &&
34
+ record.password.length > 0 &&
35
+ record.password.length <= 1024;
36
+
37
+ if (!hasValidUsername || !hasValidPassword) {
38
+ return null;
39
+ }
40
+
41
+ return {
42
+ username: record.username as string,
43
+ password: record.password as string,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Safely extracts an error message for logging without exposing user data.
49
+ */
50
+ function getSafeErrorMessage(error: unknown): string {
51
+ if (error instanceof Error) {
52
+ // Only include error name and a sanitized message
53
+ // Avoid logging full stack traces which may contain user data
54
+ return `${error.name}: ${error.message.slice(0, 200)}`;
55
+ }
56
+ return "Unknown error";
57
+ }
58
+
59
+ export async function POST(req: Request) {
60
+ try {
61
+ const body = await req.json();
62
+
63
+ // Validate input structure before extracting values
64
+ const credentials = validateLoginBody(body);
65
+ if (!credentials) {
66
+ return Response.json(
67
+ { error: "Username and password are required" },
68
+ { status: 400 }
69
+ );
70
+ }
71
+
72
+ const { username, password } = credentials;
73
+
74
+ const cortex = getCortex();
75
+ const sanitizedUsername = username.toLowerCase();
76
+
77
+ // Get user profile
78
+ const user = await cortex.users.get(sanitizedUsername);
79
+ if (!user) {
80
+ return Response.json(
81
+ { error: "Invalid username or password" },
82
+ { status: 401 }
83
+ );
84
+ }
85
+
86
+ // Verify password
87
+ const storedHash = user.data.passwordHash as string;
88
+ if (!storedHash) {
89
+ return Response.json(
90
+ { error: "Invalid username or password" },
91
+ { status: 401 }
92
+ );
93
+ }
94
+
95
+ const isValid = await verifyPassword(password, storedHash);
96
+ if (!isValid) {
97
+ return Response.json(
98
+ { error: "Invalid username or password" },
99
+ { status: 401 }
100
+ );
101
+ }
102
+
103
+ // Update last login time
104
+ await cortex.users.update(sanitizedUsername, {
105
+ lastLoginAt: Date.now(),
106
+ });
107
+
108
+ // Generate session token
109
+ const sessionToken = generateSessionToken();
110
+
111
+ return Response.json({
112
+ success: true,
113
+ user: {
114
+ id: sanitizedUsername,
115
+ displayName: (user.data.displayName as string) || sanitizedUsername,
116
+ },
117
+ sessionToken,
118
+ });
119
+ } catch (error) {
120
+ // Log sanitized error to prevent log injection
121
+ console.error("[Login Error]", getSafeErrorMessage(error));
122
+
123
+ return Response.json(
124
+ { error: "Failed to authenticate" },
125
+ { status: 500 }
126
+ );
127
+ }
128
+ }
@@ -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,19 +106,117 @@ 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
+
125
+ /**
126
+ * Normalize messages to ensure they have the `parts` array format
127
+ * expected by AI SDK v6's convertToModelMessages.
128
+ *
129
+ * Handles:
130
+ * - Messages with `content` string (legacy format) -> converts to `parts` array
131
+ * - Messages with `role: "agent"` -> converts to `role: "assistant"`
132
+ * - Messages already in v6 format -> passes through unchanged
133
+ */
134
+ function normalizeMessages(messages: unknown[]): unknown[] {
135
+ return messages.map((msg: unknown) => {
136
+ const m = msg as Record<string, unknown>;
137
+
138
+ // Normalize role: "agent" -> "assistant"
139
+ let role = m.role as string;
140
+ if (role === "agent") {
141
+ role = "assistant";
142
+ }
143
+
144
+ // Ensure parts array exists
145
+ let parts = m.parts as Array<{ type: string; text?: string }> | undefined;
146
+ if (!parts) {
147
+ // Convert content string to parts array
148
+ const content = m.content as string | undefined;
149
+ if (content) {
150
+ parts = [{ type: "text", text: content }];
151
+ } else {
152
+ parts = [];
153
+ }
154
+ }
155
+
156
+ return {
157
+ ...m,
158
+ role,
159
+ parts,
160
+ };
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Extract text from a message (handles both content string and parts array)
166
+ */
167
+ function getMessageText(message: { content?: string; parts?: Array<{ type: string; text?: string }> }): string {
168
+ if (typeof message.content === "string") {
169
+ return message.content;
170
+ }
171
+ if (message.parts && Array.isArray(message.parts)) {
172
+ return message.parts
173
+ .filter((part) => part.type === "text" && part.text)
174
+ .map((part) => part.text)
175
+ .join("");
176
+ }
177
+ return "";
178
+ }
179
+
104
180
  export async function POST(req: Request) {
105
181
  try {
106
182
  const body = await req.json();
107
- const { messages, memorySpaceId, userId } = body;
183
+ const { messages, memorySpaceId, userId, conversationId: providedConversationId } = body;
184
+
185
+ // Validate messages array exists
186
+ if (!messages || !Array.isArray(messages)) {
187
+ return new Response(
188
+ JSON.stringify({ error: "messages array is required" }),
189
+ { status: 400, headers: { "Content-Type": "application/json" } },
190
+ );
191
+ }
192
+
193
+ // Generate conversation ID if not provided (new chat)
194
+ const conversationId = providedConversationId ||
195
+ `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
196
+ const isNewConversation = !providedConversationId;
197
+
198
+ // Normalize messages to ensure they have the `parts` array format
199
+ // expected by AI SDK v6's convertToModelMessages
200
+ const normalizedMessages = normalizeMessages(messages);
108
201
 
109
202
  // Convert UIMessage[] from useChat to ModelMessage[] for streamText
110
203
  // Note: In AI SDK v6+, convertToModelMessages may return a Promise
111
- const modelMessagesResult = convertToModelMessages(messages);
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
+ const modelMessagesResult = convertToModelMessages(normalizedMessages as any);
112
206
  const modelMessages =
113
207
  modelMessagesResult instanceof Promise
114
208
  ? await modelMessagesResult
115
209
  : modelMessagesResult;
116
210
 
211
+ // Get the first user message for title generation
212
+ const firstUserMessage = messages.find((m: { role: string }) => m.role === "user") as {
213
+ role: string;
214
+ content?: string;
215
+ parts?: Array<{ type: string; text?: string }>;
216
+ } | undefined;
217
+
218
+ const messageText = firstUserMessage ? getMessageText(firstUserMessage) : "";
219
+
117
220
  // Use createUIMessageStream to send both LLM text and layer events
118
221
  return createUIMessageStreamResponse({
119
222
  stream: createUIMessageStream({
@@ -157,10 +260,11 @@ export async function POST(req: Request) {
157
260
  },
158
261
  };
159
262
 
160
- // Build config with the observer
263
+ // Build config with the observer and conversation ID
161
264
  const config = getCortexMemoryConfig(
162
265
  memorySpaceId || "quickstart-demo",
163
266
  userId || "demo-user",
267
+ conversationId,
164
268
  layerObserver,
165
269
  );
166
270
 
@@ -177,6 +281,38 @@ export async function POST(req: Request) {
177
281
 
178
282
  // Merge LLM stream into the UI message stream
179
283
  writer.merge(result.toUIMessageStream());
284
+
285
+ // If this is a new conversation, create it in the SDK and update the title
286
+ if (isNewConversation && messageText) {
287
+ try {
288
+ const cortex = getCortex();
289
+ const title = generateTitle(messageText);
290
+
291
+ // Create the conversation with the SDK
292
+ await cortex.conversations.create({
293
+ memorySpaceId: memorySpaceId || "quickstart-demo",
294
+ conversationId,
295
+ type: "user-agent",
296
+ participants: {
297
+ userId: userId || "demo-user",
298
+ agentId: "quickstart-assistant",
299
+ },
300
+ metadata: { title },
301
+ });
302
+
303
+ // Send conversation update to the client
304
+ writer.write({
305
+ type: "data-conversation-update",
306
+ data: {
307
+ conversationId,
308
+ title,
309
+ },
310
+ transient: true,
311
+ });
312
+ } catch (error) {
313
+ console.error("Failed to create conversation:", error);
314
+ }
315
+ }
180
316
  },
181
317
  }),
182
318
  });