sessionmem 1.3.0 → 1.5.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/.github/sessionmem-logo.svg +57 -0
- package/CHANGELOG.md +128 -0
- package/CONTRIBUTING.md +130 -0
- package/README.md +292 -108
- package/SECURITY.md +33 -0
- package/dist/adapters/diagnostics.js +109 -0
- package/dist/adapters/generic.js +6 -2
- package/dist/adapters/global/antigravity.js +4 -1
- package/dist/adapters/global/claudeCode.js +19 -4
- package/dist/adapters/global/codex.js +7 -6
- package/dist/adapters/global/qcoder.js +4 -1
- package/dist/adapters/ide/cline.js +3 -0
- package/dist/adapters/ide/cursor.js +3 -0
- package/dist/adapters/ide/installer.js +17 -5
- package/dist/adapters/ide/windsurf.js +3 -0
- package/dist/adapters/installedHosts.js +51 -0
- package/dist/cli/commands/dedupe.js +1 -1
- package/dist/cli/commands/doctor.js +146 -0
- package/dist/cli/commands/entities.js +27 -0
- package/dist/cli/commands/history.js +23 -0
- package/dist/cli/commands/import.js +15 -44
- package/dist/cli/commands/ingestHook.js +4 -78
- package/dist/cli/commands/install.js +37 -59
- package/dist/cli/commands/list.js +3 -0
- package/dist/cli/commands/run.js +10 -12
- package/dist/cli/commands/scan.js +16 -0
- package/dist/cli/commands/search.js +9 -4
- package/dist/cli/commands/team.js +7 -3
- package/dist/cli/commands/ui.js +67 -0
- package/dist/cli/commands/uninstall.js +2 -1
- package/dist/cli/commands/watch.js +48 -0
- package/dist/cli/context.js +8 -6
- package/dist/cli/index.js +48 -7
- package/dist/cli/projectId.js +1 -2
- package/dist/cli/transcriptSweep.js +182 -0
- package/dist/core/api/contracts.js +1 -0
- package/dist/core/api/memoryCoreService.js +8 -0
- package/dist/core/api/sessionLifecycleService.js +13 -1
- package/dist/core/capture/toolEvent.js +82 -0
- package/dist/core/config/policyConfig.js +5 -1
- package/dist/core/extract/factClaims.js +97 -0
- package/dist/core/extract/knowledge.js +127 -0
- package/dist/core/injection/tokenBudget.js +10 -2
- package/dist/core/retrieve/retrieveMemories.js +20 -4
- package/dist/core/schema/migrations/012_transcript_checkpoints.sql +4 -0
- package/dist/core/schema/migrations/013_fts_porter.sql +32 -0
- package/dist/core/schema/migrations/014_memory_entities.sql +19 -0
- package/dist/core/schema/migrations/015_memory_supersessions.sql +41 -0
- package/dist/core/schema/runMigrations.js +55 -5
- package/dist/core/storage/db.js +31 -9
- package/dist/core/storage/knowledgeRepo.js +67 -0
- package/dist/core/storage/memoryRepo.js +45 -12
- package/dist/core/storage/memorySearchRepo.js +30 -20
- package/dist/core/storage/supersessionRepo.js +101 -0
- package/dist/core/summarize/cloudSummarizer.js +3 -8
- package/dist/core/summarize/summaryShape.js +1 -1
- package/dist/ui/assets.js +181 -0
- package/dist/ui/server.js +147 -0
- package/docs/REGISTRY_LISTING.md +88 -0
- package/docs/architecture.md +77 -0
- package/docs/benchmark.md +122 -0
- package/docs/cloud-summarization.md +57 -0
- package/docs/migration.md +44 -0
- package/docs/privacy-and-retention.md +139 -0
- package/docs/team-mode.md +117 -0
- package/docs/troubleshooting.md +126 -0
- package/docs/typescript-upgrade.md +48 -0
- package/package.json +25 -13
|
@@ -14,7 +14,7 @@ export async function importCommand(pathArg, options, ctx) {
|
|
|
14
14
|
// Read and parse the JSON file
|
|
15
15
|
let parsed;
|
|
16
16
|
try {
|
|
17
|
-
const raw =
|
|
17
|
+
const raw = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(inPath));
|
|
18
18
|
parsed = JSON.parse(raw);
|
|
19
19
|
}
|
|
20
20
|
catch (err) {
|
|
@@ -27,61 +27,32 @@ export async function importCommand(pathArg, options, ctx) {
|
|
|
27
27
|
console.error("Import file must be a JSON array of memory records.");
|
|
28
28
|
process.exit(1);
|
|
29
29
|
}
|
|
30
|
-
const
|
|
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.
|
|
30
|
+
const existingIds = options.merge ? undefined : listAllMemoryIds(context.db);
|
|
67
31
|
const validMemories = [];
|
|
32
|
+
let skippedCount = 0;
|
|
68
33
|
let invalidCount = 0;
|
|
69
|
-
for (
|
|
70
|
-
const
|
|
34
|
+
for (const [index, record] of parsed.entries()) {
|
|
35
|
+
const isRecord = record !== null && typeof record === "object" && !Array.isArray(record);
|
|
36
|
+
if (isRecord && typeof record.id === "string" && existingIds?.has(record.id)) {
|
|
37
|
+
skippedCount += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Validate unknown values before accessing fields. Keep the source index in
|
|
41
|
+
// diagnostics even when earlier records were duplicates, and preserve the
|
|
42
|
+
// optional provenance fields carried by exports.
|
|
43
|
+
const check = importMemoryRecordSchema.safeParse(isRecord ? { ...record, projectId: record.projectId ?? context.projectId } : record);
|
|
71
44
|
if (!check.success) {
|
|
72
|
-
console.error(`Record at index ${
|
|
45
|
+
console.error(`Record at index ${index} is invalid, skipping: ${check.error.message}`);
|
|
73
46
|
invalidCount += 1;
|
|
74
47
|
continue;
|
|
75
48
|
}
|
|
76
|
-
// Use the parsed/transformed record so `kind` is narrowed to the
|
|
77
|
-
// canonical enum (memoryKindSchema maps legacy values like `architecture`).
|
|
78
49
|
validMemories.push(check.data);
|
|
79
50
|
}
|
|
80
51
|
if (validMemories.length === 0) {
|
|
81
52
|
if (invalidCount > 0) {
|
|
82
53
|
console.log(`Imported 0, skipped ${invalidCount} invalid record(s).`);
|
|
83
54
|
}
|
|
84
|
-
else
|
|
55
|
+
else {
|
|
85
56
|
console.log(`Imported 0, skipped ${skippedCount} duplicates.`);
|
|
86
57
|
}
|
|
87
58
|
return;
|
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import { createCliContext } from "../context.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
const CAPTURED_TOOL_TYPES = new Set(["Bash", "Edit", "Write", "MultiEdit"]);
|
|
5
|
-
// Max payload size stored per hook event (chars). The full hook payload can be
|
|
6
|
-
// large (file diffs); cap it so a single tool use can't bloat session_events.
|
|
7
|
-
const MAX_HOOK_PAYLOAD_CHARS = 4000;
|
|
8
|
-
// Per-field cap applied BEFORE serialization. Truncating the serialized JSON
|
|
9
|
-
// instead would produce a string that is no longer valid JSON, which the
|
|
10
|
-
// `payloadJson` contract rejects (it parses the value) — the event would then be
|
|
11
|
-
// dropped, silently, by the catch-all below. Trim the inputs, not the output.
|
|
12
|
-
const MAX_FIELD_CHARS = 1200;
|
|
13
|
-
// One-line human summary shown in session summaries (summaryShape reads `text`).
|
|
14
|
-
const MAX_TEXT_CHARS = 300;
|
|
3
|
+
import { buildToolEvent } from "../../core/capture/toolEvent.js";
|
|
15
4
|
/** Emit a hook diagnostic to stderr when SESSIONMEM_DEBUG=1. */
|
|
16
5
|
function debug(message) {
|
|
17
6
|
if (process.env.SESSIONMEM_DEBUG === "1") {
|
|
18
7
|
process.stderr.write(`[sessionmem] ingest-hook: ${message}\n`);
|
|
19
8
|
}
|
|
20
9
|
}
|
|
21
|
-
function truncate(value, max) {
|
|
22
|
-
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
23
|
-
}
|
|
24
10
|
async function readStdinJson() {
|
|
25
11
|
if (process.stdin.isTTY)
|
|
26
12
|
return {};
|
|
@@ -56,65 +42,6 @@ function resolveSessionId(payload) {
|
|
|
56
42
|
}
|
|
57
43
|
return process.env.CLAUDE_CODE_SESSION_ID ?? process.env.SESSION_ID ?? `session-${Date.now()}`;
|
|
58
44
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Shorten every string in a tool input to MAX_FIELD_CHARS, recursively. Keeps
|
|
61
|
-
* the object's shape (so it still serializes to valid JSON) while bounding the
|
|
62
|
-
* size contributed by large fields such as a Write tool's `content` or an Edit
|
|
63
|
-
* tool's replacement text.
|
|
64
|
-
*/
|
|
65
|
-
function shrinkValue(value, depth = 0) {
|
|
66
|
-
if (typeof value === "string")
|
|
67
|
-
return truncate(value, MAX_FIELD_CHARS);
|
|
68
|
-
if (depth >= 4)
|
|
69
|
-
return undefined;
|
|
70
|
-
if (Array.isArray(value)) {
|
|
71
|
-
return value.slice(0, 20).map((item) => shrinkValue(item, depth + 1));
|
|
72
|
-
}
|
|
73
|
-
if (value && typeof value === "object") {
|
|
74
|
-
const out = {};
|
|
75
|
-
for (const [key, inner] of Object.entries(value)) {
|
|
76
|
-
out[key] = shrinkValue(inner, depth + 1);
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
// Numbers/booleans/null pass through; functions/undefined/symbols drop out.
|
|
81
|
-
return value === undefined || typeof value === "function" || typeof value === "symbol"
|
|
82
|
-
? undefined
|
|
83
|
-
: value;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* A short human-readable line describing the tool use. Stored alongside the
|
|
87
|
-
* structured input as `text`, which the local summarizer prefers over the raw
|
|
88
|
-
* payload — so session summaries read as prose instead of dumped JSON.
|
|
89
|
-
*/
|
|
90
|
-
function describeToolUse(toolName, input) {
|
|
91
|
-
const str = (key) => typeof input?.[key] === "string" ? input[key] : "";
|
|
92
|
-
if (toolName === "Bash") {
|
|
93
|
-
const command = str("command").replace(/\s+/g, " ").trim();
|
|
94
|
-
return truncate(command ? `Ran: ${command}` : "Ran a shell command", MAX_TEXT_CHARS);
|
|
95
|
-
}
|
|
96
|
-
const filePath = str("file_path") || str("path") || str("notebook_path");
|
|
97
|
-
const verb = toolName === "Write" ? "Wrote" : "Edited";
|
|
98
|
-
return truncate(filePath ? `${verb} ${filePath}` : `${verb} a file`, MAX_TEXT_CHARS);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Serialize the event payload, guaranteeing valid JSON within
|
|
102
|
-
* MAX_HOOK_PAYLOAD_CHARS. Per-field shrinking handles the common case; if the
|
|
103
|
-
* result is still over budget (many fields, or one huge key set), drop the
|
|
104
|
-
* structured input and keep only the human-readable summary rather than
|
|
105
|
-
* emitting truncated — and therefore unparseable — JSON.
|
|
106
|
-
*/
|
|
107
|
-
function buildPayloadJson(toolName, text, input, cwd) {
|
|
108
|
-
const full = JSON.stringify({
|
|
109
|
-
tool: toolName,
|
|
110
|
-
text,
|
|
111
|
-
input: shrinkValue(input),
|
|
112
|
-
cwd,
|
|
113
|
-
});
|
|
114
|
-
if (full.length <= MAX_HOOK_PAYLOAD_CHARS)
|
|
115
|
-
return full;
|
|
116
|
-
return JSON.stringify({ tool: toolName, text, cwd, truncated: true });
|
|
117
|
-
}
|
|
118
45
|
/**
|
|
119
46
|
* `sessionmem ingest-hook` — PostToolUse hook handler.
|
|
120
47
|
* Reads Claude Code's PostToolUse JSON payload from stdin and stores it as a
|
|
@@ -126,14 +53,14 @@ export async function ingestHookCommand(ctx) {
|
|
|
126
53
|
try {
|
|
127
54
|
const payload = await readStdinJson();
|
|
128
55
|
const toolName = payload.tool_name ?? "";
|
|
129
|
-
|
|
56
|
+
const event = buildToolEvent(toolName, payload.tool_input, payload.cwd);
|
|
57
|
+
if (!event) {
|
|
130
58
|
debug(`skipped: tool "${toolName}" is not captured`);
|
|
131
59
|
return;
|
|
132
60
|
}
|
|
133
61
|
context = ctx ?? createCliContext();
|
|
134
62
|
const sessionId = resolveSessionId(payload);
|
|
135
63
|
const projectId = context.projectId;
|
|
136
|
-
const text = describeToolUse(toolName, payload.tool_input);
|
|
137
64
|
// eventIndex is intentionally omitted: the service assigns the next free
|
|
138
65
|
// index per session inside an immediate transaction, so parallel tool calls
|
|
139
66
|
// cannot collide (see nextSessionEventIndex).
|
|
@@ -142,8 +69,7 @@ export async function ingestHookCommand(ctx) {
|
|
|
142
69
|
sessionId,
|
|
143
70
|
events: [{
|
|
144
71
|
id: randomUUID(),
|
|
145
|
-
|
|
146
|
-
payloadJson: buildPayloadJson(toolName, text, payload.tool_input, payload.cwd),
|
|
72
|
+
...event,
|
|
147
73
|
}],
|
|
148
74
|
});
|
|
149
75
|
if (!result.ok) {
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveInstallAdapters } from "../../adapters/installedHosts.js";
|
|
5
5
|
import { injectGuidanceBlock } from "../../adapters/claudeMdInjector.js";
|
|
6
|
-
import { createCliContext } from "../context.js";
|
|
7
|
-
/**
|
|
8
|
-
* Resolve the adapter to install into: an explicit `--adapter` flag wins, then
|
|
9
|
-
* the SESSIONMEM_ADAPTER env override, else auto-detection from the host env.
|
|
10
|
-
* Throws (via AdapterFactory.forName) on an unknown explicit name so the CLI
|
|
11
|
-
* surfaces a clear error rather than silently installing the generic adapter.
|
|
12
|
-
*/
|
|
13
|
-
function resolveAdapter(options) {
|
|
14
|
-
const explicit = options?.adapter && options.adapter.trim() !== ""
|
|
15
|
-
? options.adapter.trim()
|
|
16
|
-
: process.env.SESSIONMEM_ADAPTER && process.env.SESSIONMEM_ADAPTER.trim() !== ""
|
|
17
|
-
? process.env.SESSIONMEM_ADAPTER.trim()
|
|
18
|
-
: undefined;
|
|
19
|
-
if (explicit) {
|
|
20
|
-
return { adapter: AdapterFactory.forName(explicit), forced: true };
|
|
21
|
-
}
|
|
22
|
-
return { adapter: AdapterFactory.detectAdapter(), forced: false };
|
|
23
|
-
}
|
|
6
|
+
import { createCliContext, resolveDbPath } from "../context.js";
|
|
24
7
|
import { configFilePath, writePolicyConfig, DEFAULT_POLICY_CONFIG, } from "../../core/config/policyConfig.js";
|
|
25
8
|
export const MANUAL_CONFIG_BLOCK = JSON.stringify({
|
|
26
9
|
mcpServers: {
|
|
@@ -37,10 +20,10 @@ export function printManualFallback(adapterName) {
|
|
|
37
20
|
export async function installCommand(options, contextOverrides) {
|
|
38
21
|
// Resolve the target adapter up front so an invalid `--adapter` fails before
|
|
39
22
|
// any filesystem work (DB init, config write).
|
|
40
|
-
let
|
|
23
|
+
let adapters;
|
|
41
24
|
let forced;
|
|
42
25
|
try {
|
|
43
|
-
({
|
|
26
|
+
({ adapters, forced } = resolveInstallAdapters(options));
|
|
44
27
|
}
|
|
45
28
|
catch (err) {
|
|
46
29
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -51,10 +34,12 @@ export async function installCommand(options, contextOverrides) {
|
|
|
51
34
|
try {
|
|
52
35
|
const ctx = createCliContext(contextOverrides);
|
|
53
36
|
dbPath = ctx.dbPath;
|
|
37
|
+
if (!contextOverrides?.db)
|
|
38
|
+
ctx.db.close();
|
|
54
39
|
}
|
|
55
40
|
catch (err) {
|
|
56
|
-
console.error(`✗ DB init failed (
|
|
57
|
-
console.error("Hint: ensure
|
|
41
|
+
console.error(`✗ DB init failed (${resolveDbPath(contextOverrides?.dbPath)}): ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
console.error("Hint: ensure the database parent directory is writable and run `sessionmem install` again.");
|
|
58
43
|
process.exit(1);
|
|
59
44
|
}
|
|
60
45
|
console.log(`✓ DB initialized (${dbPath})`);
|
|
@@ -69,47 +54,40 @@ export async function installCommand(options, contextOverrides) {
|
|
|
69
54
|
writePolicyConfig(configPath, { ...DEFAULT_POLICY_CONFIG });
|
|
70
55
|
console.log(`✓ config.json initialized (${configPath})`);
|
|
71
56
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!adapter.install) {
|
|
79
|
-
console.error(`✗ ${adapter.name} config update failed`);
|
|
80
|
-
printManualFallback(adapter.name);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
const success = await adapter.install();
|
|
84
|
-
if (!success) {
|
|
85
|
-
console.error(`✗ ${adapter.name} config update failed`);
|
|
86
|
-
printManualFallback(adapter.name);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
console.log(`✓ ${adapter.name} config updated`);
|
|
90
|
-
// Step 3: Guidance injection — non-fatal. Inject the sessionmem instruction
|
|
91
|
-
// block into the file(s) the detected host actually reads at session start so
|
|
92
|
-
// the agent automatically knows the MCP exists and how to use it. Adapters
|
|
93
|
-
// declare their own target(s); when none are declared (e.g. a minimal mock or
|
|
94
|
-
// an unknown host) fall back to the global Claude Code memory file.
|
|
95
|
-
const guidanceTargets = adapter.guidanceTargets?.() ?? [join(homedir(), ".claude", "CLAUDE.md")];
|
|
96
|
-
for (const target of guidanceTargets) {
|
|
57
|
+
// Try every detected host even when one host's config cannot be updated.
|
|
58
|
+
let failed = false;
|
|
59
|
+
for (const adapter of adapters) {
|
|
60
|
+
console.log(forced
|
|
61
|
+
? `→ Installing for adapter: ${adapter.name} (forced)`
|
|
62
|
+
: `→ Detected host: ${adapter.name} (override with --adapter <name>)`);
|
|
97
63
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log(`✓ Agent guidance injected (${target})`);
|
|
64
|
+
if (!adapter.install || !(await adapter.install())) {
|
|
65
|
+
throw new Error("config update failed");
|
|
101
66
|
}
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
console.log(`✓ ${adapter.name} config updated`);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(`✗ ${adapter.name} config update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
printManualFallback(adapter.name);
|
|
72
|
+
failed = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const guidanceTargets = adapter.guidanceTargets?.() ?? [join(homedir(), ".claude", "CLAUDE.md")];
|
|
76
|
+
for (const target of guidanceTargets) {
|
|
77
|
+
try {
|
|
78
|
+
if (!injectGuidanceBlock(target))
|
|
79
|
+
throw new Error("injection failed");
|
|
80
|
+
console.log(`✓ ${adapter.name}: Agent guidance injected (${target})`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
console.error(`✗ ${adapter.name}: Agent guidance injection failed (non-fatal): ${target}`);
|
|
104
84
|
}
|
|
105
85
|
}
|
|
106
|
-
|
|
107
|
-
console.
|
|
86
|
+
if (adapter.name === "Claude Code") {
|
|
87
|
+
console.log("✓ Claude Code: SessionStart, SessionEnd, PostToolUse hooks configured (~/.claude/settings.json)");
|
|
108
88
|
}
|
|
109
89
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log("✓ Auto-injection hook installed (~/.claude/settings.json) — prior memories load automatically at the start of every Claude Code session");
|
|
113
|
-
}
|
|
90
|
+
if (failed)
|
|
91
|
+
process.exit(1);
|
|
114
92
|
console.log("✓ sessionmem ready — restart your editor/agent so it picks up the MCP server");
|
|
115
93
|
}
|
|
@@ -10,4 +10,7 @@ export async function listCommand(ctx) {
|
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
console.log(formatTable(result.memories));
|
|
13
|
+
if (result.total > result.memories.length) {
|
|
14
|
+
console.error(`Showing ${result.memories.length} of ${result.total} memories. Use \`sessionmem export\` for the full project.`);
|
|
15
|
+
}
|
|
13
16
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { AdapterFactory } from "../../adapters/factory.js";
|
|
2
2
|
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
3
|
+
import { join, dirname } from "path";
|
|
5
4
|
import { deriveProjectId } from "../projectId.js";
|
|
6
|
-
import {
|
|
5
|
+
import { resolveDbPath } from "../context.js";
|
|
7
6
|
export async function runMcpServer() {
|
|
8
7
|
const adapter = AdapterFactory.detectAdapter();
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Keep diagnostics beside the selected database so an override never
|
|
9
|
+
// creates the default home directory just to write a log.
|
|
10
|
+
const dbPath = resolveDbPath();
|
|
11
|
+
const logDir = join(dirname(dbPath), "logs");
|
|
11
12
|
const logPath = join(logDir, "mcp.log");
|
|
12
|
-
// Derive db path for diagnostics (mirrors context.ts defaultDbPath)
|
|
13
|
-
const envDbPath = process.env.SESSIONMEM_DB_PATH;
|
|
14
|
-
const dbPath = envDbPath && envDbPath.trim() !== ""
|
|
15
|
-
? expandTilde(envDbPath)
|
|
16
|
-
: join(homedir(), ".sessionmem", "memories.db");
|
|
17
13
|
// Derive project ID for diagnostics. Shares the exact derivation the server
|
|
18
14
|
// uses (createCliContext → deriveProjectId) so the logged id never diverges
|
|
19
15
|
// from the bucket the server actually serves.
|
|
@@ -21,8 +17,10 @@ export async function runMcpServer() {
|
|
|
21
17
|
const adapterName = adapter.name;
|
|
22
18
|
const logMessage = `[${new Date().toISOString()}] Started sessionmem | adapter=${adapterName} db=${dbPath} project=${projectId}\n`;
|
|
23
19
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
if (dbPath !== ":memory:") {
|
|
21
|
+
mkdirSync(logDir, { recursive: true });
|
|
22
|
+
writeFileSync(logPath, logMessage, { flag: "a" });
|
|
23
|
+
}
|
|
26
24
|
}
|
|
27
25
|
catch {
|
|
28
26
|
// best-effort logging; ignore failures
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createCliContext } from "../context.js";
|
|
2
|
+
import { printSweepSummary, sweepTranscripts } from "../transcriptSweep.js";
|
|
3
|
+
export async function scanCommand(options = {}, ctx) {
|
|
4
|
+
const context = ctx ?? createCliContext();
|
|
5
|
+
try {
|
|
6
|
+
const summary = await sweepTranscripts(context, options);
|
|
7
|
+
printSweepSummary("scan", summary);
|
|
8
|
+
if (summary.errors)
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
return summary;
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
if (!ctx)
|
|
14
|
+
context.db.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { createCliContext } from "../context.js";
|
|
2
2
|
import { formatTable } from "../output.js";
|
|
3
|
-
const DEFAULT_LIMIT =
|
|
3
|
+
const DEFAULT_LIMIT = 10;
|
|
4
4
|
function coerceLimit(value) {
|
|
5
5
|
if (value === undefined)
|
|
6
6
|
return DEFAULT_LIMIT;
|
|
7
|
-
const parsed = Number
|
|
8
|
-
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
9
|
+
throw new Error(`Invalid --limit "${value}": expected an integer from 1 to 100.`);
|
|
10
|
+
}
|
|
11
|
+
return parsed;
|
|
9
12
|
}
|
|
10
13
|
export async function searchCommand(query, options = {}, ctx) {
|
|
14
|
+
const limit = coerceLimit(options.limit);
|
|
11
15
|
const context = ctx ?? createCliContext();
|
|
12
16
|
const result = await context.service.call("retrieveMemories", {
|
|
13
17
|
projectId: context.projectId,
|
|
14
18
|
query,
|
|
15
|
-
limit
|
|
19
|
+
limit,
|
|
16
20
|
mode: "auto",
|
|
17
21
|
depth: "default",
|
|
22
|
+
includeHistory: options.includeHistory,
|
|
18
23
|
});
|
|
19
24
|
if (!result.ok) {
|
|
20
25
|
console.error(result.error.message);
|
|
@@ -3,6 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
4
|
import { configFilePath, readPolicyConfig, writePolicyConfig, } from "../../core/config/policyConfig.js";
|
|
5
5
|
import { createCliContext } from "../context.js";
|
|
6
|
+
import { reconcileProjectSupersessions } from "../../core/storage/supersessionRepo.js";
|
|
6
7
|
function resolvePath(options) {
|
|
7
8
|
return options?.configPath ?? configFilePath();
|
|
8
9
|
}
|
|
@@ -89,8 +90,11 @@ export function teamDisableCommand(options, ctx) {
|
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
const context = ctx ?? createCliContext();
|
|
92
|
-
const result = context.db
|
|
93
|
-
.prepare("DELETE FROM memories WHERE project_id = ? AND author != ? AND author != ''")
|
|
94
|
-
|
|
93
|
+
const result = context.db.transaction(() => {
|
|
94
|
+
const removed = context.db.prepare("DELETE FROM memories WHERE project_id = ? AND author != ? AND author != ''").run(context.projectId, context.username);
|
|
95
|
+
if (removed.changes)
|
|
96
|
+
reconcileProjectSupersessions(context.db, context.projectId);
|
|
97
|
+
return removed;
|
|
98
|
+
})();
|
|
95
99
|
console.log(`Team mode disabled. Removed ${result.changes} teammate-authored ${result.changes === 1 ? "memory" : "memories"}.`);
|
|
96
100
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { createMemoryCoreService } from "../../core/api/memoryCoreService.js";
|
|
5
|
+
import { deriveProjectId } from "../projectId.js";
|
|
6
|
+
import { resolveDbPath } from "../context.js";
|
|
7
|
+
import { inspectMigrations } from "../../core/schema/runMigrations.js";
|
|
8
|
+
import { parseUiPort, startUiServer } from "../../ui/server.js";
|
|
9
|
+
/** Open the OS browser without a shell or an additional runtime dependency. */
|
|
10
|
+
export function openBrowser(url) {
|
|
11
|
+
const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
12
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
13
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
14
|
+
const failed = () => console.error("Could not open a browser. Open the printed URL manually.");
|
|
15
|
+
child.once("error", failed);
|
|
16
|
+
child.once("exit", code => { if (code)
|
|
17
|
+
failed(); });
|
|
18
|
+
child.unref();
|
|
19
|
+
}
|
|
20
|
+
export async function uiCommand(options = {}) {
|
|
21
|
+
const port = parseUiPort(options.port);
|
|
22
|
+
// Unlike the normal CLI context, a viewer must not create, migrate or write the store.
|
|
23
|
+
let db;
|
|
24
|
+
try {
|
|
25
|
+
db = new BetterSqlite3(resolveDbPath(), { readonly: true, fileMustExist: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("Cannot open the memory store. Run sessionmem ping first, or check SESSIONMEM_DB_PATH.");
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
db.pragma("query_only = ON");
|
|
32
|
+
db.pragma("busy_timeout = 5000");
|
|
33
|
+
const migrations = inspectMigrations(db);
|
|
34
|
+
if (migrations.unknown.length) {
|
|
35
|
+
throw new Error("The memory store uses an unrecognized schema. Use the sessionmem version that created it.");
|
|
36
|
+
}
|
|
37
|
+
if (migrations.pending.length) {
|
|
38
|
+
throw new Error("The memory store needs an upgrade. Run sessionmem ping first to apply pending migrations.");
|
|
39
|
+
}
|
|
40
|
+
const viewer = await startUiServer({ db, service: createMemoryCoreService({ db }), projectId: deriveProjectId() }, port);
|
|
41
|
+
let stopping = false;
|
|
42
|
+
const stop = () => {
|
|
43
|
+
if (stopping)
|
|
44
|
+
return;
|
|
45
|
+
stopping = true;
|
|
46
|
+
void viewer.close().catch(error => {
|
|
47
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
process.once("SIGINT", stop);
|
|
52
|
+
process.once("SIGTERM", stop);
|
|
53
|
+
try {
|
|
54
|
+
const closed = once(viewer.server, "close");
|
|
55
|
+
console.log(`sessionmem viewer: ${viewer.url}\nRead-only. Press Ctrl-C to stop.`);
|
|
56
|
+
openBrowser(viewer.url);
|
|
57
|
+
await closed;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
process.removeListener("SIGINT", stop);
|
|
61
|
+
process.removeListener("SIGTERM", stop);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, rmSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
+
import { resolveDbPath } from "../context.js";
|
|
4
5
|
import { AdapterFactory } from "../../adapters/factory.js";
|
|
5
6
|
import { removeClaudeMdBlock } from "../../adapters/claudeMdInjector.js";
|
|
6
7
|
/**
|
|
@@ -51,7 +52,7 @@ export async function uninstallCommand(options = {}) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
// Resolve dbPath: use injected override (for tests) or the default location
|
|
54
|
-
const dbPath = options.dbPath
|
|
55
|
+
const dbPath = resolveDbPath(options.dbPath);
|
|
55
56
|
if (options.purge) {
|
|
56
57
|
// --purge: delete only memories.db, not logs or the ~/.sessionmem directory
|
|
57
58
|
if (existsSync(dbPath)) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers/promises";
|
|
2
|
+
import { createCliContext } from "../context.js";
|
|
3
|
+
import { printSweepSummary, sweepTranscripts } from "../transcriptSweep.js";
|
|
4
|
+
export async function watchCommand(options = {}, ctx) {
|
|
5
|
+
const interval = Number(options.interval ?? 5);
|
|
6
|
+
if (!Number.isFinite(interval) || interval <= 0 || interval > 86400) {
|
|
7
|
+
throw new Error("--interval must be a number greater than 0 and at most 86400 seconds");
|
|
8
|
+
}
|
|
9
|
+
const context = ctx ?? createCliContext();
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const stop = () => controller.abort();
|
|
12
|
+
process.once("SIGINT", stop);
|
|
13
|
+
process.once("SIGTERM", stop);
|
|
14
|
+
let firstSweep = true;
|
|
15
|
+
try {
|
|
16
|
+
do {
|
|
17
|
+
try {
|
|
18
|
+
const summary = await sweepTranscripts(context, options);
|
|
19
|
+
if (firstSweep || options.once || summary.sessionsImported || summary.errors) {
|
|
20
|
+
printSweepSummary("watch", summary);
|
|
21
|
+
}
|
|
22
|
+
firstSweep = false;
|
|
23
|
+
if (options.once && summary.errors)
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (options.once)
|
|
28
|
+
throw error;
|
|
29
|
+
console.error(`[sessionmem] watch: ${error instanceof Error ? error.message : String(error)}`);
|
|
30
|
+
}
|
|
31
|
+
if (options.once || controller.signal.aborted)
|
|
32
|
+
break;
|
|
33
|
+
try {
|
|
34
|
+
await setTimeout(interval * 1000, undefined, { signal: controller.signal });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!controller.signal.aborted)
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
} while (!controller.signal.aborted);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
process.removeListener("SIGINT", stop);
|
|
44
|
+
process.removeListener("SIGTERM", stop);
|
|
45
|
+
if (!ctx)
|
|
46
|
+
context.db.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/cli/context.js
CHANGED
|
@@ -42,19 +42,21 @@ export function expandTilde(p) {
|
|
|
42
42
|
}
|
|
43
43
|
return p;
|
|
44
44
|
}
|
|
45
|
-
function
|
|
45
|
+
export function resolveDbPath(override) {
|
|
46
|
+
if (override !== undefined)
|
|
47
|
+
return expandTilde(override);
|
|
46
48
|
// Env override seam (see deriveProjectId). Defaults to ~/.sessionmem/memories.db.
|
|
47
49
|
const envDbPath = process.env.SESSIONMEM_DB_PATH;
|
|
48
50
|
if (envDbPath && envDbPath.trim() !== "")
|
|
49
51
|
return expandTilde(envDbPath);
|
|
50
|
-
// `dir` is the fixed ~/.sessionmem dir computed above, not user input.
|
|
51
52
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
52
|
-
return join(
|
|
53
|
+
return join(homedir(), ".sessionmem", "memories.db");
|
|
53
54
|
}
|
|
54
55
|
export function createCliContext(overrides = {}) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const dbPath = resolveDbPath(overrides.dbPath);
|
|
57
|
+
if (!overrides.db && dbPath !== ":memory:") {
|
|
58
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
59
|
+
}
|
|
58
60
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
59
61
|
const migrationsDir = overrides.migrationsDir ?? join(here, "..", "core", "schema", "migrations");
|
|
60
62
|
const username = overrides.username ?? localUsername();
|