@cortexmemory/cli 0.26.2 → 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 (90) 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 +210 -36
  9. package/dist/commands/dev.js.map +1 -1
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +273 -43
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/commands/setup.d.ts.map +1 -1
  14. package/dist/commands/setup.js +102 -46
  15. package/dist/commands/setup.js.map +1 -1
  16. package/dist/commands/status.d.ts.map +1 -1
  17. package/dist/commands/status.js +94 -7
  18. package/dist/commands/status.js.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/types.d.ts +23 -0
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/utils/app-template-sync.d.ts +95 -0
  23. package/dist/utils/app-template-sync.d.ts.map +1 -0
  24. package/dist/utils/app-template-sync.js +425 -0
  25. package/dist/utils/app-template-sync.js.map +1 -0
  26. package/dist/utils/config.d.ts +11 -0
  27. package/dist/utils/config.d.ts.map +1 -1
  28. package/dist/utils/config.js +20 -0
  29. package/dist/utils/config.js.map +1 -1
  30. package/dist/utils/deployment-selector.d.ts +21 -0
  31. package/dist/utils/deployment-selector.d.ts.map +1 -1
  32. package/dist/utils/deployment-selector.js +32 -0
  33. package/dist/utils/deployment-selector.js.map +1 -1
  34. package/dist/utils/init/graph-setup.d.ts.map +1 -1
  35. package/dist/utils/init/graph-setup.js +25 -2
  36. package/dist/utils/init/graph-setup.js.map +1 -1
  37. package/dist/utils/init/quickstart-setup.d.ts +87 -0
  38. package/dist/utils/init/quickstart-setup.d.ts.map +1 -0
  39. package/dist/utils/init/quickstart-setup.js +462 -0
  40. package/dist/utils/init/quickstart-setup.js.map +1 -0
  41. package/dist/utils/schema-sync.d.ts.map +1 -1
  42. package/dist/utils/schema-sync.js +27 -21
  43. package/dist/utils/schema-sync.js.map +1 -1
  44. package/package.json +3 -2
  45. package/templates/vercel-ai-quickstart/.env.local.example +45 -0
  46. package/templates/vercel-ai-quickstart/README.md +280 -0
  47. package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
  48. package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +83 -0
  49. package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
  50. package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
  51. package/templates/vercel-ai-quickstart/app/api/chat/route.ts +277 -0
  52. package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
  53. package/templates/vercel-ai-quickstart/app/api/facts/route.ts +39 -0
  54. package/templates/vercel-ai-quickstart/app/api/health/route.ts +99 -0
  55. package/templates/vercel-ai-quickstart/app/api/memories/route.ts +37 -0
  56. package/templates/vercel-ai-quickstart/app/globals.css +275 -0
  57. package/templates/vercel-ai-quickstart/app/layout.tsx +19 -0
  58. package/templates/vercel-ai-quickstart/app/page.tsx +216 -0
  59. package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
  60. package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
  61. package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
  62. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +334 -0
  63. package/templates/vercel-ai-quickstart/components/ConvexClientProvider.tsx +21 -0
  64. package/templates/vercel-ai-quickstart/components/DataPreview.tsx +57 -0
  65. package/templates/vercel-ai-quickstart/components/HealthStatus.tsx +214 -0
  66. package/templates/vercel-ai-quickstart/components/LayerCard.tsx +263 -0
  67. package/templates/vercel-ai-quickstart/components/LayerFlowDiagram.tsx +195 -0
  68. package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
  69. package/templates/vercel-ai-quickstart/components/MemorySpaceSwitcher.tsx +93 -0
  70. package/templates/vercel-ai-quickstart/convex/conversations.ts +67 -0
  71. package/templates/vercel-ai-quickstart/convex/facts.ts +131 -0
  72. package/templates/vercel-ai-quickstart/convex/health.ts +15 -0
  73. package/templates/vercel-ai-quickstart/convex/memories.ts +104 -0
  74. package/templates/vercel-ai-quickstart/convex/schema.ts +20 -0
  75. package/templates/vercel-ai-quickstart/convex/users.ts +105 -0
  76. package/templates/vercel-ai-quickstart/jest.config.js +45 -0
  77. package/templates/vercel-ai-quickstart/lib/animations.ts +146 -0
  78. package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
  79. package/templates/vercel-ai-quickstart/lib/layer-tracking.ts +214 -0
  80. package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
  81. package/templates/vercel-ai-quickstart/next.config.js +27 -0
  82. package/templates/vercel-ai-quickstart/package.json +46 -0
  83. package/templates/vercel-ai-quickstart/postcss.config.js +5 -0
  84. package/templates/vercel-ai-quickstart/tailwind.config.js +37 -0
  85. package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
  86. package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
  87. package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
  88. package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
  89. package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
  90. package/templates/vercel-ai-quickstart/tsconfig.json +33 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Integration Tests: Auth API Routes
