@cortexmemory/cli 0.1.0 → 0.1.1

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.
@@ -11,9 +11,67 @@ import ora from "ora";
11
11
  import { withClient } from "../utils/client.js";
12
12
  import { resolveConfig } from "../utils/config.js";
13
13
  import { formatOutput, printSuccess, printError, printWarning, printSection, formatTimestamp, formatBytes, } from "../utils/formatting.js";
14
- import { validateFilePath, requireConfirmation, requireExactConfirmation, } from "../utils/validation.js";
14
+ import { validateFilePath, requireConfirmation } from "../utils/validation.js";
15
15
  import { writeFile, readFile } from "fs/promises";
16
16
  import pc from "picocolors";
17
+ import prompts from "prompts";
18
+ import { listDeployments } from "../utils/config.js";
19
+ const MAX_LIMIT = 1000;
20
+ /**
21
+ * Select a database deployment interactively or from options
22
+ * Returns updated globalOpts with the selected deployment
23
+ */
24
+ async function selectDatabase(config, globalOpts, actionDescription) {
25
+ const deployments = listDeployments(config);
26
+ if (deployments.length === 0) {
27
+ printError("No deployments configured. Run 'cortex setup' first.");
28
+ return null;
29
+ }
30
+ // Determine target deployment
31
+ let targetDeployment = deployments.find((d) => d.isDefault);
32
+ let targetUrl = targetDeployment?.url ?? "";
33
+ let targetName = targetDeployment?.name ?? config.default;
34
+ // If --deployment flag was passed, use that
35
+ if (globalOpts.deployment) {
36
+ const specified = deployments.find((d) => d.name === globalOpts.deployment);
37
+ if (specified) {
38
+ targetDeployment = specified;
39
+ targetUrl = specified.url;
40
+ targetName = specified.name;
41
+ }
42
+ else {
43
+ printError(`Deployment "${globalOpts.deployment}" not found`);
44
+ return null;
45
+ }
46
+ }
47
+ // If multiple deployments and none specified, ask which one
48
+ if (deployments.length > 1 && !globalOpts.deployment) {
49
+ console.log();
50
+ console.log(`Current target: ${pc.cyan(targetName)} (${pc.dim(targetUrl)})`);
51
+ console.log();
52
+ const selectResponse = await prompts({
53
+ type: "select",
54
+ name: "deployment",
55
+ message: `Select database to ${actionDescription}:`,
56
+ choices: deployments.map((d) => ({
57
+ title: d.isDefault ? `${d.name} (default)` : d.name,
58
+ description: d.url,
59
+ value: d.name,
60
+ })),
61
+ initial: deployments.findIndex((d) => d.name === targetName),
62
+ });
63
+ if (!selectResponse.deployment) {
64
+ printWarning("Operation cancelled");
65
+ return null;
66
+ }
67
+ targetName = selectResponse.deployment;
68
+ const selected = deployments.find((d) => d.name === targetName);
69
+ targetUrl = selected?.url ?? "";
70
+ // Update globalOpts to use selected deployment
71
+ globalOpts = { ...globalOpts, deployment: targetName };
72
+ }
73
+ return { globalOpts, targetName, targetUrl };
74
+ }
17
75
  /**
18
76
  * Register database commands
19
77
  */
