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.
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/adapters/capabilities/fallbackTools.js +36 -0
- package/dist/adapters/contract/hostAdapterContract.js +1 -0
- package/dist/adapters/factory.js +40 -0
- package/dist/adapters/generic.js +128 -0
- package/dist/adapters/global/antigravity.js +22 -0
- package/dist/adapters/global/claudeCode.js +22 -0
- package/dist/adapters/global/codex.js +22 -0
- package/dist/adapters/global/qcoder.js +22 -0
- package/dist/adapters/ide/cline.js +20 -0
- package/dist/adapters/ide/cursor.js +28 -0
- package/dist/adapters/ide/installer.js +57 -0
- package/dist/adapters/ide/windsurf.js +28 -0
- package/dist/adapters/tools/ping.js +15 -0
- package/dist/cli/commands/config.js +79 -0
- package/dist/cli/commands/export.js +28 -0
- package/dist/cli/commands/forget.js +28 -0
- package/dist/cli/commands/import.js +112 -0
- package/dist/cli/commands/install.js +57 -0
- package/dist/cli/commands/list.js +13 -0
- package/dist/cli/commands/ping.js +12 -0
- package/dist/cli/commands/redactScan.js +40 -0
- package/dist/cli/commands/retention.js +54 -0
- package/dist/cli/commands/run.js +26 -0
- package/dist/cli/commands/search.js +29 -0
- package/dist/cli/commands/show.js +15 -0
- package/dist/cli/commands/stats.js +46 -0
- package/dist/cli/commands/sync.js +118 -0
- package/dist/cli/commands/team.js +96 -0
- package/dist/cli/commands/uninstall.js +30 -0
- package/dist/cli/context.js +69 -0
- package/dist/cli/index.js +147 -0
- package/dist/cli/output.js +37 -0
- package/dist/core/api/contracts.js +263 -0
- package/dist/core/api/errors.js +29 -0
- package/dist/core/api/localOnlyPolicy.js +29 -0
- package/dist/core/api/memoryCoreService.js +595 -0
- package/dist/core/api/sessionLifecycleService.js +289 -0
- package/dist/core/config/policyConfig.js +131 -0
- package/dist/core/embed/deterministicEmbed.js +31 -0
- package/dist/core/embed/embeddingVersion.js +1 -0
- package/dist/core/embed/reembedPolicy.js +9 -0
- package/dist/core/embed/textNormalize.js +12 -0
- package/dist/core/injection/formatStartupInjection.js +97 -0
- package/dist/core/injection/tokenBudget.js +38 -0
- package/dist/core/retrieve/decay.js +15 -0
- package/dist/core/retrieve/importance.js +6 -0
- package/dist/core/retrieve/recencyBands.js +18 -0
- package/dist/core/retrieve/retrieveMemories.js +83 -0
- package/dist/core/retrieve/score.js +25 -0
- package/dist/core/schema/migrations/001_initial.sql +25 -0
- package/dist/core/schema/migrations/002_indexes.sql +18 -0
- package/dist/core/schema/migrations/003_summarization_failures.sql +14 -0
- package/dist/core/schema/migrations/004_memory_feedback.sql +12 -0
- package/dist/core/schema/migrations/005_team_provenance.sql +9 -0
- package/dist/core/schema/runMigrations.js +38 -0
- package/dist/core/session.js +4 -0
- package/dist/core/storage/db.js +8 -0
- package/dist/core/storage/memoryFeedbackRepo.js +16 -0
- package/dist/core/storage/memoryRepo.js +179 -0
- package/dist/core/storage/memorySearchRepo.js +30 -0
- package/dist/core/storage/sessionEventsRepo.js +20 -0
- package/dist/core/storage/summarizationFailuresRepo.js +39 -0
- package/dist/core/storage/types.js +1 -0
- package/dist/core/summarize/cloudSummarizer.js +19 -0
- package/dist/core/summarize/localSummarizer.js +31 -0
- package/dist/core/summarize/redaction.js +48 -0
- package/dist/core/summarize/strategySelector.js +7 -0
- package/dist/core/summarize/summaryShape.js +49 -0
- 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
|
+
}
|