3
+ *
4
+ * Tests the authentication API routes with mocked Cortex SDK.
5
+ */
6
+
7
+ import {
8
+ createMockCortex,
9
+ resetMockStores,
10
+ seedTestData,
11
+ type MockCortex,
12
+ } from "../helpers/mock-cortex";
13
+ import { hashPassword } from "../../lib/password";
14
+
15
+ // Mock the Cortex SDK module
16
+ let mockCortex: MockCortex;
17
+
18
+ jest.mock("../../lib/cortex", () => ({
19
+ getCortex: () => mockCortex,
20
+ }));
21
+
22
+ // Import route handlers after mocking
23
+ import { GET as checkGet } from "../../app/api/auth/check/route";
24
+ import { POST as setupPost } from "../../app/api/auth/setup/route";
25
+ import { POST as registerPost } from "../../app/api/auth/register/route";
26
+ import { POST as loginPost } from "../../app/api/auth/login/route";
27
+
28
+ /**
29
+ * Helper to create a mock Request object
30
+ */
31
+ function createRequest(
32
+ method: string,
33
+ body?: Record<string, unknown>,
34
+ url: string = "http://localhost:3000"
35
+ ): Request {
36
+ const init: RequestInit = { method };
37
+ if (body) {
38
+ init.body = JSON.stringify(body);
39
+ init.headers = { "Content-Type": "application/json" };
40
+ }
41
+ return new Request(url, init);
42
+ }
43
+
44
+ /**
45
+ * Helper to parse JSON response
46
+ */
47
+ async function parseResponse(response: Response): Promise<{
48
+ status: number;
49
+ data: Record<string, unknown>;
50
+ }> {
51
+ const data = await response.json();
52
+ return { status: response.status, data };
53
+ }
54
+
55
+ describe("Auth API Routes", () => {
56
+ beforeEach(() => {
57
+ resetMockStores();
58
+ mockCortex = createMockCortex();
59
+ });
60
+
61
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62
+ // GET /api/auth/check
63
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
64
+
65
+ describe("GET /api/auth/check", () => {
66
+ it("should return isSetup: false when admin not configured", async () => {
67
+ const response = await checkGet();
68
+ const { status, data } = await parseResponse(response);
69
+
70
+ expect(status).toBe(200);
71
+ expect(data.isSetup).toBe(false);
72
+ });
73
+
74
+ it("should return isSetup: true when admin configured", async () => {
75
+ // Seed admin password hash
76
+ seedTestData.mutable(
77
+ "quickstart-config",
78
+ "admin_password_hash",
79
+ "somehash:value"
80
+ );
81
+
82
+ const response = await checkGet();
83
+ const { status, data } = await parseResponse(response);
84
+
85
+ expect(status).toBe(200);
86
+ expect(data.isSetup).toBe(true);
87
+ });
88
+
89
+ it("should call cortex.mutable.get with correct namespace and key", async () => {
90
+ await checkGet();
91
+
92
+ expect(mockCortex.mutable.get).toHaveBeenCalledWith(
93
+ "quickstart-config",
94
+ "admin_password_hash"
95
+ );
96
+ });
97
+ });
98
+
99
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100
+ // POST /api/auth/setup
101
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102
+
103
+ describe("POST /api/auth/setup", () => {
104
+ it("should return 400 if password is missing", async () => {
105
+ const request = createRequest("POST", {});
106
+ const response = await setupPost(request);
107
+ const { status, data } = await parseResponse(response);
108
+
109
+ expect(status).toBe(400);
110
+ expect(data.error).toBe("Password is required");
111
+ });
112
+
113
+ it("should return 400 if password is not a string", async () => {
114
+ const request = createRequest("POST", { password: 12345 });
115
+ const response = await setupPost(request);
116
+ const { status, data } = await parseResponse(response);
117
+
118
+ expect(status).toBe(400);
119
+ expect(data.error).toBe("Password is required");
120
+ });
121
+
122
+ it("should return 400 if password is less than 4 characters", async () => {
123
+ const request = createRequest("POST", { password: "abc" });
124
+ const response = await setupPost(request);
125
+ const { status, data } = await parseResponse(response);
126
+
127
+ expect(status).toBe(400);
128
+ expect(data.error).toBe("Password must be at least 4 characters");
129
+ });
130
+
131
+ it("should return 409 if admin already configured", async () => {
132
+ // Seed existing admin
133
+ seedTestData.mutable(
134
+ "quickstart-config",
135
+ "admin_password_hash",
136
+ "existing:hash"
137
+ );
138
+
139
+ const request = createRequest("POST", { password: "newpassword" });
140
+ const response = await setupPost(request);
141
+ const { status, data } = await parseResponse(response);
142
+
143
+ expect(status).toBe(409);
144
+ expect(data.error).toBe("Admin already configured");
145
+ });
146
+
147
+ it("should store hashed password and return success", async () => {
148
+ const request = createRequest("POST", { password: "adminpass123" });
149
+ const response = await setupPost(request);
150
+ const { status, data } = await parseResponse(response);
151
+
152
+ expect(status).toBe(200);
153
+ expect(data.success).toBe(true);
154
+ expect(data.message).toBe("Admin password configured successfully");
155
+
156
+ // Verify mutable.set was called with a hashed password
157
+ expect(mockCortex.mutable.set).toHaveBeenCalledWith(
158
+ "quickstart-config",
159
+ "admin_password_hash",
160
+ expect.stringContaining(":") // salt:hash format
161
+ );
162
+ });
163
+ });
164
+
165
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
166
+ // POST /api/auth/register
167
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
168
+
169
+ describe("POST /api/auth/register", () => {
170
+ it("should return 400 if username is missing", async () => {
171
+ const request = createRequest("POST", { password: "pass1234" });
172
+ const response = await registerPost(request);
173
+ const { status, data } = await parseResponse(response);
174
+
175
+ expect(status).toBe(400);
176
+ expect(data.error).toBe("Username is required");
177
+ });
178
+
179
+ it("should return 400 if password is missing", async () => {
180
+ const request = createRequest("POST", { username: "testuser" });
181
+ const response = await registerPost(request);
182
+ const { status, data } = await parseResponse(response);
183
+
184
+ expect(status).toBe(400);
185
+ expect(data.error).toBe("Password is required");
186
+ });
187
+
188
+ it("should return 400 if username is less than 2 characters", async () => {
189
+ const request = createRequest("POST", {
190
+ username: "a",
191
+ password: "pass1234",
192
+ });
193
+ const response = await registerPost(request);
194
+ const { status, data } = await parseResponse(response);
195
+
196
+ expect(status).toBe(400);
197
+ expect(data.error).toBe("Username must be at least 2 characters");
198
+ });
199
+
200
+ it("should return 400 if password is less than 4 characters", async () => {
201
+ const request = createRequest("POST", {
202
+ username: "testuser",
203
+ password: "abc",
204
+ });
205
+ const response = await registerPost(request);
206
+ const { status, data } = await parseResponse(response);
207
+
208
+ expect(status).toBe(400);
209
+ expect(data.error).toBe("Password must be at least 4 characters");
210
+ });
211
+
212
+ it("should return 400 for invalid username characters", async () => {
213
+ const request = createRequest("POST", {
214
+ username: "test user@!",
215
+ password: "pass1234",
216
+ });
217
+ const response = await registerPost(request);
218
+ const { status, data } = await parseResponse(response);
219
+
220
+ expect(status).toBe(400);
221
+ expect(data.error).toContain("can only contain");
222
+ });
223
+
224
+ it("should return 409 if username already exists", async () => {
225
+ // Seed existing user
226
+ seedTestData.user("existinguser", { displayName: "Existing User" });
227
+
228
+ const request = createRequest("POST", {
229
+ username: "existinguser",
230
+ password: "pass1234",
231
+ });
232
+ const response = await registerPost(request);
233
+ const { status, data } = await parseResponse(response);
234
+
235
+ expect(status).toBe(409);
236
+ expect(data.error).toBe("Username already taken");
237
+ });
238
+
239
+ it("should create user and return success with sessionToken", async () => {
240
+ const request = createRequest("POST", {
241
+ username: "NewUser",
242
+ password: "pass1234",
243
+ displayName: "New User Display",
244
+ });
245
+ const response = await registerPost(request);
246
+ const { status, data } = await parseResponse(response);
247
+
248
+ expect(status).toBe(200);
249
+ expect(data.success).toBe(true);
250
+ expect(data.user).toEqual({
251
+ id: "newuser", // lowercase
252
+ displayName: "New User Display",
253
+ });
254
+ expect(data.sessionToken).toBeDefined();
255
+ expect((data.sessionToken as string).length).toBe(64);
256
+
257
+ // Verify user was created with hashed password
258
+ expect(mockCortex.users.update).toHaveBeenCalledWith(
259
+ "newuser",
260
+ expect.objectContaining({
261
+ displayName: "New User Display",
262
+ passwordHash: expect.stringContaining(":"),
263
+ createdAt: expect.any(Number),
264
+ lastLoginAt: expect.any(Number),
265
+ })
266
+ );
267
+ });
268
+
269
+ it("should normalize username to lowercase", async () => {
270
+ const request = createRequest("POST", {
271
+ username: "TestUser",
272
+ password: "pass1234",
273
+ });
274
+ const response = await registerPost(request);
275
+ const { status, data } = await parseResponse(response);
276
+
277
+ expect(status).toBe(200);
278
+ expect((data.user as { id: string }).id).toBe("testuser");
279
+ });
280
+
281
+ it("should use username as displayName if not provided", async () => {
282
+ const request = createRequest("POST", {
283
+ username: "testuser",
284
+ password: "pass1234",
285
+ });
286
+ const response = await registerPost(request);
287
+ const { status, data } = await parseResponse(response);
288
+
289
+ expect(status).toBe(200);
290
+ expect((data.user as { displayName: string }).displayName).toBe(
291
+ "testuser"
292
+ );
293
+ });
294
+
295
+ it("should allow underscores and hyphens in username", async () => {
296
+ const request = createRequest("POST", {
297
+ username: "test_user-123",
298
+ password: "pass1234",
299
+ });
300
+ const response = await registerPost(request);
301
+ const { status, data } = await parseResponse(response);
302
+
303
+ expect(status).toBe(200);
304
+ expect((data.user as { id: string }).id).toBe("test_user-123");
305
+ });
306
+ });
307
+
308
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
309
+ // POST /api/auth/login
310
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
311
+
312
+ describe("POST /api/auth/login", () => {
313
+ it("should return 400 if username is missing", async () => {
314
+ const request = createRequest("POST", { password: "pass1234" });
315
+ const response = await loginPost(request);
316
+ const { status, data } = await parseResponse(response);
317
+
318
+ expect(status).toBe(400);
319
+ expect(data.error).toBe("Username is required");
320
+ });
321
+
322
+ it("should return 400 if password is missing", async () => {
323
+ const request = createRequest("POST", { username: "testuser" });
324
+ const response = await loginPost(request);
325
+ const { status, data } = await parseResponse(response);
326
+
327
+ expect(status).toBe(400);
328
+ expect(data.error).toBe("Password is required");
329
+ });
330
+
331
+ it("should return 401 for non-existent user", async () => {
332
+ const request = createRequest("POST", {
333
+ username: "nonexistent",
334
+ password: "pass1234",
335
+ });
336
+ const response = await loginPost(request);
337
+ const { status, data } = await parseResponse(response);
338
+
339
+ expect(status).toBe(401);
340
+ expect(data.error).toBe("Invalid username or password");
341
+ });
342
+
343
+ it("should return 401 for wrong password", async () => {
344
+ // Seed user with known password hash
345
+ const passwordHash = await hashPassword("correctpassword");
346
+ seedTestData.user("testuser", {
347
+ displayName: "Test User",
348
+ passwordHash,
349
+ });
350
+
351
+ const request = createRequest("POST", {
352
+ username: "testuser",
353
+ password: "wrongpassword",
354
+ });
355
+ const response = await loginPost(request);
356
+ const { status, data } = await parseResponse(response);
357
+
358
+ expect(status).toBe(401);
359
+ expect(data.error).toBe("Invalid username or password");
360
+ });
361
+
362
+ it("should return 401 if user has no password hash", async () => {
363
+ // Seed user without password hash
364
+ seedTestData.user("testuser", { displayName: "Test User" });
365
+
366
+ const request = createRequest("POST", {
367
+ username: "testuser",
368
+ password: "anypassword",
369
+ });
370
+ const response = await loginPost(request);
371
+ const { status, data } = await parseResponse(response);
372
+
373
+ expect(status).toBe(401);
374
+ expect(data.error).toBe("Invalid username or password");
375
+ });
376
+
377
+ it("should return success with user data and sessionToken for valid credentials", async () => {
378
+ // Seed user with known password
379
+ const passwordHash = await hashPassword("correctpassword");
380
+ seedTestData.user("testuser", {
381
+ displayName: "Test User Display",
382
+ passwordHash,
383
+ });
384
+
385
+ const request = createRequest("POST", {
386
+ username: "testuser",
387
+ password: "correctpassword",
388
+ });
389
+ const response = await loginPost(request);
390
+ const { status, data } = await parseResponse(response);
391
+
392
+ expect(status).toBe(200);
393
+ expect(data.success).toBe(true);
394
+ expect(data.user).toEqual({
395
+ id: "testuser",
396
+ displayName: "Test User Display",
397
+ });
398
+ expect(data.sessionToken).toBeDefined();
399
+ expect((data.sessionToken as string).length).toBe(64);
400
+ });
401
+
402
+ it("should update lastLoginAt on successful login", async () => {
403
+ const passwordHash = await hashPassword("correctpassword");
404
+ seedTestData.user("testuser", {
405
+ displayName: "Test User",
406
+ passwordHash,
407
+ });
408
+
409
+ const request = createRequest("POST", {
410
+ username: "testuser",
411
+ password: "correctpassword",
412
+ });
413
+ await loginPost(request);
414
+
415
+ expect(mockCortex.users.update).toHaveBeenCalledWith("testuser", {
416
+ lastLoginAt: expect.any(Number),
417
+ });
418
+ });
419
+
420
+ it("should normalize username to lowercase for lookup", async () => {
421
+ const passwordHash = await hashPassword("correctpassword");
422
+ seedTestData.user("testuser", {
423
+ displayName: "Test User",
424
+ passwordHash,
425
+ });
426
+
427
+ const request = createRequest("POST", {
428
+ username: "TestUser",
429
+ password: "correctpassword",
430
+ });
431
+ const response = await loginPost(request);
432
+ const { status, data } = await parseResponse(response);
433
+
434
+ expect(status).toBe(200);
435
+ expect(data.success).toBe(true);
436
+ });
437
+
438
+ it("should use username as displayName if displayName not set", async () => {
439
+ const passwordHash = await hashPassword("correctpassword");
440
+ seedTestData.user("testuser", { passwordHash }); // No displayName
441
+
442
+ const request = createRequest("POST", {
443
+ username: "testuser",
444
+ password: "correctpassword",
445
+ });
446
+ const response = await loginPost(request);
447
+ const { status, data } = await parseResponse(response);
448
+
449
+ expect(status).toBe(200);
450
+ expect((data.user as { displayName: string }).displayName).toBe(
451
+ "testuser"
452
+ );
453
+ });
454
+ });
455
+ });