@@ -24,65 +82,137 @@ export function registerDbCommands(program, config) {
24
82
  .description("Show database statistics")
25
83
  .option("-f, --format <format>", "Output format: table, json")
26
84
  .action(async (options) => {
27
- const globalOpts = program.opts();
85
+ let globalOpts = program.opts();
28
86
  const resolved = resolveConfig(config, globalOpts);
29
87
  const format = (options.format ?? resolved.format);
30
- const spinner = ora("Loading database statistics...").start();
31
88
  try {
89
+ // Select database
90
+ const selection = await selectDatabase(config, globalOpts, "view stats for");
91
+ if (!selection)
92
+ return;
93
+ globalOpts = selection.globalOpts;
94
+ const { targetName, targetUrl } = selection;
95
+ const spinner = ora(`Loading statistics for ${targetName}...`).start();
32
96
  await withClient(config, globalOpts, async (client) => {
33
- // Get counts from all tables
34
- const [spacesCount, usersCount] = await Promise.all([
35
- client.memorySpaces.count(),
36
- client.users.count(),
37
- ]);
38
- // Get space-level statistics
39
- const spaces = await client.memorySpaces.list({ limit: 1000 });
40
- let totalMemories = 0;
41
- let totalConversations = 0;
42
- let totalFacts = 0;
43
- for (const space of spaces) {
44
- try {
45
- const stats = await client.memorySpaces.getStats(space.memorySpaceId);
46
- totalMemories += stats.totalMemories;
47
- totalConversations += stats.totalConversations;
48
- totalFacts += stats.totalFacts;
49
- }
50
- catch {
51
- // Skip if stats not available
97
+ // Get deployment info
98
+ const info = {
99
+ url: targetUrl,
100
+ isLocal: targetUrl.includes("127.0.0.1") ||
101
+ targetUrl.includes("localhost"),
102
+ };
103
+ const rawClient = client.getClient();
104
+ // Get comprehensive counts from all tables using admin function
105
+ spinner.text = "Counting all tables...";
106
+ let tableCounts = {};
107
+ try {
108
+ tableCounts = await rawClient.query("admin:getAllCounts", {});
109
+ }
110
+ catch {
111
+ // Fall back to individual counts if admin function not available
112
+ tableCounts = {
113
+ agents: 0,
114
+ contexts: 0,
115
+ conversations: 0,
116
+ facts: 0,
117
+ governanceEnforcement: 0,
118
+ governancePolicies: 0,
119
+ graphSyncQueue: 0,
120
+ immutable: 0,
121
+ memories: 0,
122
+ memorySpaces: 0,
123
+ mutable: 0,
124
+ };
125
+ }
126
+ // Get user count from SDK (users may be managed separately)
127
+ let usersCount = 0;
128
+ try {
129
+ usersCount = await client.users.count();
130
+ }
131
+ catch {
132
+ // Users API may not be available
133
+ }
134
+ // Count messages in conversations
135
+ spinner.text = "Counting messages...";
136
+ let totalMessages = 0;
137
+ try {
138
+ const convos = await client.conversations.list({
139
+ limit: MAX_LIMIT,
140
+ });
141
+ for (const convo of convos) {
142
+ totalMessages += convo.messageCount ?? 0;
52
143
  }
53
144
  }
145
+ catch {
146
+ // Skip if not available
147
+ }
54
148
  spinner.stop();
55
149
  const stats = {
56
- memorySpaces: spacesCount,
57
- conversations: totalConversations,
58
- memories: totalMemories,
59
- facts: totalFacts,
150
+ memorySpaces: tableCounts.memorySpaces ?? 0,
151
+ conversations: tableCounts.conversations ?? 0,
152
+ memories: tableCounts.memories ?? 0,
153
+ facts: tableCounts.facts ?? 0,
60
154
  users: usersCount,
61
- immutableRecords: 0, // Would need separate query
62
- mutableRecords: 0, // Would need separate query
63
- contexts: 0, // Would need separate query
155
+ immutableRecords: tableCounts.immutable ?? 0,
156
+ mutableRecords: tableCounts.mutable ?? 0,
157
+ contexts: tableCounts.contexts ?? 0,
64
158
  };
65
159
  if (format === "json") {
66
- console.log(formatOutput(stats, "json"));
160
+ console.log(formatOutput({
161
+ ...stats,
162
+ agents: tableCounts.agents ?? 0,
163
+ messages: totalMessages,
164
+ governancePolicies: tableCounts.governancePolicies ?? 0,
165
+ governanceEnforcement: tableCounts.governanceEnforcement ?? 0,
166
+ graphSyncQueue: tableCounts.graphSyncQueue ?? 0,
167
+ deployment: {
168
+ name: globalOpts.deployment ?? config.default ?? "default",
169
+ url: info.url,
170
+ isLocal: info.isLocal,
171
+ },
172
+ }, "json"));
67
173
  }
68
174
  else {
69
175
  console.log();
70
- printSection("Database Statistics", {
71
- "Memory Spaces": stats.memorySpaces,
72
- "Total Memories": stats.memories,
73
- "Total Conversations": stats.conversations,
74
- "Total Facts": stats.facts,
75
- "User Profiles": stats.users,
76
- });
77
- // Show deployment info
78
- const info = (await import("../utils/client.js")).getDeploymentInfo(config, globalOpts);
79
- console.log(` ${pc.dim("Deployment:")} ${info.url}`);
80
- console.log(` ${pc.dim("Mode:")} ${info.isLocal ? "Local" : "Cloud"}`);
176
+ console.log(pc.bold(`📊 Database Statistics: ${pc.cyan(targetName)}`));
177
+ console.log(pc.dim("".repeat(45)));
178
+ console.log();
179
+ // Core entities
180
+ console.log(pc.bold(" Core Entities"));
181
+ console.log(` Memory Spaces: ${pc.yellow(String(stats.memorySpaces))}`);
182
+ console.log(` Users: ${pc.yellow(String(stats.users))}`);
183
+ console.log(` Agents: ${pc.yellow(String(tableCounts.agents ?? 0))}`);
184
+ console.log();
185
+ // Memory data
186
+ console.log(pc.bold(" Memory Data"));
187
+ console.log(` Memories: ${pc.yellow(String(stats.memories))}`);
188
+ console.log(` Facts: ${pc.yellow(String(stats.facts))}`);
189
+ console.log(` Contexts: ${pc.yellow(String(stats.contexts))}`);
190
+ console.log();
191
+ // Conversation data
192
+ console.log(pc.bold(" Conversations"));
193
+ console.log(` Conversations: ${pc.yellow(String(stats.conversations))}`);
194
+ console.log(` Messages: ${pc.yellow(String(totalMessages))}`);
195
+ console.log();
196
+ // Shared stores
197
+ console.log(pc.bold(" Shared Stores"));
198
+ console.log(` Immutable: ${pc.yellow(String(stats.immutableRecords))}`);
199
+ console.log(` Mutable: ${pc.yellow(String(stats.mutableRecords))}`);
200
+ console.log();
201
+ // System tables
202
+ console.log(pc.bold(" System Tables"));
203
+ console.log(` Gov. Policies: ${pc.yellow(String(tableCounts.governancePolicies ?? 0))}`);
204
+ console.log(` Gov. Logs: ${pc.yellow(String(tableCounts.governanceEnforcement ?? 0))}`);
205
+ console.log(` Graph Sync Queue: ${pc.yellow(String(tableCounts.graphSyncQueue ?? 0))}`);
206
+ console.log();
207
+ // Deployment info
208
+ console.log(pc.bold(" Deployment"));
209
+ console.log(` URL: ${pc.dim(info.url)}`);
210
+ console.log(` Mode: ${info.isLocal ? pc.green("Local") : pc.blue("Cloud")}`);
211
+ console.log();
81
212
  }
82
213
  });
83
214
  }
84
215
  catch (error) {
85
- spinner.stop();
86
216
  printError(error instanceof Error ? error.message : "Failed to load statistics");
87
217
  process.exit(1);
88
218
  }
@@ -90,67 +220,368 @@ export function registerDbCommands(program, config) {
90
220
  // db clear
91
221
  db.command("clear")
92
222
  .description("Clear entire database (DANGEROUS!)")
93
- .option("--confirm <text>", 'Confirmation text: "I understand this is irreversible"')
223
+ .option("-y, --yes", "Skip confirmation prompt", false)
94
224
  .action(async (options) => {
95
- const globalOpts = program.opts();
225
+ let globalOpts = program.opts();
96
226
  try {
97
- // Require exact confirmation text
98
- const expectedText = "I understand this is irreversible";
99
- if (options.confirm !== expectedText) {
100
- console.log();
101
- console.log(pc.red(pc.bold("⚠️ DANGER: This will DELETE ALL DATA in the database!")));
102
- console.log();
103
- console.log("This operation will permanently delete:");
104
- console.log(" • All memory spaces");
105
- console.log(" • All memories");
106
- console.log(" All conversations");
107
- console.log(" • All facts");
108
- console.log(" • All user profiles");
109
- console.log(" • All immutable and mutable records");
110
- console.log(" • All contexts");
111
- console.log();
112
- console.log(pc.yellow("This cannot be undone!"));
113
- console.log();
114
- const confirmed = await requireExactConfirmation(expectedText, `To proceed, type exactly: "${expectedText}"`);
115
- if (!confirmed) {
227
+ console.log();
228
+ console.log(pc.red(pc.bold("⚠️ DANGER: Clear Database")));
229
+ // Select database
230
+ const selection = await selectDatabase(config, globalOpts, "clear");
231
+ if (!selection)
232
+ return;
233
+ globalOpts = selection.globalOpts;
234
+ const { targetName, targetUrl } = selection;
235
+ console.log();
236
+ console.log("This will permanently delete:");
237
+ console.log(" • All memory spaces and memories");
238
+ console.log(" • All conversations and messages");
239
+ console.log(" • All facts and user profiles");
240
+ // Check if graph sync is enabled (same logic as Cortex.create())
241
+ const neo4jUri = process.env.NEO4J_URI;
242
+ const memgraphUri = process.env.MEMGRAPH_URI;
243
+ const graphSyncEnabled = process.env.CORTEX_GRAPH_SYNC === "true" ||
244
+ !!(neo4jUri || memgraphUri);
245
+ // Debug: Show env var detection
246
+ if (process.env.DEBUG || program.opts().debug) {
247
+ console.log(pc.dim(` [DEBUG] CORTEX_GRAPH_SYNC=${process.env.CORTEX_GRAPH_SYNC}`));
248
+ console.log(pc.dim(` [DEBUG] NEO4J_URI=${neo4jUri ? "set" : "unset"}`));
249
+ console.log(pc.dim(` [DEBUG] MEMGRAPH_URI=${memgraphUri ? "set" : "unset"}`));
250
+ console.log(pc.dim(` [DEBUG] graphSyncEnabled=${graphSyncEnabled}`));
251
+ }
252
+ if (graphSyncEnabled) {
253
+ const dbType = neo4jUri ? "Neo4j" : "Memgraph";
254
+ console.log(` • All graph database nodes and relationships (${dbType})`);
255
+ }
256
+ console.log();
257
+ // Simple y/N confirmation
258
+ if (!options.yes) {
259
+ const confirmResponse = await prompts({
260
+ type: "confirm",
261
+ name: "confirmed",
262
+ message: `Clear ALL data from ${pc.red(targetName)}?`,
263
+ initial: false,
264
+ });
265
+ if (!confirmResponse.confirmed) {
116
266
  printWarning("Operation cancelled");
117
267
  return;
118
268
  }
119
269
  }
120
- const spinner = ora("Clearing database...").start();
270
+ const spinner = ora(`Clearing ${targetName}...`).start();
121
271
  await withClient(config, globalOpts, async (client) => {
122
- // Delete all memory spaces (with cascade)
123
- const spaces = await client.memorySpaces.list({ limit: 10000 });
124
- let deletedSpaces = 0;
125
- for (const space of spaces) {
272
+ const deleted = {
273
+ agents: 0,
274
+ contexts: 0,
275
+ conversations: 0,
276
+ messages: 0,
277
+ facts: 0,
278
+ memories: 0,
279
+ memorySpaces: 0,
280
+ immutable: 0,
281
+ mutable: 0,
282
+ users: 0,
283
+ governancePolicies: 0,
284
+ governanceEnforcement: 0,
285
+ graphSyncQueue: 0,
286
+ };
287
+ // Get raw Convex client for direct table access via admin functions
288
+ const rawClient = client.getClient();
289
+ // Helper to clear a table using the admin:clearTable mutation
290
+ const clearTableDirect = async (tableName, counter) => {
291
+ let hasMore = true;
292
+ while (hasMore) {
293
+ spinner.text = `Clearing ${tableName}... (${deleted[counter]} deleted)`;
294
+ try {
295
+ const result = await rawClient.mutation("admin:clearTable", { table: tableName, limit: MAX_LIMIT });
296
+ deleted[counter] += result.deleted;
297
+ hasMore = result.hasMore;
298
+ }
299
+ catch {
300
+ hasMore = false;
301
+ }
302
+ }
303
+ };
304
+ // 1. Clear agents (using SDK for proper unregister)
305
+ let hasMoreAgents = true;
306
+ while (hasMoreAgents) {
307
+ spinner.text = `Clearing agents... (${deleted.agents} deleted)`;
126
308
  try {
127
- await client.memorySpaces.delete(space.memorySpaceId, {
128
- cascade: true,
309
+ const agents = await client.agents.list({ limit: MAX_LIMIT });
310
+ if (agents.length === 0) {
311
+ hasMoreAgents = false;
312
+ break;
313
+ }
314
+ for (const agent of agents) {
315
+ try {
316
+ await client.agents.unregister(agent.id, { cascade: false });
317
+ deleted.agents++;
318
+ }
319
+ catch {
320
+ // Continue on error
321
+ }
322
+ }
323
+ if (agents.length < MAX_LIMIT) {
324
+ hasMoreAgents = false;
325
+ }
326
+ }
327
+ catch {
328
+ // Fall back to direct table clear if SDK fails
329
+ await clearTableDirect("agents", "agents");
330
+ hasMoreAgents = false;
331
+ }
332
+ }
333
+ // 2. Clear contexts (using SDK for cascade)
334
+ let hasMoreContexts = true;
335
+ while (hasMoreContexts) {
336
+ spinner.text = `Clearing contexts... (${deleted.contexts} deleted)`;
337
+ try {
338
+ const contexts = await client.contexts.list({ limit: MAX_LIMIT });
339
+ if (contexts.length === 0) {
340
+ hasMoreContexts = false;
341
+ break;
342
+ }
343
+ for (const ctx of contexts) {
344
+ try {
345
+ await client.contexts.delete(ctx.contextId, {
346
+ cascadeChildren: true,
347
+ });
348
+ deleted.contexts++;
349
+ }
350
+ catch {
351
+ // Continue on error
352
+ }
353
+ }
354
+ if (contexts.length < MAX_LIMIT) {
355
+ hasMoreContexts = false;
356
+ }
357
+ }
358
+ catch {
359
+ await clearTableDirect("contexts", "contexts");
360
+ hasMoreContexts = false;
361
+ }
362
+ }
363
+ // 3. Clear conversations (count messages)
364
+ let hasMoreConvos = true;
365
+ while (hasMoreConvos) {
366
+ spinner.text = `Clearing conversations... (${deleted.conversations} deleted, ${deleted.messages} messages)`;
367
+ try {
368
+ const convos = await client.conversations.list({
369
+ limit: MAX_LIMIT,
129
370
  });
130
- deletedSpaces++;
371
+ if (convos.length === 0) {
372
+ hasMoreConvos = false;
373
+ break;
374
+ }
375
+ for (const convo of convos) {
376
+ try {
377
+ deleted.messages += convo.messageCount || 0;
378
+ await client.conversations.delete(convo.conversationId);
379
+ deleted.conversations++;
380
+ }
381
+ catch {
382
+ // Continue on error
383
+ }
384
+ }
385
+ if (convos.length < MAX_LIMIT) {
386
+ hasMoreConvos = false;
387
+ }
131
388
  }
132
389
  catch {
133
- // Continue on error
390
+ await clearTableDirect("conversations", "conversations");
391
+ hasMoreConvos = false;
134
392
  }
135
393
  }
136
- // Delete all users
137
- const users = await client.users.list({ limit: 10000 });
138
- let deletedUsers = 0;
139
- for (const user of users) {
394
+ // 4. Clear facts (direct table clear)
395
+ await clearTableDirect("facts", "facts");
396
+ // 5. Clear memories (direct table clear)
397
+ await clearTableDirect("memories", "memories");
398
+ // 6. Clear memory spaces (using raw client for invalid IDs)
399
+ let hasMoreSpaces = true;
400
+ while (hasMoreSpaces) {
401
+ spinner.text = `Clearing memorySpaces... (${deleted.memorySpaces} deleted)`;
140
402
  try {
141
- await client.users.delete(user.id, { cascade: true });
142
- deletedUsers++;
403
+ const spaces = await client.memorySpaces.list({
404
+ limit: MAX_LIMIT,
405
+ });
406
+ if (spaces.length === 0) {
407
+ hasMoreSpaces = false;
408
+ break;
409
+ }
410
+ for (const space of spaces) {
411
+ try {
412
+ await rawClient.mutation("memorySpaces:deleteSpace", { memorySpaceId: space.memorySpaceId, cascade: true });
413
+ deleted.memorySpaces++;
414
+ }
415
+ catch {
416
+ // Continue on error
417
+ }
418
+ }
419
+ if (spaces.length < MAX_LIMIT) {
420
+ hasMoreSpaces = false;
421
+ }
143
422
  }
144
423
  catch {
145
- // Continue on error
424
+ await clearTableDirect("memorySpaces", "memorySpaces");
425
+ hasMoreSpaces = false;
426
+ }
427
+ }
428
+ // 7. Clear immutable (using SDK)
429
+ let hasMoreImmutable = true;
430
+ while (hasMoreImmutable) {
431
+ spinner.text = `Clearing immutable... (${deleted.immutable} deleted)`;
432
+ try {
433
+ const records = await client.immutable.list({ limit: MAX_LIMIT });
434
+ if (records.length === 0) {
435
+ hasMoreImmutable = false;
436
+ break;
437
+ }
438
+ for (const record of records) {
439
+ try {
440
+ await client.immutable.purge(record.type, record.id);
441
+ deleted.immutable++;
442
+ }
443
+ catch {
444
+ // Continue on error
445
+ }
446
+ }
447
+ if (records.length < MAX_LIMIT) {
448
+ hasMoreImmutable = false;
449
+ }
450
+ }
451
+ catch {
452
+ await clearTableDirect("immutable", "immutable");
453
+ hasMoreImmutable = false;
454
+ }
455
+ }
456
+ // 8. Clear mutable (direct table clear)
457
+ await clearTableDirect("mutable", "mutable");
458
+ // 9. Clear users (using SDK for cascade - users table is virtual/SDK-managed)
459
+ let hasMoreUsers = true;
460
+ while (hasMoreUsers) {
461
+ spinner.text = `Clearing users... (${deleted.users} deleted)`;
462
+ try {
463
+ const users = await client.users.list({ limit: MAX_LIMIT });
464
+ if (users.length === 0) {
465
+ hasMoreUsers = false;
466
+ break;
467
+ }
468
+ for (const user of users) {
469
+ try {
470
+ await client.users.delete(user.id, { cascade: true });
471
+ deleted.users++;
472
+ }
473
+ catch {
474
+ // Continue on error
475
+ }
476
+ }
477
+ if (users.length < MAX_LIMIT) {
478
+ hasMoreUsers = false;
479
+ }
480
+ }
481
+ catch {
482
+ hasMoreUsers = false;
483
+ }
484
+ }
485
+ // 10. Clear governance policies
486
+ await clearTableDirect("governancePolicies", "governancePolicies");
487
+ // 11. Clear governance enforcement logs
488
+ await clearTableDirect("governanceEnforcement", "governanceEnforcement");
489
+ // 12. Clear graph sync queue
490
+ await clearTableDirect("graphSyncQueue", "graphSyncQueue");
491
+ // 13. Clear graph database if graph sync is enabled
492
+ // Check both explicit flag and auto-detection (same logic as Cortex.create())
493
+ const neo4jUri = process.env.NEO4J_URI;
494
+ const memgraphUri = process.env.MEMGRAPH_URI;
495
+ const graphSyncEnabled = process.env.CORTEX_GRAPH_SYNC === "true" ||
496
+ !!(neo4jUri || memgraphUri);
497
+ if (graphSyncEnabled) {
498
+ spinner.text = "Clearing graph database...";
499
+ let graphCleared = false;
500
+ try {
501
+ if (neo4jUri || memgraphUri) {
502
+ // Dynamically import neo4j-driver only when needed
503
+ const neo4j = await import("neo4j-driver");
504
+ // Determine which database to connect to
505
+ const uri = neo4jUri || memgraphUri;
506
+ const username = neo4jUri
507
+ ? process.env.NEO4J_USERNAME || "neo4j"
508
+ : process.env.MEMGRAPH_USERNAME || "memgraph";
509
+ const password = neo4jUri
510
+ ? process.env.NEO4J_PASSWORD || ""
511
+ : process.env.MEMGRAPH_PASSWORD || "";
512
+ // Connect to graph database
513
+ const driver = neo4j.default.driver(uri, neo4j.default.auth.basic(username, password));
514
+ // Verify connectivity
515
+ await driver.verifyConnectivity();
516
+ // Create session and clear all data
517
+ const session = driver.session();
518
+ try {
519
+ // DETACH DELETE removes nodes and all their relationships
520
+ // Works for both Neo4j and Memgraph
521
+ await session.run("MATCH (n) DETACH DELETE n");
522
+ graphCleared = true;
523
+ }
524
+ finally {
525
+ await session.close();
526
+ }
527
+ await driver.close();
528
+ }
529
+ }
530
+ catch (error) {
531
+ // Log warning but don't fail the entire operation
532
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
533
+ spinner.warn(pc.yellow(`Graph database clear failed: ${errorMsg}`));
534
+ }
535
+ if (graphCleared) {
536
+ // Only show success if we actually cleared
537
+ deleted.graphSyncQueue = -1; // Use as flag to indicate graph was cleared
146
538
  }
147
539
  }
148
540
  spinner.stop();
149
- printSuccess("Database cleared");
541
+ printSuccess(`Database "${targetName}" cleared`);
542
+ console.log();
150
543
  printSection("Deletion Summary", {
151
- "Memory Spaces": deletedSpaces,
152
- Users: deletedUsers,
544
+ Database: targetName,
545
+ URL: targetUrl,
153
546
  });
547
+ console.log();
548
+ // Show counts with categories
549
+ const coreEntities = {
550
+ Agents: deleted.agents,
551
+ Users: deleted.users,
552
+ "Memory Spaces": deleted.memorySpaces,
553
+ };
554
+ const memoryData = {
555
+ Memories: deleted.memories,
556
+ Facts: deleted.facts,
557
+ Contexts: deleted.contexts,
558
+ };
559
+ const conversationData = {
560
+ Conversations: deleted.conversations,
561
+ Messages: deleted.messages,
562
+ };
563
+ const sharedStores = {
564
+ Immutable: deleted.immutable,
565
+ Mutable: deleted.mutable,
566
+ };
567
+ const systemTables = {
568
+ "Governance Policies": deleted.governancePolicies,
569
+ "Governance Logs": deleted.governanceEnforcement,
570
+ "Graph Sync Queue": deleted.graphSyncQueue >= 0 ? deleted.graphSyncQueue : 0,
571
+ };
572
+ printSection("Core Entities", coreEntities);
573
+ printSection("Memory Data", memoryData);
574
+ printSection("Conversations", conversationData);
575
+ printSection("Shared Stores", sharedStores);
576
+ printSection("System Tables", systemTables);
577
+ // Show graph database status if it was cleared
578
+ if (deleted.graphSyncQueue === -1) {
579
+ const dbType = process.env.NEO4J_URI ? "Neo4j" : "Memgraph";
580
+ console.log();
581
+ printSection("Graph Database", {
582
+ [dbType]: pc.green("Cleared ✓"),
583
+ });
584
+ }
154
585
  });
155
586
  }
156
587
  catch (error) {
@@ -164,25 +595,31 @@ export function registerDbCommands(program, config) {
164
595
  .option("-o, --output <file>", "Output file path", "cortex-backup.json")
165
596
  .option("--include-all", "Include all data (may be large)", false)
166
597
  .action(async (options) => {
167
- const globalOpts = program.opts();
168
- const spinner = ora("Creating backup...").start();
598
+ let globalOpts = program.opts();
169
599
  try {
170
600
  validateFilePath(options.output);
601
+ // Select database
602
+ const selection = await selectDatabase(config, globalOpts, "backup");
603
+ if (!selection)
604
+ return;
605
+ globalOpts = selection.globalOpts;
606
+ const { targetName, targetUrl } = selection;
607
+ const spinner = ora(`Creating backup of ${targetName}...`).start();
171
608
  await withClient(config, globalOpts, async (client) => {
172
609
  const backup = {
173
610
  version: "1.0",
174
611
  timestamp: Date.now(),
175
- deployment: resolveConfig(config, globalOpts).url,
612
+ deployment: targetUrl,
176
613
  data: {},
177
614
  };
178
- // Backup memory spaces
615
+ // Backup memory spaces (paginate if needed)
179
616
  spinner.text = "Backing up memory spaces...";
180
617
  backup.data.memorySpaces = await client.memorySpaces.list({
181
- limit: 10000,
618
+ limit: MAX_LIMIT,
182
619
  });
183
- // Backup users
620
+ // Backup users (paginate if needed)
184
621
  spinner.text = "Backing up users...";
185
- backup.data.users = await client.users.list({ limit: 10000 });
622
+ backup.data.users = await client.users.list({ limit: MAX_LIMIT });
186
623
  if (options.includeAll) {
187
624
  // Backup conversations
188
625
  spinner.text = "Backing up conversations...";
@@ -191,7 +628,7 @@ export function registerDbCommands(program, config) {
191
628
  for (const space of spaces) {
192
629
  const convs = await client.conversations.list({
193
630
  memorySpaceId: space.memorySpaceId,
194
- limit: 10000,
631
+ limit: MAX_LIMIT,
195
632
  });
196
633
  backup.data.conversations.push(...convs);
197
634
  }
@@ -201,7 +638,7 @@ export function registerDbCommands(program, config) {
201
638
  for (const space of spaces) {
202
639
  const memories = await client.memory.list({
203
640
  memorySpaceId: space.memorySpaceId,
204
- limit: 10000,
641
+ limit: MAX_LIMIT,
205
642
  });
206
643
  backup.data.memories.push(...memories);
207
644
  }
@@ -211,7 +648,7 @@ export function registerDbCommands(program, config) {
211
648
  for (const space of spaces) {
212
649
  const facts = await client.facts.list({
213
650
  memorySpaceId: space.memorySpaceId,
214
- limit: 10000,
651
+ limit: MAX_LIMIT,
215
652
  });
216
653
  backup.data.facts.push(...facts);
217
654
  }
@@ -241,7 +678,6 @@ export function registerDbCommands(program, config) {
241
678
  });
242
679
  }
243
680
  catch (error) {
244
- spinner.stop();
245
681
  printError(error instanceof Error ? error.message : "Backup failed");
246
682
  process.exit(1);
247
683
  }
@@ -253,10 +689,10 @@ export function registerDbCommands(program, config) {
253
689
  .option("--dry-run", "Preview what would be restored", false)
254
690
  .option("-y, --yes", "Skip confirmation", false)
255
691
  .action(async (options) => {
256
- const globalOpts = program.opts();
692
+ let globalOpts = program.opts();
257
693
  try {
258
694
  validateFilePath(options.input);
259
- // Read backup file
695
+ // Read backup file first to show info before selecting target
260
696
  const content = await readFile(options.input, "utf-8");
261
697
  const backup = JSON.parse(content);
262
698
  // Validate backup format
@@ -279,14 +715,20 @@ export function registerDbCommands(program, config) {
279
715
  printWarning("DRY RUN - No data will be restored");
280
716
  return;
281
717
  }
718
+ // Select target database
719
+ const selection = await selectDatabase(config, globalOpts, "restore to");
720
+ if (!selection)
721
+ return;
722
+ globalOpts = selection.globalOpts;
723
+ const { targetName } = selection;
282
724
  if (!options.yes) {
283
- const confirmed = await requireConfirmation("Restore this backup? Existing data may be overwritten.", config);
725
+ const confirmed = await requireConfirmation(`Restore this backup to ${targetName}? Existing data may be overwritten.`, config);
284
726
  if (!confirmed) {
285
727
  printWarning("Restore cancelled");
286
728
  return;
287
729
  }
288
730
  }
289
- const spinner = ora("Restoring backup...").start();
731
+ const spinner = ora(`Restoring backup to ${targetName}...`).start();
290
732
  await withClient(config, globalOpts, async (client) => {
291
733
  let restored = {
292
734
  spaces: 0,
@@ -345,15 +787,22 @@ export function registerDbCommands(program, config) {
345
787
  .description("Export all data to JSON")
346
788
  .option("-o, --output <file>", "Output file path", "cortex-export.json")
347
789
  .action(async (options) => {
348
- const globalOpts = program.opts();
349
- const spinner = ora("Exporting data...").start();
790
+ let globalOpts = program.opts();
350
791
  try {
351
792
  validateFilePath(options.output);
793
+ // Select database
794
+ const selection = await selectDatabase(config, globalOpts, "export");
795
+ if (!selection)
796
+ return;
797
+ globalOpts = selection.globalOpts;
798
+ const { targetName, targetUrl } = selection;
799
+ const spinner = ora(`Exporting data from ${targetName}...`).start();
352
800
  await withClient(config, globalOpts, async (client) => {
353
801
  const exportData = {
354
802
  exportedAt: Date.now(),
355
- memorySpaces: await client.memorySpaces.list({ limit: 10000 }),
356
- users: await client.users.list({ limit: 10000 }),
803
+ deployment: { name: targetName, url: targetUrl },
804
+ memorySpaces: await client.memorySpaces.list({ limit: MAX_LIMIT }),
805
+ users: await client.users.list({ limit: MAX_LIMIT }),
357
806
  };
358
807
  const content = JSON.stringify(exportData, null, 2);
359
808
  await writeFile(options.output, content, "utf-8");
@@ -362,7 +811,6 @@ export function registerDbCommands(program, config) {
362
811
  });
363
812
  }
364
813
  catch (error) {
365
- spinner.stop();
366
814
  printError(error instanceof Error ? error.message : "Export failed");
367
815
  process.exit(1);
368
816
  }