sessionmem 1.4.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +186 -41
  3. package/dist/adapters/diagnostics.js +109 -0
  4. package/dist/adapters/generic.js +6 -2
  5. package/dist/adapters/global/antigravity.js +4 -1
  6. package/dist/adapters/global/claudeCode.js +19 -4
  7. package/dist/adapters/global/codex.js +7 -6
  8. package/dist/adapters/global/qcoder.js +4 -1
  9. package/dist/adapters/ide/cline.js +3 -0
  10. package/dist/adapters/ide/cursor.js +3 -0
  11. package/dist/adapters/ide/installer.js +17 -5
  12. package/dist/adapters/ide/windsurf.js +3 -0
  13. package/dist/adapters/installedHosts.js +51 -0
  14. package/dist/cli/commands/doctor.js +146 -0
  15. package/dist/cli/commands/entities.js +27 -0
  16. package/dist/cli/commands/history.js +23 -0
  17. package/dist/cli/commands/ingestHook.js +4 -78
  18. package/dist/cli/commands/install.js +34 -56
  19. package/dist/cli/commands/scan.js +16 -0
  20. package/dist/cli/commands/search.js +1 -0
  21. package/dist/cli/commands/team.js +7 -3
  22. package/dist/cli/commands/ui.js +67 -0
  23. package/dist/cli/commands/watch.js +48 -0
  24. package/dist/cli/index.js +42 -1
  25. package/dist/cli/projectId.js +1 -2
  26. package/dist/cli/transcriptSweep.js +182 -0
  27. package/dist/core/api/contracts.js +1 -0
  28. package/dist/core/api/memoryCoreService.js +8 -0
  29. package/dist/core/api/sessionLifecycleService.js +13 -1
  30. package/dist/core/capture/toolEvent.js +82 -0
  31. package/dist/core/extract/factClaims.js +97 -0
  32. package/dist/core/extract/knowledge.js +127 -0
  33. package/dist/core/retrieve/retrieveMemories.js +20 -4
  34. package/dist/core/schema/migrations/012_transcript_checkpoints.sql +4 -0
  35. package/dist/core/schema/migrations/013_fts_porter.sql +32 -0
  36. package/dist/core/schema/migrations/014_memory_entities.sql +19 -0
  37. package/dist/core/schema/migrations/015_memory_supersessions.sql +41 -0
  38. package/dist/core/schema/runMigrations.js +46 -0
  39. package/dist/core/storage/knowledgeRepo.js +67 -0
  40. package/dist/core/storage/memoryRepo.js +45 -12
  41. package/dist/core/storage/memorySearchRepo.js +30 -20
  42. package/dist/core/storage/supersessionRepo.js +101 -0
  43. package/dist/core/summarize/summaryShape.js +1 -1
  44. package/dist/ui/assets.js +181 -0
  45. package/dist/ui/server.js +147 -0
  46. package/docs/REGISTRY_LISTING.md +88 -0
  47. package/docs/architecture.md +27 -3
  48. package/docs/benchmark.md +105 -33
  49. package/docs/migration.md +2 -0
  50. package/docs/troubleshooting.md +52 -4
  51. package/package.json +1 -1
