@juspay/neurolink 9.87.3 → 9.88.0

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 (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/browser/neurolink.min.js +381 -381
  3. package/dist/cli/commands/proxy.d.ts +16 -1
  4. package/dist/cli/commands/proxy.js +275 -238
  5. package/dist/cli/factories/commandFactory.d.ts +39 -0
  6. package/dist/cli/factories/commandFactory.js +484 -3
  7. package/dist/core/conversationMemoryManager.d.ts +11 -1
  8. package/dist/core/conversationMemoryManager.js +44 -0
  9. package/dist/core/redisConversationMemoryManager.d.ts +12 -1
  10. package/dist/core/redisConversationMemoryManager.js +80 -0
  11. package/dist/lib/core/conversationMemoryManager.d.ts +11 -1
  12. package/dist/lib/core/conversationMemoryManager.js +44 -0
  13. package/dist/lib/core/redisConversationMemoryManager.d.ts +12 -1
  14. package/dist/lib/core/redisConversationMemoryManager.js +80 -0
  15. package/dist/lib/neurolink.d.ts +27 -0
  16. package/dist/lib/neurolink.js +133 -0
  17. package/dist/lib/proxy/globalInstaller.d.ts +5 -0
  18. package/dist/lib/proxy/globalInstaller.js +156 -0
  19. package/dist/lib/proxy/openaiFormat.d.ts +3 -1
  20. package/dist/lib/proxy/openaiFormat.js +19 -4
  21. package/dist/lib/proxy/proxyActivity.d.ts +8 -0
  22. package/dist/lib/proxy/proxyActivity.js +77 -0
  23. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +56 -6
  24. package/dist/lib/server/routes/claudeProxyRoutes.js +208 -50
  25. package/dist/lib/server/routes/openaiProxyRoutes.js +47 -6
  26. package/dist/lib/types/cli.d.ts +4 -0
  27. package/dist/lib/types/conversation.d.ts +36 -0
  28. package/dist/lib/types/conversationMemoryInterface.d.ts +5 -1
  29. package/dist/lib/types/proxy.d.ts +49 -0
  30. package/dist/lib/utils/fileDetector.js +34 -10
  31. package/dist/neurolink.d.ts +27 -0
  32. package/dist/neurolink.js +133 -0
  33. package/dist/proxy/globalInstaller.d.ts +5 -0
  34. package/dist/proxy/globalInstaller.js +155 -0
  35. package/dist/proxy/openaiFormat.d.ts +3 -1
  36. package/dist/proxy/openaiFormat.js +19 -4
  37. package/dist/proxy/proxyActivity.d.ts +8 -0
  38. package/dist/proxy/proxyActivity.js +76 -0
  39. package/dist/server/routes/claudeProxyRoutes.d.ts +56 -6
  40. package/dist/server/routes/claudeProxyRoutes.js +208 -50
  41. package/dist/server/routes/openaiProxyRoutes.js +47 -6
  42. package/dist/types/cli.d.ts +4 -0
  43. package/dist/types/conversation.d.ts +36 -0
  44. package/dist/types/conversationMemoryInterface.d.ts +5 -1
  45. package/dist/types/proxy.d.ts +49 -0
  46. package/dist/utils/fileDetector.js +34 -10
  47. package/package.json +3 -2
@@ -261,6 +261,45 @@ export declare class CLICommandFactory {
261
261
  * Execute memory clear command
262
262
  */
263
263
  private static executeMemoryClear;
264
+ /**
265
+ * Execute memory list command
266
+ */
267
+ private static executeMemoryList;
268
+ /**
269
+ * Resolve the requested export format for memory export commands.
270
+ * The generic `--format` flag is typed for the generate/stream commands
271
+ * (text/json/table/yaml), so read the raw value here and narrow it to the
272
+ * two serializations the memory export path actually supports.
273
+ */
274
+ private static resolveMemoryExportFormat;
275
+ /**
276
+ * Build a filesystem-safe filename for a session export.
277
+ *
278
+ * A session ID is caller-controlled data (it can come from Redis keys, user
279
+ * input, etc.), so it must never be interpolated directly into a path — a
280
+ * value like `../../etc/cron.d/x` would otherwise let export-all write
281
+ * outside the chosen output directory. We reduce the ID to a bare basename
282
+ * and allow-list its characters; callers additionally verify containment.
283
+ */
284
+ private static sanitizeSessionExportFilename;
285
+ /**
286
+ * Serialize one or more session exports to CSV. Each message becomes a row;
287
+ * session-level fields are repeated per row so the output is a flat,
288
+ * spreadsheet-friendly table.
289
+ */
290
+ private static sessionExportsToCsv;
291
+ /**
292
+ * Execute memory export command
293
+ */
294
+ private static executeMemoryExport;
295
+ /**
296
+ * Execute memory export-all command
297
+ */
298
+ private static executeMemoryExportAll;
299
+ /**
300
+ * Execute memory delete command
301
+ */
302
+ private static executeMemoryDelete;
264
303
  /**
265
304
  * Execute completion command
266
305
  */
@@ -1636,9 +1636,85 @@ export class CLICommandFactory {
1636
1636
  static createMemoryCommands() {
1637
1637
  return {
1638
1638
  command: "memory <subcommand>",
1639
- describe: "Manage conversation memory",
1639
+ describe: "Manage conversation memory and session history",
1640
1640
  builder: (yargs) => {
1641
1641
  return yargs
1642
+ .command("list", "List all conversation sessions with metadata", (y) => CLICommandFactory.buildOptions(y)
1643
+ .option("user-id", {
1644
+ type: "string",
1645
+ description: "User ID to filter sessions (required for Redis)",
1646
+ alias: "u",
1647
+ })
1648
+ .example("$0 memory list", "List all sessions")
1649
+ .example("$0 memory list --format json", "List sessions as JSON")
1650
+ .example("$0 memory list --user-id user123", "List sessions for specific user"), async (argv) => await CLICommandFactory.executeMemoryList(argv))
1651
+ .command("export", "Export a specific session to JSON/CSV", (y) => CLICommandFactory.buildOptions(y)
1652
+ .option("session-id", {
1653
+ type: "string",
1654
+ description: "Session ID to export",
1655
+ demandOption: true,
1656
+ alias: "s",
1657
+ })
1658
+ .option("include-metadata", {
1659
+ type: "boolean",
1660
+ default: false,
1661
+ description: "Include export metadata",
1662
+ })
1663
+ // Override the generic --format so this command accepts csv.
1664
+ // (Alias -f / --output-format comes from the common options.)
1665
+ .option("format", {
1666
+ choices: ["json", "csv"],
1667
+ default: "json",
1668
+ description: "Export format (json or csv)",
1669
+ })
1670
+ .example("$0 memory export --session-id session-123", "Export session to stdout")
1671
+ .example("$0 memory export --session-id session-123 --format json > history.json", "Export to file")
1672
+ .example("$0 memory export --session-id session-123 --format csv > history.csv", "Export as CSV")
1673
+ .example("$0 memory export --session-id session-123 --include-metadata", "Export with metadata"), async (argv) => await CLICommandFactory.executeMemoryExport(argv))
1674
+ .command("export-all", "Export all sessions to a directory", (y) => CLICommandFactory.buildOptions(y)
1675
+ .option("output", {
1676
+ type: "string",
1677
+ description: "Output directory for exported files",
1678
+ default: "./memory-exports",
1679
+ alias: "o",
1680
+ })
1681
+ .option("user-id", {
1682
+ type: "string",
1683
+ description: "User ID to filter sessions (required for Redis)",
1684
+ alias: "u",
1685
+ })
1686
+ .option("include-metadata", {
1687
+ type: "boolean",
1688
+ default: true,
1689
+ description: "Include export metadata",
1690
+ })
1691
+ // Override the generic --format so this command accepts csv;
1692
+ // it controls the extension/serialization of the per-session
1693
+ // files written to the output directory. (Alias -f /
1694
+ // --output-format comes from the common options.)
1695
+ .option("format", {
1696
+ choices: ["json", "csv"],
1697
+ default: "json",
1698
+ description: "Per-file export format (json or csv)",
1699
+ })
1700
+ .example("$0 memory export-all --output ./exports/", "Export all sessions to directory")
1701
+ .example("$0 memory export-all --user-id user123 --output ./user-exports/", "Export all sessions for user")
1702
+ .example("$0 memory export-all --output ./exports/ --format csv", "Export all sessions as CSV files"), async (argv) => await CLICommandFactory.executeMemoryExportAll(argv))
1703
+ .command("delete", "Delete a specific conversation session", (y) => CLICommandFactory.buildOptions(y)
1704
+ .option("session-id", {
1705
+ type: "string",
1706
+ description: "Session ID to delete",
1707
+ demandOption: true,
1708
+ alias: "s",
1709
+ })
1710
+ .option("force", {
1711
+ type: "boolean",
1712
+ default: false,
1713
+ description: "Skip confirmation prompt",
1714
+ alias: "f",
1715
+ })
1716
+ .example("$0 memory delete --session-id session-123", "Delete session with confirmation")
1717
+ .example("$0 memory delete --session-id session-123 --force", "Delete session without confirmation"), async (argv) => await CLICommandFactory.executeMemoryDelete(argv))
1642
1718
  .command("stats", "Show conversation memory statistics", (y) => CLICommandFactory.buildOptions(y)
1643
1719
  .example("$0 memory stats", "Show memory usage statistics")
1644
1720
  .example("$0 memory stats --format json", "Export stats as JSON"), async (argv) => await CLICommandFactory.executeMemoryStats(argv))
@@ -1650,13 +1726,18 @@ export class CLICommandFactory {
1650
1726
  })
1651
1727
  .example("$0 memory history session-123", "Show conversation history")
1652
1728
  .example("$0 memory history session-123 --format json", "Export history as JSON"), async (argv) => await CLICommandFactory.executeMemoryHistory(argv))
1653
- .command("clear [sessionId]", "Clear conversation history", (y) => CLICommandFactory.buildOptions(y)
1729
+ .command("clear [sessionId]", "Clear conversation history (use --confirm to clear all)", (y) => CLICommandFactory.buildOptions(y)
1654
1730
  .positional("sessionId", {
1655
1731
  type: "string",
1656
1732
  description: "Session ID to clear (omit to clear all sessions)",
1657
1733
  demandOption: false,
1658
1734
  })
1659
- .example("$0 memory clear", "Clear all conversation history")
1735
+ .option("confirm", {
1736
+ type: "boolean",
1737
+ default: false,
1738
+ description: "Confirm clearing all sessions",
1739
+ })
1740
+ .example("$0 memory clear --confirm", "Clear all conversation history")
1660
1741
  .example("$0 memory clear session-123", "Clear specific session"), async (argv) => await CLICommandFactory.executeMemoryClear(argv))
1661
1742
  .demandCommand(1, "Please specify a memory subcommand");
1662
1743
  },
@@ -3709,6 +3790,13 @@ export class CLICommandFactory {
3709
3790
  static async executeMemoryClear(argv) {
3710
3791
  const options = CLICommandFactory.processOptions(argv);
3711
3792
  const isAllSessions = !argv.sessionId;
3793
+ // Require --confirm flag when clearing all sessions
3794
+ if (isAllSessions && !argv.confirm) {
3795
+ logger.always(chalk.red("⚠️ Clearing all sessions requires --confirm flag"));
3796
+ logger.always(chalk.yellow("Usage: neurolink memory clear --confirm"));
3797
+ logger.always(chalk.gray("This safeguard prevents accidental data loss."));
3798
+ return;
3799
+ }
3712
3800
  const target = isAllSessions ? "all sessions" : `session ${argv.sessionId}`;
3713
3801
  const spinner = options.quiet
3714
3802
  ? null
@@ -3777,6 +3865,399 @@ export class CLICommandFactory {
3777
3865
  }
3778
3866
  }
3779
3867
  }
3868
+ /**
3869
+ * Execute memory list command
3870
+ */
3871
+ static async executeMemoryList(argv) {
3872
+ const options = CLICommandFactory.processOptions(argv);
3873
+ const spinner = options.quiet
3874
+ ? null
3875
+ : ora("📋 Listing conversation sessions...").start();
3876
+ try {
3877
+ const sdk = globalSession.getOrCreateNeuroLink();
3878
+ // Handle dry-run mode
3879
+ if (options.dryRun) {
3880
+ const mockSessions = [
3881
+ {
3882
+ id: "session-123",
3883
+ title: "Machine Learning Discussion",
3884
+ messageCount: 15,
3885
+ lastActive: "2 hours ago",
3886
+ createdAt: "2024-01-01T10:00:00Z",
3887
+ updatedAt: "2024-01-01T12:30:00Z",
3888
+ },
3889
+ {
3890
+ id: "session-456",
3891
+ title: "API Design Review",
3892
+ messageCount: 8,
3893
+ lastActive: "1 day ago",
3894
+ createdAt: "2024-01-02T14:00:00Z",
3895
+ updatedAt: "2024-01-02T15:15:00Z",
3896
+ },
3897
+ ];
3898
+ if (spinner) {
3899
+ spinner.succeed(chalk.green("✅ Sessions listed (dry-run)"));
3900
+ }
3901
+ CLICommandFactory.handleOutput(mockSessions, options);
3902
+ return;
3903
+ }
3904
+ const sessions = await sdk.listSessions(argv.userId);
3905
+ if (spinner) {
3906
+ spinner.succeed(chalk.green(`✅ Found ${sessions.length} session(s)`));
3907
+ }
3908
+ if (sessions.length === 0) {
3909
+ logger.always(chalk.yellow("⚠️ No conversation sessions found"));
3910
+ return;
3911
+ }
3912
+ if (options.format === "json") {
3913
+ CLICommandFactory.handleOutput(sessions, options);
3914
+ }
3915
+ else {
3916
+ // Table format output
3917
+ logger.always(chalk.blue(`📋 Conversation Sessions (${sessions.length} total):`));
3918
+ logger.always("");
3919
+ logger.always(chalk.gray("Session ID".padEnd(40) +
3920
+ " | " +
3921
+ "Messages".padEnd(10) +
3922
+ " | " +
3923
+ "Last Active"));
3924
+ logger.always(chalk.gray("-".repeat(75)));
3925
+ for (const session of sessions) {
3926
+ const idDisplay = session.id.length > 38
3927
+ ? session.id.substring(0, 35) + "..."
3928
+ : session.id.padEnd(40);
3929
+ const msgCount = String(session.messageCount).padEnd(10);
3930
+ const lastActive = session.lastActive || "Unknown";
3931
+ logger.always(`${idDisplay} | ${msgCount} | ${lastActive}`);
3932
+ }
3933
+ }
3934
+ }
3935
+ catch (error) {
3936
+ if (spinner) {
3937
+ spinner.fail("Session listing failed");
3938
+ }
3939
+ if (error.message.includes("not enabled")) {
3940
+ logger.always(chalk.yellow("⚠️ Conversation memory is not enabled"));
3941
+ logger.always("Enable it by using --enable-conversation-memory with loop mode");
3942
+ }
3943
+ else {
3944
+ handleError(error, "Memory list");
3945
+ }
3946
+ }
3947
+ }
3948
+ /**
3949
+ * Resolve the requested export format for memory export commands.
3950
+ * The generic `--format` flag is typed for the generate/stream commands
3951
+ * (text/json/table/yaml), so read the raw value here and narrow it to the
3952
+ * two serializations the memory export path actually supports.
3953
+ */
3954
+ static resolveMemoryExportFormat(argv) {
3955
+ const raw = typeof argv.format === "string" ? argv.format.toLowerCase() : "";
3956
+ return raw === "csv" ? "csv" : "json";
3957
+ }
3958
+ /**
3959
+ * Build a filesystem-safe filename for a session export.
3960
+ *
3961
+ * A session ID is caller-controlled data (it can come from Redis keys, user
3962
+ * input, etc.), so it must never be interpolated directly into a path — a
3963
+ * value like `../../etc/cron.d/x` would otherwise let export-all write
3964
+ * outside the chosen output directory. We reduce the ID to a bare basename
3965
+ * and allow-list its characters; callers additionally verify containment.
3966
+ */
3967
+ static sanitizeSessionExportFilename(sessionId, extension) {
3968
+ // Strip any directory components, then drop everything that isn't a safe
3969
+ // filename character and any leading dots (which could form `..`).
3970
+ const base = path.basename(String(sessionId ?? ""));
3971
+ const sanitized = base.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^\.+/, "");
3972
+ const safe = sanitized.length > 0 ? sanitized : "session";
3973
+ return `${safe}.${extension}`;
3974
+ }
3975
+ /**
3976
+ * Serialize one or more session exports to CSV. Each message becomes a row;
3977
+ * session-level fields are repeated per row so the output is a flat,
3978
+ * spreadsheet-friendly table.
3979
+ */
3980
+ static sessionExportsToCsv(exports) {
3981
+ const escape = (value) => {
3982
+ const s = value === undefined || value === null ? "" : String(value);
3983
+ return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
3984
+ };
3985
+ const header = [
3986
+ "sessionId",
3987
+ "title",
3988
+ "messageId",
3989
+ "role",
3990
+ "content",
3991
+ "timestamp",
3992
+ "tool",
3993
+ ];
3994
+ const rows = [header.join(",")];
3995
+ for (const exp of exports) {
3996
+ for (const msg of exp.messages) {
3997
+ rows.push([
3998
+ escape(exp.sessionId),
3999
+ escape(exp.title),
4000
+ escape(msg.id),
4001
+ escape(msg.role),
4002
+ escape(msg.content),
4003
+ escape(msg.timestamp ?? msg.metadata?.timestamp),
4004
+ escape(msg.tool),
4005
+ ].join(","));
4006
+ }
4007
+ }
4008
+ return rows.join("\r\n") + "\r\n";
4009
+ }
4010
+ /**
4011
+ * Execute memory export command
4012
+ */
4013
+ static async executeMemoryExport(argv) {
4014
+ const options = CLICommandFactory.processOptions(argv);
4015
+ const exportFormat = CLICommandFactory.resolveMemoryExportFormat(argv);
4016
+ const spinner = options.quiet
4017
+ ? null
4018
+ : ora(`📤 Exporting session ${argv.sessionId}...`).start();
4019
+ try {
4020
+ const sdk = globalSession.getOrCreateNeuroLink();
4021
+ // Handle dry-run mode
4022
+ if (options.dryRun) {
4023
+ const mockExport = {
4024
+ sessionId: argv.sessionId,
4025
+ title: "Sample Conversation",
4026
+ createdAt: "2024-01-01T10:00:00Z",
4027
+ updatedAt: "2024-01-01T12:30:00Z",
4028
+ messages: [
4029
+ { id: "msg-1", role: "user", content: "Hello!" },
4030
+ { id: "msg-2", role: "assistant", content: "Hi there!" },
4031
+ ],
4032
+ exportMetadata: argv.includeMetadata
4033
+ ? {
4034
+ exportedAt: new Date().toISOString(),
4035
+ exportFormat,
4036
+ }
4037
+ : undefined,
4038
+ };
4039
+ if (spinner) {
4040
+ spinner.succeed(chalk.green(`✅ Session exported (dry-run)`));
4041
+ }
4042
+ if (exportFormat === "csv") {
4043
+ logger.always(CLICommandFactory.sessionExportsToCsv([mockExport]));
4044
+ }
4045
+ else {
4046
+ CLICommandFactory.handleOutput(mockExport, {
4047
+ ...options,
4048
+ format: "json",
4049
+ });
4050
+ }
4051
+ return;
4052
+ }
4053
+ const exportData = await sdk.exportSession(argv.sessionId, {
4054
+ includeMetadata: argv.includeMetadata,
4055
+ format: exportFormat,
4056
+ });
4057
+ if (!exportData) {
4058
+ if (spinner) {
4059
+ spinner.warn(chalk.yellow(`⚠️ Session ${argv.sessionId} not found`));
4060
+ }
4061
+ return;
4062
+ }
4063
+ if (spinner) {
4064
+ spinner.succeed(chalk.green(`✅ Session exported (${exportData.messages.length} messages)`));
4065
+ }
4066
+ // Emit in the requested format (suitable for piping to a file).
4067
+ if (exportFormat === "csv") {
4068
+ logger.always(CLICommandFactory.sessionExportsToCsv([exportData]));
4069
+ }
4070
+ else {
4071
+ CLICommandFactory.handleOutput(exportData, {
4072
+ ...options,
4073
+ format: "json",
4074
+ });
4075
+ }
4076
+ }
4077
+ catch (error) {
4078
+ if (spinner) {
4079
+ spinner.fail("Session export failed");
4080
+ }
4081
+ if (error.message.includes("not enabled")) {
4082
+ logger.always(chalk.yellow("⚠️ Conversation memory is not enabled"));
4083
+ logger.always("Enable it by using --enable-conversation-memory with loop mode");
4084
+ }
4085
+ else {
4086
+ handleError(error, "Memory export");
4087
+ }
4088
+ }
4089
+ }
4090
+ /**
4091
+ * Execute memory export-all command
4092
+ */
4093
+ static async executeMemoryExportAll(argv) {
4094
+ const options = CLICommandFactory.processOptions(argv);
4095
+ const exportFormat = CLICommandFactory.resolveMemoryExportFormat(argv);
4096
+ const spinner = options.quiet
4097
+ ? null
4098
+ : ora("📤 Exporting all sessions...").start();
4099
+ try {
4100
+ const sdk = globalSession.getOrCreateNeuroLink();
4101
+ // Handle dry-run mode
4102
+ if (options.dryRun) {
4103
+ if (spinner) {
4104
+ spinner.succeed(chalk.green(`✅ Sessions would be exported to ${argv.output} (dry-run)`));
4105
+ }
4106
+ const result = {
4107
+ success: true,
4108
+ outputDirectory: argv.output,
4109
+ sessionCount: 2,
4110
+ message: "Dry-run: sessions would be exported",
4111
+ };
4112
+ CLICommandFactory.handleOutput(result, options);
4113
+ return;
4114
+ }
4115
+ // Ensure output directory exists
4116
+ if (!fs.existsSync(argv.output)) {
4117
+ fs.mkdirSync(argv.output, { recursive: true });
4118
+ }
4119
+ const exports = await sdk.exportAllSessions(argv.userId, {
4120
+ includeMetadata: argv.includeMetadata,
4121
+ format: exportFormat,
4122
+ });
4123
+ if (exports.length === 0) {
4124
+ if (spinner) {
4125
+ spinner.warn(chalk.yellow("⚠️ No sessions found to export"));
4126
+ }
4127
+ return;
4128
+ }
4129
+ // Write each session to a file. The session ID is untrusted input, so it
4130
+ // is sanitized to a bare filename and the resolved path is verified to
4131
+ // stay inside the output directory before writing (path-traversal guard).
4132
+ const resolvedOutput = path.resolve(argv.output);
4133
+ let writtenCount = 0;
4134
+ for (const sessionExport of exports) {
4135
+ const filename = CLICommandFactory.sanitizeSessionExportFilename(sessionExport.sessionId, exportFormat);
4136
+ const filepath = path.resolve(resolvedOutput, filename);
4137
+ // Defense-in-depth: refuse to write anything that escaped the dir.
4138
+ if (path.dirname(filepath) !== resolvedOutput) {
4139
+ logger.always(chalk.yellow(`⚠️ Skipping session with unsafe id: ${sessionExport.sessionId}`));
4140
+ continue;
4141
+ }
4142
+ const contents = exportFormat === "csv"
4143
+ ? CLICommandFactory.sessionExportsToCsv([sessionExport])
4144
+ : JSON.stringify(sessionExport, null, 2);
4145
+ fs.writeFileSync(filepath, contents);
4146
+ writtenCount++;
4147
+ }
4148
+ if (spinner) {
4149
+ spinner.succeed(chalk.green(`✅ Exported ${writtenCount} session(s) to ${argv.output}`));
4150
+ }
4151
+ if (options.format === "json") {
4152
+ const result = {
4153
+ success: true,
4154
+ outputDirectory: argv.output,
4155
+ format: exportFormat,
4156
+ sessionCount: writtenCount,
4157
+ sessions: exports.map((e) => ({
4158
+ sessionId: e.sessionId,
4159
+ messageCount: e.messages.length,
4160
+ })),
4161
+ };
4162
+ CLICommandFactory.handleOutput(result, options);
4163
+ }
4164
+ else {
4165
+ logger.always(chalk.blue(`📁 Exported sessions to: ${argv.output}`));
4166
+ for (const sessionExport of exports) {
4167
+ logger.always(` • ${sessionExport.sessionId} (${sessionExport.messages.length} messages)`);
4168
+ }
4169
+ }
4170
+ }
4171
+ catch (error) {
4172
+ if (spinner) {
4173
+ spinner.fail("Export all failed");
4174
+ }
4175
+ if (error.message.includes("not enabled")) {
4176
+ logger.always(chalk.yellow("⚠️ Conversation memory is not enabled"));
4177
+ logger.always("Enable it by using --enable-conversation-memory with loop mode");
4178
+ }
4179
+ else {
4180
+ handleError(error, "Memory export-all");
4181
+ }
4182
+ }
4183
+ }
4184
+ /**
4185
+ * Execute memory delete command
4186
+ */
4187
+ static async executeMemoryDelete(argv) {
4188
+ const options = CLICommandFactory.processOptions(argv);
4189
+ // Require confirmation unless --force is explicitly provided
4190
+ if (!argv.force) {
4191
+ // In quiet mode, we can't prompt interactively, so require --force
4192
+ if (options.quiet) {
4193
+ logger.always(chalk.red("⚠️ Deleting a session in quiet mode requires --force flag"));
4194
+ logger.always(chalk.yellow(`Usage: neurolink memory delete ${argv.sessionId} --force`));
4195
+ return;
4196
+ }
4197
+ const readline = await import("readline");
4198
+ const rl = readline.createInterface({
4199
+ input: process.stdin,
4200
+ output: process.stdout,
4201
+ });
4202
+ const answer = await new Promise((resolve) => {
4203
+ rl.question(chalk.yellow(`⚠️ Are you sure you want to delete session "${argv.sessionId}"? (y/N): `), resolve);
4204
+ });
4205
+ rl.close();
4206
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
4207
+ logger.always(chalk.gray("Deletion cancelled."));
4208
+ return;
4209
+ }
4210
+ }
4211
+ const spinner = options.quiet
4212
+ ? null
4213
+ : ora(`🗑️ Deleting session ${argv.sessionId}...`).start();
4214
+ try {
4215
+ const sdk = globalSession.getOrCreateNeuroLink();
4216
+ // Handle dry-run mode
4217
+ if (options.dryRun) {
4218
+ if (spinner) {
4219
+ spinner.succeed(chalk.green(`✅ Session ${argv.sessionId} deleted (dry-run)`));
4220
+ }
4221
+ const result = {
4222
+ success: true,
4223
+ action: "delete",
4224
+ sessionId: argv.sessionId,
4225
+ message: "Session would be deleted",
4226
+ };
4227
+ CLICommandFactory.handleOutput(result, options);
4228
+ return;
4229
+ }
4230
+ const success = await sdk.clearConversationSession(argv.sessionId);
4231
+ if (spinner) {
4232
+ if (success) {
4233
+ spinner.succeed(chalk.green(`✅ Session ${argv.sessionId} deleted successfully`));
4234
+ }
4235
+ else {
4236
+ spinner.warn(chalk.yellow(`⚠️ Session ${argv.sessionId} not found or already deleted`));
4237
+ }
4238
+ }
4239
+ if (options.format === "json") {
4240
+ const result = {
4241
+ success,
4242
+ action: "delete",
4243
+ sessionId: argv.sessionId,
4244
+ };
4245
+ CLICommandFactory.handleOutput(result, options);
4246
+ }
4247
+ }
4248
+ catch (error) {
4249
+ if (spinner) {
4250
+ spinner.fail("Session deletion failed");
4251
+ }
4252
+ if (error.message.includes("not enabled")) {
4253
+ logger.always(chalk.yellow("⚠️ Conversation memory is not enabled"));
4254
+ logger.always("Enable it by using --enable-conversation-memory with loop mode");
4255
+ }
4256
+ else {
4257
+ handleError(error, "Memory delete");
4258
+ }
4259
+ }
4260
+ }
3780
4261
  /**
3781
4262
  * Execute completion command
3782
4263
  */
@@ -2,7 +2,7 @@
2
2
  * Conversation Memory Manager for NeuroLink
3
3
  * Handles in-memory conversation storage, session management, and context injection
4
4
  */
5
- import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, SessionMemory, StoreConversationTurnOptions, IConversationMemoryManager } from "../types/index.js";
5
+ import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, SessionListItem, SessionMemory, StoreConversationTurnOptions, IConversationMemoryManager } from "../types/index.js";
6
6
  export declare class ConversationMemoryManager implements IConversationMemoryManager {
7
7
  private sessions;
8
8
  config: ConversationMemoryConfig;
@@ -56,6 +56,16 @@ export declare class ConversationMemoryManager implements IConversationMemoryMan
56
56
  private createNewSession;
57
57
  private enforceSessionLimit;
58
58
  getStats(): Promise<ConversationMemoryStats>;
59
+ /**
60
+ * List all sessions with metadata
61
+ * @param userId - Optional user ID to filter sessions
62
+ * @returns Array of session list items with metadata
63
+ */
64
+ listSessions(userId?: string): Promise<SessionListItem[]>;
65
+ /**
66
+ * Format milliseconds into human-readable time ago string
67
+ */
68
+ private formatTimeAgo;
59
69
  clearSession(sessionId: string): Promise<boolean>;
60
70
  clearAllSessions(): Promise<void>;
61
71
  /**
@@ -292,6 +292,50 @@ export class ConversationMemoryManager {
292
292
  totalTurns,
293
293
  };
294
294
  }
295
+ /**
296
+ * List all sessions with metadata
297
+ * @param userId - Optional user ID to filter sessions
298
+ * @returns Array of session list items with metadata
299
+ */
300
+ async listSessions(userId) {
301
+ await this.ensureInitialized();
302
+ const sessions = Array.from(this.sessions.values());
303
+ const now = Date.now();
304
+ return sessions
305
+ .filter((session) => !userId || session.userId === userId)
306
+ .map((session) => {
307
+ const lastActive = this.formatTimeAgo(now - session.lastActivity);
308
+ return {
309
+ id: session.sessionId,
310
+ title: session.sessionId, // In-memory doesn't store title, use sessionId
311
+ createdAt: new Date(session.createdAt).toISOString(),
312
+ updatedAt: new Date(session.lastActivity).toISOString(),
313
+ userId: session.userId,
314
+ messageCount: session.messages.length,
315
+ lastActive,
316
+ };
317
+ })
318
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
319
+ }
320
+ /**
321
+ * Format milliseconds into human-readable time ago string
322
+ */
323
+ formatTimeAgo(ms) {
324
+ const seconds = Math.floor(ms / 1000);
325
+ const minutes = Math.floor(seconds / 60);
326
+ const hours = Math.floor(minutes / 60);
327
+ const days = Math.floor(hours / 24);
328
+ if (days > 0) {
329
+ return `${days} day${days > 1 ? "s" : ""} ago`;
330
+ }
331
+ if (hours > 0) {
332
+ return `${hours} hour${hours > 1 ? "s" : ""} ago`;
333
+ }
334
+ if (minutes > 0) {
335
+ return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
336
+ }
337
+ return "just now";
338
+ }
295
339
  async clearSession(sessionId) {
296
340
  return withSpan({
297
341
  name: "neurolink.memory.clear",
@@ -2,7 +2,7 @@
2
2
  * Redis Conversation Memory Manager for NeuroLink
3
3
  * Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
4
4
  */
5
- import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata, IConversationMemoryManager } from "../types/index.js";
5
+ import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionListItem, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata, IConversationMemoryManager } from "../types/index.js";
6
6
  /**
7
7
  * Redis-based implementation of the ConversationMemoryManager
8
8
  * Uses the same interface but stores data in Redis
@@ -175,6 +175,17 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
175
175
  * @returns Array of session metadata objects
176
176
  */
177
177
  getUserAllSessionsHistory(userId: string): Promise<SessionMetadata[]>;
178
+ /**
179
+ * List all sessions with metadata for CLI/API usage
180
+ * Implements IConversationMemoryManager.listSessions
181
+ * @param userId - Optional user identifier to filter sessions
182
+ * @returns Array of session list items with metadata
183
+ */
184
+ listSessions(userId?: string): Promise<SessionListItem[]>;
185
+ /**
186
+ * Format milliseconds into human-readable time ago string
187
+ */
188
+ private formatTimeAgo;
178
189
  /**
179
190
  * Clean up stale pending tool execution data
180
191
  * Removes data older than 5 minutes to prevent memory leaks