@juspay/neurolink 9.87.4 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/browser/neurolink.min.js +208 -208
- package/dist/cli/factories/commandFactory.d.ts +39 -0
- package/dist/cli/factories/commandFactory.js +484 -3
- package/dist/core/conversationMemoryManager.d.ts +11 -1
- package/dist/core/conversationMemoryManager.js +44 -0
- package/dist/core/redisConversationMemoryManager.d.ts +12 -1
- package/dist/core/redisConversationMemoryManager.js +80 -0
- package/dist/lib/core/conversationMemoryManager.d.ts +11 -1
- package/dist/lib/core/conversationMemoryManager.js +44 -0
- package/dist/lib/core/redisConversationMemoryManager.d.ts +12 -1
- package/dist/lib/core/redisConversationMemoryManager.js +80 -0
- package/dist/lib/neurolink.d.ts +27 -0
- package/dist/lib/neurolink.js +133 -0
- package/dist/lib/types/conversation.d.ts +36 -0
- package/dist/lib/types/conversationMemoryInterface.d.ts +5 -1
- package/dist/lib/utils/fileDetector.js +34 -10
- package/dist/neurolink.d.ts +27 -0
- package/dist/neurolink.js +133 -0
- package/dist/types/conversation.d.ts +36 -0
- package/dist/types/conversationMemoryInterface.d.ts +5 -1
- package/dist/utils/fileDetector.js +34 -10
- 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
|
-
.
|
|
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
|