@@ -21,6 +21,9 @@ export class CursorAdapter extends GenericMCPAdapter {
21
21
  get configPath() {
22
22
  return join(homedir(), ".cursor", "mcp.json");
23
23
  }
24
+ installation() {
25
+ return { mcp: { path: this.configPath, command: "sessionmem", args: ["run"] } };
26
+ }
24
27
  async install() {
25
28
  return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"]);
26
29
  }
@@ -35,6 +35,15 @@ export const SESSIONMEM_POST_TOOL_HOOK_COMMAND = _isWindows
35
35
  export const SESSIONMEM_POST_TOOL_MATCHER = "Bash|Edit|Write|MultiEdit";
36
36
  /** Default Claude Code hook event for the legacy 2-arg hook helpers. */
37
37
  const DEFAULT_HOOK_EVENT = "SessionStart";
38
+ /** Recognize our bare/Windows hook forms, including stale command arguments. */
39
+ export function isManagedClaudeHook(value, expected) {
40
+ if (typeof value !== "string")
41
+ return false;
42
+ const normalize = (command) => command.replace(/^cmd \/c /i, "");
43
+ const base = normalize(expected);
44
+ const candidate = normalize(value);
45
+ return candidate === base || candidate.startsWith(`${base} `);
46
+ }
38
47
  export class IDEInstaller {
39
48
  static parseJsonc(content) {
40
49
  // Strip single-line comments (//) that appear outside of string literals.
@@ -201,11 +210,14 @@ export class IDEInstaller {
201
210
  const eventEntries = Array.isArray(hooks[eventName])
202
211
  ? hooks[eventName]
203
212
  : [];
204
- // Drop any pre-existing sessionmem entry so re-install stays idempotent and
205
- // a stale command form is replaced rather than duplicated. hookEntryHasCommand
206
- // only inspects entry.hooks, so it dedupes correctly whether or not the entry
207
- // carries a matcher.
208
- const filtered = eventEntries.filter((entry) => !this.hookEntryHasCommand(entry, command));
213
+ // Replace our commands, including stale forms, while preserving user hooks
214
+ // that share an entry. The fresh entry restores the current matcher/type.
215
+ const filtered = eventEntries.flatMap((entry) => {
216
+ if (!entry || !Array.isArray(entry.hooks))
217
+ return [entry];
218
+ const remaining = entry.hooks.filter((hook) => !isManagedClaudeHook(hook?.command, command));
219
+ return remaining.length ? [{ ...entry, hooks: remaining }] : [];
220
+ });
209
221
  filtered.push(matcher !== undefined
210
222
  ? { matcher, hooks: [{ type: "command", command }] }
211
223
  : { hooks: [{ type: "command", command }] });
@@ -20,6 +20,9 @@ export class WindsurfAdapter extends GenericMCPAdapter {
20
20
  get configPath() {
21
21
  return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
22
22
  }
23
+ installation() {
24
+ return { mcp: { path: this.configPath, command: "sessionmem", args: ["run"] } };
25
+ }
23
26
  async install() {
24
27
  return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"]);
25
28
  }
@@ -0,0 +1,51 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { delimiter, join } from "node:path";
4
+ import { AdapterFactory } from "./factory.js";
5
+ // Installed-host detection is deliberately separate from runtime session-env
6
+ // detection. Add a row here when another host has a verified install probe.
7
+ export const INSTALLED_HOSTS = [
8
+ {
9
+ adapter: "claude-code",
10
+ executables: ["claude"],
11
+ homeExecutables: [".local/bin/claude", ".claude/local/claude"],
12
+ },
13
+ ];
14
+ export function isExecutable(file) {
15
+ try {
16
+ if (!statSync(file).isFile())
17
+ return false;
18
+ accessSync(file, process.platform === "win32" ? constants.F_OK : constants.X_OK);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ export function executableCandidates(file) {
26
+ if (process.platform !== "win32")
27
+ return [file];
28
+ const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";");
29
+ return [file, ...extensions.filter(Boolean).map((ext) => file + ext.toLowerCase())];
30
+ }
31
+ export function detectInstalledAdapters(table = INSTALLED_HOSTS) {
32
+ const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
33
+ return table.filter((host) => {
34
+ const paths = [
35
+ ...host.executables.flatMap((name) => pathDirs.map((dir) => join(dir.replace(/^"|"$/g, ""), name))),
36
+ ...(host.homeExecutables ?? []).map((file) => join(homedir(), file)),
37
+ ];
38
+ return paths.some((file) => executableCandidates(file).some(isExecutable));
39
+ }).map((host) => AdapterFactory.forName(host.adapter));
40
+ }
41
+ /** Explicit flags take precedence over the existing environment override. */
42
+ export function resolveInstallAdapters(options) {
43
+ const explicit = options?.adapter?.trim() || process.env.SESSIONMEM_ADAPTER?.trim();
44
+ if (explicit)
45
+ return { adapters: [AdapterFactory.forName(explicit)], forced: true };
46
+ const adapters = detectInstalledAdapters();
47
+ if (adapters.length === 0) {
48
+ throw new Error("No supported agent host found. Install Claude Code or use sessionmem install --adapter <name>.");
49
+ }
50
+ return { adapters, forced: false };
51
+ }
@@ -0,0 +1,146 @@
1
+ import BetterSqlite3 from "better-sqlite3";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { resolveInstallAdapters } from "../../adapters/installedHosts.js";
5
+ import { checkHooks, readMcpLaunch, probeMcpWiring } from "../../adapters/diagnostics.js";
6
+ import { ADAPTER_NAMES, AdapterFactory } from "../../adapters/factory.js";
7
+ import { createMemoryCoreService } from "../../core/api/memoryCoreService.js";
8
+ import { inspectMigrations } from "../../core/schema/runMigrations.js";
9
+ import { resolveDbPath } from "../context.js";
10
+ const VERSION = createRequire(import.meta.url)("../../../package.json").version;
11
+ /** Exercise real storage, embedding, ranking and injection without retaining data. */
12
+ export async function probeRecall(db) {
13
+ const id = `sessionmem-doctor-${randomUUID()}`;
14
+ const content = "Sessionmem doctor canary checks local memory recall.";
15
+ const service = createMemoryCoreService({ db });
16
+ // A separate project prevents both ranking collisions and access-count changes
17
+ // to user memories. Rollback also removes FTS entries and feedback on failure.
18
+ db.exec("BEGIN IMMEDIATE");
19
+ try {
20
+ const stored = await service.call("storeMemory", {
21
+ projectId: id, sessionId: id, memoryId: id, sourceAdapter: "doctor",
22
+ kind: "fact", content, importance: 5,
23
+ });
24
+ if (!stored.ok)
25
+ throw new Error(`Canary write failed: ${stored.error.message}`);
26
+ const recalled = await service.call("retrieveMemories", {
27
+ projectId: id, query: content, limit: 5, mode: "auto", depth: "default",
28
+ });
29
+ if (!recalled.ok)
30
+ throw new Error(`Canary recall failed: ${recalled.error.message}`);
31
+ if (!recalled.memories.some((memory) => memory.id === id && memory.content === content)) {
32
+ throw new Error("Recall did not return the canary memory");
33
+ }
34
+ }
35
+ finally {
36
+ db.exec("ROLLBACK");
37
+ }
38
+ }
39
+ export async function doctorCommand(options, overrides = {}) {
40
+ const results = [];
41
+ const report = (check, ok, detail, fix) => {
42
+ results.push({ check, ok, detail, ...(fix ? { fix } : {}) });
43
+ console.log(`${ok ? "PASS" : "FAIL"} ${check}: ${detail}`);
44
+ if (fix)
45
+ console.log(` Fix: ${fix}`);
46
+ };
47
+ const message = (err) => err instanceof Error ? err.message : String(err);
48
+ let adapters = [];
49
+ try {
50
+ adapters = resolveInstallAdapters(options).adapters;
51
+ }
52
+ catch (err) {
53
+ report("Agent hosts", false, message(err), "Install a supported host or select one with sessionmem doctor --adapter <name>");
54
+ }
55
+ const dbPath = resolveDbPath(overrides.dbPath);
56
+ let db;
57
+ let schemaOk = false;
58
+ try {
59
+ try {
60
+ if (dbPath === ":memory:")
61
+ throw new Error("No persistent memory store selected (:memory:)");
62
+ // fileMustExist prevents doctor from hiding a missing store; migrations
63
+ // are inspected separately and are never applied during diagnosis.
64
+ db = new BetterSqlite3(dbPath, { fileMustExist: true, timeout: 5000 });
65
+ const status = db.pragma("quick_check", { simple: true });
66
+ if (status !== "ok")
67
+ throw new Error(`SQLite quick_check: ${String(status)}`);
68
+ db.pragma("foreign_keys = ON");
69
+ report("Memory store", true, `Exists and opens (${dbPath})`);
70
+ }
71
+ catch (err) {
72
+ db?.close();
73
+ db = undefined;
74
+ report("Memory store", false, message(err), "Check the selected database path and directory permissions; initialize a missing store with sessionmem ping");
75
+ }
76
+ if (db) {
77
+ try {
78
+ const schema = inspectMigrations(db, overrides.migrationsDir);
79
+ if (schema.unknown.length) {
80
+ report("Schema", false, `Unrecognized migrations: ${schema.unknown.join(", ")}`, "Use the sessionmem version that created this database; do not downgrade its schema");
81
+ }
82
+ else if (schema.pending.length) {
83
+ report("Schema", false, `Pending migrations: ${schema.pending.join(", ")}`, "sessionmem ping");
84
+ }
85
+ else {
86
+ schemaOk = true;
87
+ report("Schema", true, "All bundled migrations are applied");
88
+ }
89
+ }
90
+ catch (err) {
91
+ report("Schema", false, message(err), "Check the database and bundled schema migrations; sessionmem ping applies pending migrations");
92
+ }
93
+ }
94
+ else {
95
+ report("Schema", false, "Cannot inspect an unavailable store", "sessionmem ping");
96
+ }
97
+ for (const adapter of adapters) {
98
+ const name = ADAPTER_NAMES.find((name) => AdapterFactory.forName(name).name === adapter.name);
99
+ const installFix = name ? `sessionmem install --adapter ${name}` : "sessionmem install";
100
+ const installation = adapter.installation?.();
101
+ if (!installation) {
102
+ report(`${adapter.name} hooks`, false, "Adapter has no installation diagnostics", installFix);
103
+ report(`${adapter.name} adapter wiring`, false, "Adapter has no installation diagnostics", installFix);
104
+ continue;
105
+ }
106
+ try {
107
+ report(`${adapter.name} hooks`, true, `${checkHooks(installation)}; sessionmem ${VERSION}`);
108
+ }
109
+ catch (err) {
110
+ report(`${adapter.name} hooks`, false, message(err), `Enable hooks in the host settings if disabled, then run ${installFix}`);
111
+ }
112
+ try {
113
+ const launch = readMcpLaunch(installation);
114
+ if (!schemaOk)
115
+ throw new Error("MCP launch probe blocked until the store and schema checks pass");
116
+ await probeMcpWiring(launch, dbPath, VERSION);
117
+ report(`${adapter.name} adapter wiring`, true, "Configured MCP command starts, matches this version, and answers stats");
118
+ }
119
+ catch (err) {
120
+ report(`${adapter.name} adapter wiring`, false, message(err), `Ensure sessionmem is on PATH, then run ${installFix}`);
121
+ }
122
+ }
123
+ if (!adapters.length) {
124
+ report("Hook configuration", false, "No host selected", "sessionmem doctor --adapter <name>");
125
+ report("Adapter wiring", false, "No host selected", "sessionmem doctor --adapter <name>");
126
+ }
127
+ if (db && schemaOk) {
128
+ try {
129
+ await probeRecall(db);
130
+ report("Recall canary", true, "Write and recall succeeded; canary rolled back");
131
+ }
132
+ catch (err) {
133
+ report("Recall canary", false, message(err), "Check database write permissions and errors; run sessionmem doctor again after repair");
134
+ }
135
+ }
136
+ else {
137
+ report("Recall canary", false, "Blocked until the store and schema checks pass", "sessionmem ping");
138
+ }
139
+ }
140
+ finally {
141
+ db?.close();
142
+ }
143
+ if (results.some((result) => !result.ok))
144
+ process.exitCode = 1;
145
+ return results;
146
+ }
@@ -0,0 +1,27 @@
1
+ import { listProjectKnowledge } from "../../core/storage/knowledgeRepo.js";
2
+ import { createCliContext } from "../context.js";
3
+ export function entitiesCommand(options = {}, ctx) {
4
+ const context = ctx ?? createCliContext();
5
+ try {
6
+ const projectId = options.project ?? context.projectId;
7
+ const knowledge = listProjectKnowledge(context.db, projectId);
8
+ if (options.json) {
9
+ console.log(JSON.stringify({ projectId, ...knowledge }, null, 2));
10
+ return;
11
+ }
12
+ console.log(`Entities for ${projectId}:`);
13
+ for (const entity of knowledge.entities) {
14
+ console.log(` [${entity.kind}] ${entity.name} (memories: ${entity.memoryIds.join(", ")})`);
15
+ }
16
+ console.log("Relations:");
17
+ for (const edge of knowledge.relations) {
18
+ console.log(` [${edge.sourceKind}] ${edge.sourceName} --${edge.relation}--> [${edge.targetKind}] ${edge.targetName} (memories: ${edge.memoryIds.join(", ")})`);
19
+ }
20
+ if (!knowledge.entities.length)
21
+ console.log(" No entities captured.");
22
+ }
23
+ finally {
24
+ if (!ctx)
25
+ context.db.close();
26
+ }
27
+ }
@@ -0,0 +1,23 @@
1
+ import { listSupersessions } from "../../core/storage/supersessionRepo.js";
2
+ import { createCliContext } from "../context.js";
3
+ export function historyCommand(options = {}, ctx) {
4
+ const context = ctx ?? createCliContext();
5
+ try {
6
+ const projectId = options.project ?? context.projectId;
7
+ const links = listSupersessions(context.db, projectId);
8
+ if (options.json) {
9
+ console.log(JSON.stringify({ projectId, links }, null, 2));
10
+ return;
11
+ }
12
+ console.log(`Supersession links for ${projectId}:`);
13
+ for (const link of links) {
14
+ console.log(` ${link.supersedes} -> ${link.supersededBy} at ${link.supersededAt} (${link.entity} ${link.relation}: ${link.oldValue} -> ${link.newValue}; ${link.reason}; ${link.fullySuperseded ? "superseded" : "partially superseded"})`);
15
+ }
16
+ if (!links.length)
17
+ console.log(" No supersessions captured.");
18
+ }
19
+ finally {
20
+ if (!ctx)
21
+ context.db.close();
22
+ }
23
+ }
@@ -1,26 +1,12 @@
1
1
  import { createCliContext } from "../context.js";
2
2
  import { randomUUID } from "node:crypto";
3
- // High-signal tool types that we capture as session events.
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
- if (!CAPTURED_TOOL_TYPES.has(toolName)) {
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
- eventType: `tool_use:${toolName.toLowerCase()}`,
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 { AdapterFactory } from "../../adapters/factory.js";
4
+ import { resolveInstallAdapters } from "../../adapters/installedHosts.js";
5
5
  import { injectGuidanceBlock } from "../../adapters/claudeMdInjector.js";
6
6
  import { createCliContext, resolveDbPath } 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
- }
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 adapter;
23
+ let adapters;
41
24
  let forced;
42
25
  try {
43
- ({ adapter, forced } = resolveAdapter(options));
26
+ ({ adapters, forced } = resolveInstallAdapters(options));
44
27
  }
45
28
  catch (err) {
46
29
  console.error(err instanceof Error ? err.message : String(err));
@@ -51,6 +34,8 @@ 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
41
  console.error(`✗ DB init failed (${resolveDbPath(contextOverrides?.dbPath)}): ${err instanceof Error ? err.message : String(err)}`);
@@ -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
- // Step 2: Adapter config — install into the resolved adapter. Print which
73
- // adapter was selected and how, so the user can see whether auto-detection
74
- // picked the host they expected (and re-run with `--adapter` if not).
75
- console.log(forced
76
- ? `→ Installing for adapter: ${adapter.name} (forced)`
77
- : `→ Detected host: ${adapter.name} (override with --adapter <name>)`);
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
- const injected = injectGuidanceBlock(target);
99
- if (injected) {
100
- console.log(`✓ Agent guidance injected (${target})`);
64
+ if (!adapter.install || !(await adapter.install())) {
65
+ throw new Error("config update failed");
101
66
  }
102
- else {
103
- console.error(`✗ Agent guidance injection failed (non-fatal): ${target}`);
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
- catch {
107
- console.error(`✗ Agent guidance injection failed (non-fatal): ${target}`);
86
+ if (adapter.name === "Claude Code") {
87
+ console.log("✓ Claude Code: SessionStart, SessionEnd, PostToolUse hooks configured (~/.claude/settings.json)");
108
88
  }
109
89
  }
110
- // Step 4: Full success checklist
111
- if (adapter.name === "Claude Code") {
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
  }
@@ -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
+ }
@@ -19,6 +19,7 @@ export async function searchCommand(query, options = {}, ctx) {
19
19
  limit,
20
20
  mode: "auto",
21
21
  depth: "default",
22
+ includeHistory: options.includeHistory,
22
23
  });
23
24
  if (!result.ok) {
24
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
- .run(context.projectId, context.username);
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
  }