sessionmem 1.0.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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +344 -0
  3. package/dist/adapters/capabilities/fallbackTools.js +36 -0
  4. package/dist/adapters/contract/hostAdapterContract.js +1 -0
  5. package/dist/adapters/factory.js +40 -0
  6. package/dist/adapters/generic.js +128 -0
  7. package/dist/adapters/global/antigravity.js +22 -0
  8. package/dist/adapters/global/claudeCode.js +22 -0
  9. package/dist/adapters/global/codex.js +22 -0
  10. package/dist/adapters/global/qcoder.js +22 -0
  11. package/dist/adapters/ide/cline.js +20 -0
  12. package/dist/adapters/ide/cursor.js +28 -0
  13. package/dist/adapters/ide/installer.js +57 -0
  14. package/dist/adapters/ide/windsurf.js +28 -0
  15. package/dist/adapters/tools/ping.js +15 -0
  16. package/dist/cli/commands/config.js +79 -0
  17. package/dist/cli/commands/export.js +28 -0
  18. package/dist/cli/commands/forget.js +28 -0
  19. package/dist/cli/commands/import.js +112 -0
  20. package/dist/cli/commands/install.js +57 -0
  21. package/dist/cli/commands/list.js +13 -0
  22. package/dist/cli/commands/ping.js +12 -0
  23. package/dist/cli/commands/redactScan.js +40 -0
  24. package/dist/cli/commands/retention.js +54 -0
  25. package/dist/cli/commands/run.js +26 -0
  26. package/dist/cli/commands/search.js +29 -0
  27. package/dist/cli/commands/show.js +15 -0
  28. package/dist/cli/commands/stats.js +46 -0
  29. package/dist/cli/commands/sync.js +118 -0
  30. package/dist/cli/commands/team.js +96 -0
  31. package/dist/cli/commands/uninstall.js +30 -0
  32. package/dist/cli/context.js +69 -0
  33. package/dist/cli/index.js +147 -0
  34. package/dist/cli/output.js +37 -0
  35. package/dist/core/api/contracts.js +263 -0
  36. package/dist/core/api/errors.js +29 -0
  37. package/dist/core/api/localOnlyPolicy.js +29 -0
  38. package/dist/core/api/memoryCoreService.js +595 -0
  39. package/dist/core/api/sessionLifecycleService.js +289 -0
  40. package/dist/core/config/policyConfig.js +131 -0
  41. package/dist/core/embed/deterministicEmbed.js +31 -0
  42. package/dist/core/embed/embeddingVersion.js +1 -0
  43. package/dist/core/embed/reembedPolicy.js +9 -0
  44. package/dist/core/embed/textNormalize.js +12 -0
  45. package/dist/core/injection/formatStartupInjection.js +97 -0
  46. package/dist/core/injection/tokenBudget.js +38 -0
  47. package/dist/core/retrieve/decay.js +15 -0
  48. package/dist/core/retrieve/importance.js +6 -0
  49. package/dist/core/retrieve/recencyBands.js +18 -0
  50. package/dist/core/retrieve/retrieveMemories.js +83 -0
  51. package/dist/core/retrieve/score.js +25 -0
  52. package/dist/core/schema/migrations/001_initial.sql +25 -0
  53. package/dist/core/schema/migrations/002_indexes.sql +18 -0
  54. package/dist/core/schema/migrations/003_summarization_failures.sql +14 -0
  55. package/dist/core/schema/migrations/004_memory_feedback.sql +12 -0
  56. package/dist/core/schema/migrations/005_team_provenance.sql +9 -0
  57. package/dist/core/schema/runMigrations.js +38 -0
  58. package/dist/core/session.js +4 -0
  59. package/dist/core/storage/db.js +8 -0
  60. package/dist/core/storage/memoryFeedbackRepo.js +16 -0
  61. package/dist/core/storage/memoryRepo.js +179 -0
  62. package/dist/core/storage/memorySearchRepo.js +30 -0
  63. package/dist/core/storage/sessionEventsRepo.js +20 -0
  64. package/dist/core/storage/summarizationFailuresRepo.js +39 -0
  65. package/dist/core/storage/types.js +1 -0
  66. package/dist/core/summarize/cloudSummarizer.js +19 -0
  67. package/dist/core/summarize/localSummarizer.js +31 -0
  68. package/dist/core/summarize/redaction.js +48 -0
  69. package/dist/core/summarize/strategySelector.js +7 -0
  70. package/dist/core/summarize/summaryShape.js +49 -0
  71. package/package.json +48 -0
@@ -0,0 +1,20 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { GenericMCPAdapter } from "../generic.js";
4
+ import { IDEInstaller } from "./installer.js";
5
+ export class ClineAdapter extends GenericMCPAdapter {
6
+ name = "Cline";
7
+ capabilities = {
8
+ supportsPrompts: true,
9
+ supportsResources: true,
10
+ supportsTools: true,
11
+ };
12
+ async install() {
13
+ const configPath = join(homedir(), ".cline", "config.json");
14
+ return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", ["run"]);
15
+ }
16
+ async uninstall() {
17
+ const configPath = join(homedir(), ".cline", "config.json");
18
+ return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { GenericMCPAdapter } from "../generic.js";
4
+ import { IDEInstaller } from "./installer.js";
5
+ export class CursorAdapter extends GenericMCPAdapter {
6
+ name = "Cursor";
7
+ capabilities = {
8
+ supportsPrompts: false,
9
+ supportsResources: false,
10
+ supportsTools: true,
11
+ };
12
+ get configPath() {
13
+ const home = homedir();
14
+ if (process.platform === "win32") {
15
+ return join(process.env.APPDATA ?? home, "Cursor", "User", "settings.json");
16
+ }
17
+ if (process.platform === "darwin") {
18
+ return join(home, "Library", "Application Support", "Cursor", "User", "settings.json");
19
+ }
20
+ return join(home, ".config", "Cursor", "User", "settings.json");
21
+ }
22
+ async install() {
23
+ return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"]);
24
+ }
25
+ async uninstall() {
26
+ return IDEInstaller.removeMcpConfig(this.configPath, "sessionmem");
27
+ }
28
+ }
@@ -0,0 +1,57 @@
1
+ export class IDEInstaller {
2
+ static parseJsonc(content) {
3
+ const stripped = content
4
+ .replace(/\/\/[^\n]*/g, "")
5
+ .replace(/,(\s*[}\]])/g, "$1");
6
+ return JSON.parse(stripped);
7
+ }
8
+ static injectMcpBlock(content, serverName, command, args) {
9
+ const config = content.trim()
10
+ ? this.parseJsonc(content)
11
+ : {};
12
+ if (!config.mcpServers)
13
+ config.mcpServers = {};
14
+ config.mcpServers[serverName] = {
15
+ command,
16
+ args,
17
+ };
18
+ return JSON.stringify(config, null, 2);
19
+ }
20
+ static removeMcpBlock(content, serverName) {
21
+ const config = content.trim()
22
+ ? this.parseJsonc(content)
23
+ : {};
24
+ if (config.mcpServers) {
25
+ delete config.mcpServers[serverName];
26
+ }
27
+ return JSON.stringify(config, null, 2);
28
+ }
29
+ static async injectMcpConfig(filePath, serverName, command, args) {
30
+ try {
31
+ const { readFileSync, writeFileSync, existsSync } = await import("fs");
32
+ const existing = existsSync(filePath)
33
+ ? readFileSync(filePath, "utf-8")
34
+ : "{}";
35
+ const updated = this.injectMcpBlock(existing, serverName, command, args);
36
+ writeFileSync(filePath, updated, "utf-8");
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ static async removeMcpConfig(filePath, serverName) {
44
+ try {
45
+ const { readFileSync, writeFileSync, existsSync } = await import("fs");
46
+ if (!existsSync(filePath))
47
+ return true;
48
+ const existing = readFileSync(filePath, "utf-8");
49
+ const updated = this.removeMcpBlock(existing, serverName);
50
+ writeFileSync(filePath, updated, "utf-8");
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,28 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { GenericMCPAdapter } from "../generic.js";
4
+ import { IDEInstaller } from "./installer.js";
5
+ export class WindsurfAdapter extends GenericMCPAdapter {
6
+ name = "Windsurf";
7
+ capabilities = {
8
+ supportsPrompts: true,
9
+ supportsResources: true,
10
+ supportsTools: true,
11
+ };
12
+ get configPath() {
13
+ const home = homedir();
14
+ if (process.platform === "win32") {
15
+ return join(process.env.APPDATA ?? home, "Windsurf", "User", "settings.json");
16
+ }
17
+ if (process.platform === "darwin") {
18
+ return join(home, "Library", "Application Support", "Windsurf", "User", "settings.json");
19
+ }
20
+ return join(home, ".config", "Windsurf", "User", "settings.json");
21
+ }
22
+ async install() {
23
+ return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"]);
24
+ }
25
+ async uninstall() {
26
+ return IDEInstaller.removeMcpConfig(this.configPath, "sessionmem");
27
+ }
28
+ }
@@ -0,0 +1,15 @@
1
+ export const pingTool = {
2
+ name: "sessionmem_ping",
3
+ description: "Ping the sessionmem MCP server to verify it is running correctly.",
4
+ schema: {
5
+ type: "object",
6
+ properties: {},
7
+ },
8
+ execute: async () => {
9
+ return {
10
+ status: "ok",
11
+ version: "0.1.0",
12
+ message: "sessionmem MCP server is operational.",
13
+ };
14
+ }
15
+ };
@@ -0,0 +1,79 @@
1
+ import { configFilePath, readPolicyConfig, writePolicyConfig, } from "../../core/config/policyConfig.js";
2
+ function coerceInt(raw) {
3
+ // Reject anything that is not a clean integer so we never persist NaN or
4
+ // partial garbage (e.g. "30abc" -> 30 from parseInt is rejected here).
5
+ if (!/^-?\d+$/.test(raw.trim())) {
6
+ throw new Error(`expected an integer, got "${raw}"`);
7
+ }
8
+ return Number.parseInt(raw.trim(), 10);
9
+ }
10
+ // 100 years (in days). Far beyond any realistic retention window, and keeps
11
+ // `Date.now() - retentionDays * 24 * 60 * 60 * 1000` well within the safe
12
+ // Date range so `pruneMemories`'s cutoff computation never throws RangeError.
13
+ // Exported so `retention prune --days` enforces the same
14
+ // bound as `config set retentionDays`.
15
+ export const MAX_RETENTION_DAYS = 36500;
16
+ function coerceRetentionDays(raw) {
17
+ const n = coerceInt(raw);
18
+ if (n > MAX_RETENTION_DAYS) {
19
+ throw new Error(`retentionDays must be <= ${MAX_RETENTION_DAYS}, got "${raw}"`);
20
+ }
21
+ return n;
22
+ }
23
+ function coerceBool(raw) {
24
+ const v = raw.trim().toLowerCase();
25
+ if (v === "true")
26
+ return true;
27
+ if (v === "false")
28
+ return false;
29
+ throw new Error(`expected "true" or "false", got "${raw}"`);
30
+ }
31
+ // Accept both the dotted operator key (retention.days) and the raw policyConfig
32
+ // field name (retentionDays) so CLI and policyConfig stay consistent (plan note).
33
+ const CONFIG_KEYS = {
34
+ "retention.days": { field: "retentionDays", coerce: coerceRetentionDays },
35
+ retentionDays: { field: "retentionDays", coerce: coerceRetentionDays },
36
+ redactionEnabled: { field: "redactionEnabled", coerce: coerceBool },
37
+ };
38
+ function resolvePath(options) {
39
+ return options?.configPath ?? configFilePath();
40
+ }
41
+ /**
42
+ * `sessionmem config get <key>` — print the effective value for a known key.
43
+ * Unknown key -> error + exit 1.
44
+ */
45
+ export function configGetCommand(key, options) {
46
+ const def = CONFIG_KEYS[key];
47
+ if (!def) {
48
+ console.error(`Unknown config key "${key}". Known keys: ${Object.keys(CONFIG_KEYS).join(", ")}`);
49
+ process.exit(1);
50
+ return;
51
+ }
52
+ const config = readPolicyConfig(resolvePath(options));
53
+ console.log(String(config[def.field]));
54
+ }
55
+ /**
56
+ * `sessionmem config set <key> <value>` — coerce and persist to config.json.
57
+ * Unknown key or invalid value -> error + exit 1 with NO file write.
58
+ */
59
+ export function configSetCommand(key, value, options) {
60
+ const def = CONFIG_KEYS[key];
61
+ if (!def) {
62
+ console.error(`Unknown config key "${key}". Known keys: ${Object.keys(CONFIG_KEYS).join(", ")}`);
63
+ process.exit(1);
64
+ return;
65
+ }
66
+ let coerced;
67
+ try {
68
+ coerced = def.coerce(value);
69
+ }
70
+ catch (err) {
71
+ console.error(`Invalid value for "${key}": ${err instanceof Error ? err.message : String(err)}`);
72
+ process.exit(1);
73
+ return;
74
+ }
75
+ writePolicyConfig(resolvePath(options), {
76
+ [def.field]: coerced,
77
+ });
78
+ console.log(`Set ${key} = ${String(coerced)}`);
79
+ }
@@ -0,0 +1,28 @@
1
+ import { homedir } from "os";
2
+ import { join, dirname } from "path";
3
+ import { resolve } from "path";
4
+ import { mkdirSync, writeFileSync } from "fs";
5
+ import { createCliContext } from "../context.js";
6
+ export async function exportCommand(pathArg, ctx) {
7
+ const context = ctx ?? createCliContext();
8
+ const res = await context.service.call("exportMemories", {
9
+ projectId: context.projectId,
10
+ });
11
+ if (!res.ok) {
12
+ console.error(res.error.message);
13
+ process.exit(1);
14
+ }
15
+ // Default to an ISO-dated path; resolve user-supplied path otherwise.
16
+ // Path comes from the local CLI invoker's own argv (same trust level as
17
+ // the process itself), not from a remote/network-facing input, so
18
+ // resolving it to an absolute path is not a path-traversal vector.
19
+ const outPath = pathArg
20
+ ? resolve(pathArg) // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
21
+ : join(homedir(), ".sessionmem", `export-${new Date().toISOString().slice(0, 10)}.json`);
22
+ // Ensure the target directory exists (the default ~/.sessionmem dir may not
23
+ // have been created yet in this context, e.g. a test-supplied CliContext).
24
+ mkdirSync(dirname(outPath), { recursive: true });
25
+ // Write as a pretty-printed JSON array
26
+ writeFileSync(outPath, JSON.stringify(res.memories, null, 2), "utf8");
27
+ console.log(`Exported ${res.memories.length} memories to ${outPath}`);
28
+ }
@@ -0,0 +1,28 @@
1
+ import { createCliContext } from "../context.js";
2
+ export async function forgetCommand(id, options, ctx) {
3
+ const context = ctx ?? createCliContext();
4
+ const getResult = await context.service.call("getMemory", {
5
+ projectId: context.projectId,
6
+ memoryId: id,
7
+ });
8
+ if (!getResult.ok) {
9
+ console.error(getResult.error.message);
10
+ process.exit(1);
11
+ }
12
+ if (!options.force) {
13
+ // Dry-run: preview and exit 0 without deleting
14
+ const preview = getResult.memory.content.replace(/\s+/g, " ").slice(0, 60);
15
+ console.log(`Would delete: ${preview}. Pass --force to confirm.`);
16
+ return;
17
+ }
18
+ // --force path: actually delete
19
+ const deleteResult = await context.service.call("forgetMemory", {
20
+ projectId: context.projectId,
21
+ memoryId: id,
22
+ });
23
+ if (!deleteResult.ok) {
24
+ console.error(deleteResult.error.message);
25
+ process.exit(1);
26
+ }
27
+ console.log(`Deleted ${id}.`);
28
+ }
@@ -0,0 +1,112 @@
1
+ import { resolve } from "path";
2
+ import { readFileSync } from "fs";
3
+ import { listAllMemoryIds } from "../../core/storage/memoryRepo.js";
4
+ import { createCliContext } from "../context.js";
5
+ import { importMemoryRecordSchema } from "../../core/api/contracts.js";
6
+ export async function importCommand(pathArg, options, ctx) {
7
+ const context = ctx ?? createCliContext();
8
+ // V12: resolve user-supplied path.
9
+ // Path comes from the local CLI invoker's own argv (same trust level as
10
+ // the process itself), not from a remote/network-facing input, so
11
+ // resolving it to an absolute path is not a path-traversal vector.
12
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
13
+ const inPath = resolve(pathArg);
14
+ // Read and parse the JSON file
15
+ let parsed;
16
+ try {
17
+ const raw = readFileSync(inPath, "utf8");
18
+ parsed = JSON.parse(raw);
19
+ }
20
+ catch (err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ console.error(`Failed to read or parse import file: ${message}`);
23
+ process.exit(1);
24
+ }
25
+ // Must be an array
26
+ if (!Array.isArray(parsed)) {
27
+ console.error("Import file must be a JSON array of memory records.");
28
+ process.exit(1);
29
+ }
30
+ const records = parsed;
31
+ let toImport = records;
32
+ let skippedCount = 0;
33
+ if (!options.merge) {
34
+ // By default, skip existing IDs (pre-filter). `id` is a
35
+ // globally-unique PRIMARY KEY (not scoped by project), so the duplicate
36
+ // check must consider every project's ids -- otherwise a record whose id
37
+ // collides with another project's memory would not be pre-filtered here
38
+ // and could fall through to the service's ON CONFLICT(id) upsert.
39
+ const existingIds = listAllMemoryIds(context.db);
40
+ const filtered = records.filter((r) => {
41
+ const id = typeof r.id === "string" ? r.id : undefined;
42
+ return id === undefined || !existingIds.has(id);
43
+ });
44
+ skippedCount = records.length - filtered.length;
45
+ toImport = filtered;
46
+ }
47
+ if (toImport.length === 0 && skippedCount > 0) {
48
+ console.log(`Imported 0, skipped ${skippedCount} duplicates.`);
49
+ return;
50
+ }
51
+ // Map to the expected shape, then validate each record before sending to the service
52
+ const mapped = toImport.map((r) => ({
53
+ id: r.id,
54
+ projectId: r.projectId ?? context.projectId,
55
+ sessionId: r.sessionId,
56
+ sourceAdapter: r.sourceAdapter,
57
+ kind: r.kind,
58
+ content: r.content,
59
+ importance: r.importance,
60
+ createdAt: r.createdAt,
61
+ updatedAt: r.updatedAt,
62
+ }));
63
+ // Validate each record but skip-and-warn on individual invalid
64
+ // records (consistent with the duplicate-skip UX) instead of aborting the
65
+ // entire import on the first invalid record, which would discard earlier
66
+ // valid records with no partial import.
67
+ const validMemories = [];
68
+ let invalidCount = 0;
69
+ for (let i = 0; i < mapped.length; i++) {
70
+ const check = importMemoryRecordSchema.safeParse(mapped[i]);
71
+ if (!check.success) {
72
+ console.error(`Record at index ${i} is invalid, skipping: ${check.error.message}`);
73
+ invalidCount += 1;
74
+ continue;
75
+ }
76
+ validMemories.push(mapped[i]);
77
+ }
78
+ if (validMemories.length === 0) {
79
+ if (invalidCount > 0) {
80
+ console.log(`Imported 0, skipped ${invalidCount} invalid record(s).`);
81
+ }
82
+ else if (skippedCount > 0) {
83
+ console.log(`Imported 0, skipped ${skippedCount} duplicates.`);
84
+ }
85
+ return;
86
+ }
87
+ const result = await context.service.call("importMemories", {
88
+ projectId: context.projectId,
89
+ // No explicit redactionEnabled here: the service resolves the effective
90
+ // value from ~/.sessionmem/config.json (override > config > default),
91
+ // so `sessionmem config set redactionEnabled false` governs the
92
+ // import write path too.
93
+ memories: validMemories,
94
+ });
95
+ if (!result.ok) {
96
+ console.error(result.error.message);
97
+ process.exit(1);
98
+ }
99
+ const importedCount = result.imported;
100
+ let suffix = result.skippedCrossProject > 0
101
+ ? ` (${result.skippedCrossProject} skipped: id belongs to another project)`
102
+ : "";
103
+ if (invalidCount > 0) {
104
+ suffix += ` (${invalidCount} invalid record(s) skipped)`;
105
+ }
106
+ if (options.merge) {
107
+ console.log(`Imported (merged) ${importedCount} memories.${suffix}`);
108
+ }
109
+ else {
110
+ console.log(`Imported ${importedCount}, skipped ${skippedCount} duplicates.${suffix}`);
111
+ }
112
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync } from "fs";
2
+ import { AdapterFactory } from "../../adapters/factory.js";
3
+ import { createCliContext } from "../context.js";
4
+ import { configFilePath, writePolicyConfig, DEFAULT_POLICY_CONFIG, } from "../../core/config/policyConfig.js";
5
+ export const MANUAL_CONFIG_BLOCK = JSON.stringify({
6
+ mcpServers: {
7
+ sessionmem: {
8
+ command: "sessionmem",
9
+ args: ["run"],
10
+ },
11
+ },
12
+ }, null, 2);
13
+ export function printManualFallback(adapterName) {
14
+ console.error(`Auto-config for ${adapterName} failed. Add this block to your MCP config manually:`);
15
+ console.log(MANUAL_CONFIG_BLOCK);
16
+ }
17
+ export async function installCommand(_options, contextOverrides) {
18
+ // Step 1: DB init — run migrations and confirm DB init
19
+ let dbPath;
20
+ try {
21
+ const ctx = createCliContext(contextOverrides);
22
+ dbPath = ctx.dbPath;
23
+ }
24
+ catch (err) {
25
+ console.error(`✗ DB init failed (~/.sessionmem/memories.db): ${err instanceof Error ? err.message : String(err)}`);
26
+ console.error("Hint: ensure ~/.sessionmem directory is writable and run `sessionmem install` again.");
27
+ process.exit(1);
28
+ }
29
+ console.log(`✓ DB initialized (${dbPath})`);
30
+ // Step 1b: Config defaults — write config.json with defaults only when absent.
31
+ // An existing config is preserved byte-for-byte so user settings are
32
+ // never clobbered.
33
+ const configPath = contextOverrides?.configPath ?? configFilePath();
34
+ if (existsSync(configPath)) {
35
+ console.log(`✓ config.json preserved (${configPath})`);
36
+ }
37
+ else {
38
+ writePolicyConfig(configPath, { ...DEFAULT_POLICY_CONFIG });
39
+ console.log(`✓ config.json initialized (${configPath})`);
40
+ }
41
+ // Step 2: Adapter config — detect adapter and install
42
+ const adapter = AdapterFactory.detectAdapter();
43
+ if (!adapter.install) {
44
+ console.error(`✗ ${adapter.name} config update failed`);
45
+ printManualFallback(adapter.name);
46
+ process.exit(1);
47
+ }
48
+ const success = await adapter.install();
49
+ if (!success) {
50
+ console.error(`✗ ${adapter.name} config update failed`);
51
+ printManualFallback(adapter.name);
52
+ process.exit(1);
53
+ }
54
+ console.log(`✓ ${adapter.name} config updated`);
55
+ // Step 3: Full success checklist
56
+ console.log("✓ sessionmem ready");
57
+ }
@@ -0,0 +1,13 @@
1
+ import { createCliContext } from "../context.js";
2
+ import { formatTable } from "../output.js";
3
+ export async function listCommand(ctx) {
4
+ const context = ctx ?? createCliContext();
5
+ const result = await context.service.call("listMemories", {
6
+ projectId: context.projectId,
7
+ });
8
+ if (!result.ok) {
9
+ console.error(result.error.message);
10
+ process.exit(1);
11
+ }
12
+ console.log(formatTable(result.memories));
13
+ }
@@ -0,0 +1,12 @@
1
+ import { pingTool } from "../../adapters/tools/ping.js";
2
+ export async function pingCommand() {
3
+ const result = await pingTool.execute();
4
+ console.log(`status: ${result.status}`);
5
+ console.log(`version: ${result.version}`);
6
+ console.log(`message: ${result.message}`);
7
+ if (result.status !== "ok") {
8
+ console.error(`sessionmem ping failed: ${result.message}`);
9
+ process.exit(1);
10
+ }
11
+ // exit 0 on success (implicit)
12
+ }
@@ -0,0 +1,40 @@
1
+ import { createCliContext } from "../context.js";
2
+ /**
3
+ * `sessionmem redact-scan [--apply]`.
4
+ *
5
+ * Scan-by-default one-time scrub over pre-existing memories: with no
6
+ * flags it reports `Found N memories with potential secrets` plus truncated,
7
+ * already-redacted previews and writes nothing. `--apply` redacts matching rows
8
+ * in place and prints a summary count.
9
+ *
10
+ * The underlying `redactExisting` service builds previews from the REDACTED text
11
+ * (never the raw secret) and length-bounds them, so printing them as-is cannot
12
+ * leak a full secret. Scan always calls with apply:false so a missing
13
+ * --apply can never mutate data.
14
+ */
15
+ export async function redactScanCommand(options, ctx) {
16
+ const context = ctx ?? createCliContext();
17
+ const apply = !!options.apply;
18
+ const result = await context.service.call("redactExisting", {
19
+ projectId: context.projectId,
20
+ apply,
21
+ });
22
+ if (!result.ok) {
23
+ console.error(result.error.message);
24
+ process.exit(1);
25
+ }
26
+ if (apply) {
27
+ if (result.skipped > 0) {
28
+ console.log(`Redacted ${result.updated} memories; ${result.skipped} skipped (not found).`);
29
+ }
30
+ else {
31
+ console.log(`Redacted ${result.updated} memories.`);
32
+ }
33
+ return;
34
+ }
35
+ // Scan (non-destructive): report match count and print the safe previews.
36
+ console.log(`Found ${result.matched} memories with potential secrets`);
37
+ for (const preview of result.previews) {
38
+ console.log(` ${preview}`);
39
+ }
40
+ }
@@ -0,0 +1,54 @@
1
+ import { createCliContext } from "../context.js";
2
+ import { configFilePath, readPolicyConfig, resolvePolicySettings, } from "../../core/config/policyConfig.js";
3
+ import { MAX_RETENTION_DAYS } from "./config.js";
4
+ /**
5
+ * `sessionmem retention prune [--force] [--days <n>]`.
6
+ *
7
+ * Dry-run by default: prints the eligible count and exits 0 without
8
+ * deleting. `--force` hard-deletes eligible memories and prints a summary count.
9
+ *
10
+ * Effective retentionDays follows precedence CLI flag > config.json > default
11
+ * via {@link resolvePolicySettings}. The dry-run path always calls
12
+ * pruneMemories with dryRun:true so a missing --force can never delete.
13
+ */
14
+ export async function retentionPruneCommand(options, ctx) {
15
+ const context = ctx ?? createCliContext();
16
+ // --days override must be a clean integer, matching `config set
17
+ // retention.days`'s validation: Number.parseInt("30abc", 10) === 30
18
+ // would otherwise silently accept trailing garbage. Also enforce the same
19
+ // upper bound as `config set` so an out-of-range --days can't
20
+ // produce an Invalid Date / RangeError when computing the prune cutoff.
21
+ let override;
22
+ if (options.days !== undefined) {
23
+ const trimmed = options.days.trim();
24
+ if (!/^-?\d+$/.test(trimmed)) {
25
+ console.error(`Invalid --days value "${options.days}": expected an integer.`);
26
+ process.exit(1);
27
+ }
28
+ const parsed = Number.parseInt(trimmed, 10);
29
+ if (parsed > MAX_RETENTION_DAYS) {
30
+ console.error(`Invalid --days value "${options.days}": must be <= ${MAX_RETENTION_DAYS}.`);
31
+ process.exit(1);
32
+ }
33
+ override = { retentionDays: parsed };
34
+ }
35
+ const { retentionDays } = resolvePolicySettings({
36
+ override,
37
+ config: readPolicyConfig(configFilePath()),
38
+ });
39
+ const dryRun = !options.force;
40
+ const result = await context.service.call("pruneMemories", {
41
+ projectId: context.projectId,
42
+ retentionDays,
43
+ dryRun,
44
+ });
45
+ if (!result.ok) {
46
+ console.error(result.error.message);
47
+ process.exit(1);
48
+ }
49
+ if (dryRun) {
50
+ console.log(`Would delete ${result.eligible} memories older than ${retentionDays} days. Pass --force to confirm.`);
51
+ return;
52
+ }
53
+ console.log(`Deleted ${result.deleted} memories.`);
54
+ }
@@ -0,0 +1,26 @@
1
+ import { AdapterFactory } from "../../adapters/factory.js";
2
+ import { mkdirSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ export async function runMcpServer() {
6
+ const adapter = AdapterFactory.detectAdapter();
7
+ // Setup rudimentary log for debugging manual configs
8
+ const logDir = join(homedir(), ".sessionmem", "logs");
9
+ const logPath = join(logDir, "mcp.log");
10
+ const logMessage = `[${new Date().toISOString()}] Started sessionmem via ${adapter.name}\n`;
11
+ try {
12
+ mkdirSync(logDir, { recursive: true });
13
+ writeFileSync(logPath, logMessage, { flag: "a" });
14
+ }
15
+ catch {
16
+ // best-effort logging; ignore failures
17
+ }
18
+ // Start the server
19
+ if (adapter.startMcpServer) {
20
+ await adapter.startMcpServer();
21
+ }
22
+ else {
23
+ console.error(`Adapter ${adapter.name} does not implement startMcpServer.`);
24
+ process.exit(1);
25
+ }
26
+ }
@@ -0,0 +1,29 @@
1
+ import { createCliContext } from "../context.js";
2
+ import { formatTable } from "../output.js";
3
+ const DEFAULT_LIMIT = 20;
4
+ function coerceLimit(value) {
5
+ if (value === undefined)
6
+ return DEFAULT_LIMIT;
7
+ const parsed = Number.parseInt(String(value), 10);
8
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_LIMIT;
9
+ }
10
+ export async function searchCommand(query, options = {}, ctx) {
11
+ const context = ctx ?? createCliContext();
12
+ const result = await context.service.call("retrieveMemories", {
13
+ projectId: context.projectId,
14
+ query,
15
+ limit: coerceLimit(options.limit),
16
+ mode: "auto",
17
+ depth: "default",
18
+ });
19
+ if (!result.ok) {
20
+ console.error(result.error.message);
21
+ process.exit(1);
22
+ }
23
+ console.log(formatTable(result.memories));
24
+ // Surface the pre-rendered startup-injection block so the
25
+ // `author:` provenance annotation for teammate-authored memories is
26
+ // observable in real `search` output. Already a rendered string; no ANSI
27
+ // color.
28
+ console.log(result.startupInjection);
29
+ }
@@ -0,0 +1,15 @@
1
+ import { createCliContext } from "../context.js";
2
+ import { formatKeyValue } from "../output.js";
3
+ export async function showCommand(id, ctx) {
4
+ const context = ctx ?? createCliContext();
5
+ // Use call() to get the envelope (catches DomainError for NOT_FOUND)
6
+ const result = await context.service.call("getMemory", {
7
+ projectId: context.projectId,
8
+ memoryId: id,
9
+ });
10
+ if (!result.ok) {
11
+ console.error(result.error.message);
12
+ process.exit(1);
13
+ }
14
+ process.stdout.write(formatKeyValue(result.memory) + "\n");
15
+ }