sessionmem 1.6.0 → 1.8.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 (45) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +122 -61
  3. package/dist/adapters/claudeMdInjector.js +45 -67
  4. package/dist/adapters/diagnostics.js +29 -4
  5. package/dist/adapters/generic.js +31 -5
  6. package/dist/adapters/global/claudeCode.js +6 -3
  7. package/dist/adapters/ide/cline.js +2 -1
  8. package/dist/adapters/ide/cursor.js +3 -3
  9. package/dist/adapters/ide/installer.js +1 -0
  10. package/dist/adapters/installedHosts.js +4 -1
  11. package/dist/cli/autostart/index.js +246 -0
  12. package/dist/cli/commands/autostart.js +24 -0
  13. package/dist/cli/commands/config.js +8 -0
  14. package/dist/cli/commands/doctor.js +45 -2
  15. package/dist/cli/commands/install.js +14 -3
  16. package/dist/cli/commands/promptSubmit.js +58 -0
  17. package/dist/cli/commands/sessionStart.js +14 -2
  18. package/dist/cli/commands/uninstall.js +4 -0
  19. package/dist/cli/commands/watch.js +14 -2
  20. package/dist/cli/index.js +12 -3
  21. package/dist/cli/projectId.js +17 -2
  22. package/dist/cli/watchCheckpoint.js +35 -0
  23. package/dist/cli/watchLog.js +23 -0
  24. package/dist/core/api/contracts.js +19 -0
  25. package/dist/core/api/memoryCoreService.js +84 -21
  26. package/dist/core/config/policyConfig.js +13 -0
  27. package/dist/core/embed/localSemanticEmbedder.js +62 -0
  28. package/dist/core/retrieve/memoryIndex.js +6 -0
  29. package/dist/core/retrieve/retrieveMemories.js +57 -9
  30. package/dist/core/retrieve/signalFusion.js +21 -0
  31. package/dist/core/retrieve/temporalIntent.js +59 -0
  32. package/dist/core/schema/migrations/016_semantic_embeddings.sql +12 -0
  33. package/dist/core/storage/entityLinkRepo.js +47 -0
  34. package/dist/core/storage/memorySearchRepo.js +16 -3
  35. package/dist/core/storage/semanticSearchRepo.js +56 -0
  36. package/dist/core/storage/temporalRepo.js +13 -0
  37. package/dist/core/storage/timelineRepo.js +27 -0
  38. package/docs/architecture.md +13 -1
  39. package/docs/automatic-memory.md +86 -0
  40. package/docs/benchmark.md +16 -0
  41. package/docs/local-semantic.md +58 -0
  42. package/docs/research/automatic-capture.md +62 -0
  43. package/docs/retrieval-tools.md +144 -0
  44. package/docs/troubleshooting.md +5 -0
  45. package/package.json +21 -2
@@ -1,7 +1,7 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
- import { IDEInstaller, SESSIONMEM_HOOK_COMMAND, SESSIONMEM_SESSION_END_HOOK_COMMAND, SESSIONMEM_POST_TOOL_HOOK_COMMAND, SESSIONMEM_POST_TOOL_MATCHER, } from "../ide/installer.js";
4
+ import { IDEInstaller, SESSIONMEM_HOOK_COMMAND, SESSIONMEM_SESSION_END_HOOK_COMMAND, SESSIONMEM_POST_TOOL_HOOK_COMMAND, SESSIONMEM_POST_TOOL_MATCHER, SESSIONMEM_PROMPT_HOOK_COMMAND, } from "../ide/installer.js";
5
5
  export class ClaudeCodeAdapter extends GenericMCPAdapter {
6
6
  name = "Claude Code";
7
7
  // Capabilities inherited from GenericMCPAdapter (tools only) so the
@@ -33,6 +33,7 @@ export class ClaudeCodeAdapter extends GenericMCPAdapter {
33
33
  path: this.settingsPath(),
34
34
  entries: [
35
35
  { event: "SessionStart", command: SESSIONMEM_HOOK_COMMAND },
36
+ { event: "UserPromptSubmit", command: SESSIONMEM_PROMPT_HOOK_COMMAND },
36
37
  { event: "SessionEnd", command: SESSIONMEM_SESSION_END_HOOK_COMMAND },
37
38
  { event: "PostToolUse", command: SESSIONMEM_POST_TOOL_HOOK_COMMAND, matcher: SESSIONMEM_POST_TOOL_MATCHER },
38
39
  ],
@@ -63,7 +64,8 @@ export class ClaudeCodeAdapter extends GenericMCPAdapter {
63
64
  // MultiEdit) are captured as session events automatically, feeding the
64
65
  // SessionEnd auto-summarizer without the agent calling ingestSessionEvents.
65
66
  const postToolOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_POST_TOOL_HOOK_COMMAND, "PostToolUse", SESSIONMEM_POST_TOOL_MATCHER);
66
- return mcpOk && startHookOk && endHookOk && postToolOk;
67
+ const promptOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_PROMPT_HOOK_COMMAND, "UserPromptSubmit");
68
+ return mcpOk && startHookOk && endHookOk && postToolOk && promptOk;
67
69
  }
68
70
  async uninstall() {
69
71
  const configPath = join(homedir(), ".claude.json");
@@ -73,6 +75,7 @@ export class ClaudeCodeAdapter extends GenericMCPAdapter {
73
75
  const startOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_HOOK_COMMAND);
74
76
  const endOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
75
77
  const postToolOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_POST_TOOL_HOOK_COMMAND, "PostToolUse");
76
- return mcpOk && startOk && endOk && postToolOk;
78
+ const promptOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_PROMPT_HOOK_COMMAND, "UserPromptSubmit");
79
+ return mcpOk && startOk && endOk && postToolOk && promptOk;
77
80
  }
78
81
  }
@@ -2,6 +2,7 @@ import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { existsSync } from "fs";
4
4
  import { GenericMCPAdapter } from "../generic.js";
5
+ export const AUTOMATIC_MEMORY_TOOLS = ["searchMemoryIndex", "memoryTimeline", "getMemory", "retrieveMemories", "storeMemory", "batchStoreMemory", "ingestSessionEvents", "stats"];
5
6
  import { IDEInstaller } from "./installer.js";
6
7
  /**
7
8
  * VS Code variants Cline can be installed into, in detection-priority order.
@@ -57,7 +58,7 @@ export class ClineAdapter extends GenericMCPAdapter {
57
58
  // Cline's server schema carries `disabled` and `autoApprove` alongside
58
59
  // command/args; pass them through so the written block matches what Cline
59
60
  // expects to read.
60
- return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"], { disabled: false, autoApprove: [] });
61
+ return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"], { disabled: false, autoApprove: AUTOMATIC_MEMORY_TOOLS });
61
62
  }
62
63
  installation() {
63
64
  return { mcp: { path: this.configPath, command: "sessionmem", args: ["run"] } };
@@ -6,12 +6,12 @@ export class CursorAdapter extends GenericMCPAdapter {
6
6
  name = "Cursor";
7
7
  // Capabilities inherited from GenericMCPAdapter (tools only).
8
8
  /**
9
- * Cursor reads project rules from `.cursor/rules/*.mdc`. A home-level rule
10
- * file gives every project the sessionmem guidance. The injector seeds
9
+ * Cursor reads project rules from `.cursor/rules/*.mdc`. Install in this
10
+ * project; global MCP server instructions cover other projects. The injector seeds
11
11
  * `alwaysApply: true` frontmatter on creation so the rule is always active.
12
12
  */
13
13
  guidanceTargets() {
14
- return [join(homedir(), ".cursor", "rules", "sessionmem.mdc")];
14
+ return [join(process.cwd(), ".cursor", "rules", "sessionmem.mdc")];
15
15
  }
16
16
  /**
17
17
  * Cursor reads MCP servers from `~/.cursor/mcp.json` (global) — NOT from the
@@ -31,6 +31,7 @@ export const SESSIONMEM_SESSION_END_HOOK_COMMAND = _isWindows
31
31
  export const SESSIONMEM_POST_TOOL_HOOK_COMMAND = _isWindows
32
32
  ? "cmd /c sessionmem ingest-hook"
33
33
  : "sessionmem ingest-hook";
34
+ export const SESSIONMEM_PROMPT_HOOK_COMMAND = _isWindows ? "cmd /c sessionmem prompt-submit" : "sessionmem prompt-submit";
34
35
  /** Tool matcher for the PostToolUse auto-ingest hook. */
35
36
  export const SESSIONMEM_POST_TOOL_MATCHER = "Bash|Edit|Write|MultiEdit";
36
37
  /** Default Claude Code hook event for the legacy 2-arg hook helpers. */
@@ -5,6 +5,9 @@ import { AdapterFactory } from "./factory.js";
5
5
  // Installed-host detection is deliberately separate from runtime session-env
6
6
  // detection. Add a row here when another host has a verified install probe.
7
7
  export const INSTALLED_HOSTS = [
8
+ { adapter: "codex", executables: ["codex"] },
9
+ { adapter: "cursor", executables: ["cursor"] },
10
+ { adapter: "windsurf", executables: ["windsurf"] },
8
11
  {
9
12
  adapter: "claude-code",
10
13
  executables: ["claude"],
@@ -45,7 +48,7 @@ export function resolveInstallAdapters(options) {
45
48
  return { adapters: [AdapterFactory.forName(explicit)], forced: true };
46
49
  const adapters = detectInstalledAdapters();
47
50
  if (adapters.length === 0) {
48
- throw new Error("No supported agent host found. Install Claude Code or use sessionmem install --adapter <name>.");
51
+ throw new Error("No supported agent host found. Install a supported host CLI or use sessionmem install --adapter <name>.");
49
52
  }
50
53
  return { adapters, forced: false };
51
54
  }
@@ -0,0 +1,246 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { homedir, userInfo } from "os";
5
+ import { dirname, isAbsolute, posix, win32 } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { resolveDbPath } from "../context.js";
8
+ export const LABEL = "io.sessionmem.watch";
9
+ export const UNIT = "sessionmem-watch.service";
10
+ export const xml = (value) => value.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
11
+ const unxml = (value) => value.replace(/&(amp|lt|gt|quot|apos);/g, c => ({ "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&apos;": "'" })[c]);
12
+ const systemdQuote = (value) => JSON.stringify(value).replace(/%/g, "%%").replace(/\$/g, () => "$$");
13
+ // All generated Windows arguments are quoted. Double backslashes before quotes
14
+ // and at the closing quote according to CommandLineToArgvW rules.
15
+ export const windowsQuote = (value) => `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
16
+ export function renderPlist(launch, log, home) {
17
+ return `<?xml version="1.0" encoding="UTF-8"?>
18
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19
+ <plist version="1.0"><dict>
20
+ <key>Label</key><string>${LABEL}</string>
21
+ <key>ProgramArguments</key><array>${[launch.command, ...launch.args].map(v => `<string>${xml(v)}</string>`).join("")}</array>
22
+ <key>WorkingDirectory</key><string>${xml(home)}</string>
23
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
24
+ <key>ThrottleInterval</key><integer>10</integer>
25
+ <key>StandardOutPath</key><string>${xml(log)}</string>
26
+ <key>StandardErrorPath</key><string>${xml(log)}</string>
27
+ </dict></plist>
28
+ `;
29
+ }
30
+ export function renderSystemd(launch, log, home) {
31
+ return `[Unit]
32
+ Description=Capture agent transcripts with sessionmem
33
+ StartLimitIntervalSec=0
34
+
35
+ [Service]
36
+ Type=simple
37
+ ExecStart=${[launch.command, ...launch.args].map(systemdQuote).join(" ")}
38
+ WorkingDirectory=${home.replace(/%/g, "%%")}
39
+ Restart=always
40
+ RestartSec=10
41
+ StandardInput=null
42
+ StandardOutput=append:${log.replace(/%/g, "%%")}
43
+ StandardError=append:${log.replace(/%/g, "%%")}
44
+
45
+ [Install]
46
+ WantedBy=default.target
47
+ `;
48
+ }
49
+ export function renderTask(launch, home, user) {
50
+ return `<?xml version="1.0" encoding="UTF-8"?>
51
+ <Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
52
+ <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml(user)}</UserId></LogonTrigger></Triggers>
53
+ <Principals><Principal id="Author"><UserId>${xml(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
54
+ <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>255</Count></RestartOnFailure></Settings>
55
+ <Actions Context="Author"><Exec><Command>${xml(launch.command)}</Command><Arguments>${xml(launch.args.map(windowsQuote).join(" "))}</Arguments><WorkingDirectory>${xml(home)}</WorkingDirectory></Exec></Actions>
56
+ </Task>
57
+ `;
58
+ }
59
+ export function decodeCommandOutput(value) {
60
+ // schtasks XML may use UTF-16 even when stdout is a pipe.
61
+ return (value[0] === 0xff && value[1] === 0xfe) || value[1] === 0
62
+ ? value.toString("utf16le").replace(/^\uFEFF/, "") : value.toString("utf8").replace(/^\uFEFF/, "");
63
+ }
64
+ export const runCommand = (command, args) => {
65
+ const result = spawnSync(command, args, { windowsHide: true, timeout: 15000, stdio: ["ignore", "pipe", "pipe"] });
66
+ if (result.error)
67
+ throw new Error(`${command}: ${result.error.message}`);
68
+ return { status: result.status ?? 1, stdout: decodeCommandOutput(result.stdout), stderr: decodeCommandOutput(result.stderr) };
69
+ };
70
+ function checked(run, command, args) {
71
+ const result = run(command, args);
72
+ if (result.status !== 0)
73
+ throw new Error(`${command} ${args.join(" ")}: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
74
+ return result;
75
+ }
76
+ function save(path, content) {
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ writeFileSync(path, content, { mode: 0o600 });
79
+ }
80
+ function strings(value) {
81
+ return [...value.matchAll(/<string>([\s\S]*?)<\/string>/g)].map(m => unxml(m[1]));
82
+ }
83
+ function launchFromArgs(args) {
84
+ if (args.length < 3 || args[2] !== "watch")
85
+ throw new Error("Unrecognized watcher command; run sessionmem autostart enable");
86
+ return { command: args[0], args: args.slice(1) };
87
+ }
88
+ export function invalidLaunchPaths(launch) {
89
+ return [launch.command, launch.args[0]].filter((path, index) => {
90
+ try {
91
+ if (!path || (!isAbsolute(path) && !win32.isAbsolute(path)) || !statSync(path).isFile())
92
+ return true;
93
+ accessSync(path, index === 0 && process.platform !== "win32" ? constants.X_OK : constants.R_OK);
94
+ return false;
95
+ }
96
+ catch {
97
+ return true;
98
+ }
99
+ });
100
+ }
101
+ export function createAutostart(options = {}) {
102
+ const platform = options.platform ?? process.platform;
103
+ const home = options.home ?? homedir();
104
+ const paths = platform === "win32" ? win32 : posix;
105
+ const selectedDb = options.dbPath ?? resolveDbPath();
106
+ if (selectedDb === ":memory:")
107
+ throw new Error("Login capture requires a persistent database");
108
+ const dbPath = paths.resolve(selectedDb);
109
+ const data = paths.dirname(dbPath);
110
+ const log = paths.join(data, "logs", "watch.log");
111
+ // npm's sessionmem binary is a JS entry point. Pin both it and Node, avoiding
112
+ // /usr/bin/env, npm shims and interactive shell/version-manager setup at login.
113
+ const launch = {
114
+ command: paths.resolve(options.nodePath ?? process.execPath),
115
+ args: [paths.resolve(options.cliPath ?? fileURLToPath(new URL("../index.js", import.meta.url))), "watch", "--db-path", dbPath, "--log-file", log],
116
+ };
117
+ const run = options.run ?? runCommand;
118
+ const invoke = (command, args) => checked(run, command, args);
119
+ const prepare = () => {
120
+ if ([home, launch.command, ...launch.args].some(value => /[\r\n\0]/.test(value)))
121
+ throw new Error("Login service paths cannot contain line breaks or NUL");
122
+ const invalid = invalidLaunchPaths(launch);
123
+ if (invalid.length)
124
+ throw new Error(`Cannot read or execute installed binary: ${invalid.join(", ")}`);
125
+ mkdirSync(paths.dirname(log), { recursive: true });
126
+ };
127
+ if (platform === "darwin") {
128
+ const file = paths.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
129
+ const domain = `gui/${options.uid ?? userInfo().uid}`;
130
+ const target = `${domain}/${LABEL}`;
131
+ const loaded = () => {
132
+ const result = run("launchctl", ["print", target]);
133
+ if (result.status === 0)
134
+ return true;
135
+ if (/could not find service|no such process/i.test(result.stderr))
136
+ return false;
137
+ throw new Error(`launchctl print: ${result.stderr || result.stdout}`);
138
+ };
139
+ return {
140
+ enable() {
141
+ prepare();
142
+ if (loaded())
143
+ invoke("launchctl", ["bootout", target]);
144
+ save(file, renderPlist(launch, log, home));
145
+ invoke("launchctl", ["enable", target]);
146
+ invoke("launchctl", ["bootstrap", domain, file]);
147
+ },
148
+ disable() {
149
+ if (loaded())
150
+ invoke("launchctl", ["bootout", target]);
151
+ rmSync(file, { force: true });
152
+ },
153
+ status() {
154
+ if (!existsSync(file))
155
+ return { registered: false, detail: `No LaunchAgent (${file})` };
156
+ const content = readFileSync(file, "utf8");
157
+ const args = strings(content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "");
158
+ return { registered: loaded(), launch: launchFromArgs(args), detail: file };
159
+ },
160
+ };
161
+ }
162
+ if (platform === "linux") {
163
+ const file = paths.join(home, ".config", "systemd", "user", UNIT);
164
+ return {
165
+ enable() {
166
+ prepare();
167
+ save(file, renderSystemd(launch, log, home));
168
+ invoke("systemctl", ["--user", "daemon-reload"]);
169
+ invoke("systemctl", ["--user", "enable", UNIT]);
170
+ invoke("systemctl", ["--user", "restart", UNIT]);
171
+ },
172
+ disable() {
173
+ if (!existsSync(file))
174
+ return;
175
+ invoke("systemctl", ["--user", "disable", "--now", UNIT]);
176
+ rmSync(file, { force: true });
177
+ invoke("systemctl", ["--user", "daemon-reload"]);
178
+ },
179
+ status() {
180
+ if (!existsSync(file))
181
+ return { registered: false, detail: `No user service (${file})` };
182
+ const value = readFileSync(file, "utf8").match(/^ExecStart=(.+)$/m)?.[1] ?? "";
183
+ const args = [...value.matchAll(/"(?:\\.|[^"\\])*"/g)].map(m => JSON.parse(m[0]).replace(/%%/g, "%").replace(/\$\$/g, "$"));
184
+ const result = run("systemctl", ["--user", "is-enabled", UNIT]);
185
+ if (result.status !== 0 && !["disabled", "masked", "not-found"].includes(result.stdout.trim()))
186
+ throw new Error(`systemctl --user is-enabled: ${result.stderr || result.stdout}`);
187
+ return { registered: result.status === 0, launch: launchFromArgs(args), detail: `${file} (${result.stdout.trim()})` };
188
+ },
189
+ };
190
+ }
191
+ if (platform === "win32") {
192
+ // Task names are machine-wide; one stable name per user's home avoids
193
+ // overwriting another user's task on shared Windows machines.
194
+ const name = `sessionmem-watch-${createHash("sha256").update(home.toLowerCase()).digest("hex").slice(0, 12)}`;
195
+ const file = paths.join(data, "autostart-task.xml");
196
+ const query = () => {
197
+ const result = run("schtasks.exe", ["/Query", "/TN", name, "/XML", "/HRESULT"]);
198
+ if ((result.status >>> 0) === 0x80070002)
199
+ return undefined; // ERROR_FILE_NOT_FOUND
200
+ if (result.status !== 0)
201
+ throw new Error(`schtasks /Query: ${result.stderr || result.stdout || result.status}`);
202
+ return result.stdout;
203
+ };
204
+ const stop = () => {
205
+ const result = run("schtasks.exe", ["/End", "/TN", name, "/HRESULT"]);
206
+ // Numeric HRESULTs avoid matching localized Windows error messages.
207
+ if (result.status !== 0 && (result.status >>> 0) !== 0x8004130b)
208
+ throw new Error(`schtasks /End: ${result.stderr || result.stdout || result.status}`);
209
+ };
210
+ return {
211
+ enable() {
212
+ prepare();
213
+ const identity = invoke("whoami.exe", ["/USER", "/FO", "CSV", "/NH"]).stdout;
214
+ const user = identity.match(/"(S-1-[0-9-]+)"/)?.[1];
215
+ if (!user)
216
+ throw new Error("whoami returned no user SID for the logon task");
217
+ if (query() !== undefined)
218
+ stop();
219
+ save(file, renderTask(launch, home, user));
220
+ invoke("schtasks.exe", ["/Create", "/TN", name, "/XML", file, "/F"]);
221
+ invoke("schtasks.exe", ["/Run", "/TN", name]);
222
+ },
223
+ disable() {
224
+ if (query() !== undefined) {
225
+ stop();
226
+ invoke("schtasks.exe", ["/Delete", "/TN", name, "/F"]);
227
+ }
228
+ rmSync(file, { force: true });
229
+ },
230
+ status() {
231
+ const content = query();
232
+ if (content === undefined)
233
+ return { registered: false, detail: `No Scheduled Task (${name})` };
234
+ const command = unxml(content.match(/<Command>([\s\S]*?)<\/Command>/)?.[1] ?? "");
235
+ const args = unxml(content.match(/<Arguments>([\s\S]*?)<\/Arguments>/)?.[1] ?? "");
236
+ // The first argument is an absolute .js path, which cannot contain quotes
237
+ // on Windows. The remaining arguments are only needed for display.
238
+ const cli = args.match(/^"([^"]+)"\s+"watch"(?:\s|$)/)?.[1];
239
+ if (!command || !cli)
240
+ throw new Error("Unrecognized Scheduled Task action; run sessionmem autostart enable");
241
+ return { registered: !/<Enabled>false<\/Enabled>/.test(content), launch: { command, args: [cli, "watch"] }, detail: name };
242
+ },
243
+ };
244
+ }
245
+ throw new Error(`Login auto-start is unsupported on ${platform}`);
246
+ }
@@ -0,0 +1,24 @@
1
+ import { createAutostart, invalidLaunchPaths } from "../autostart/index.js";
2
+ export function autostartCommand(action, service) {
3
+ if (!["enable", "disable", "status"].includes(action))
4
+ throw new Error("Use sessionmem autostart enable, disable, or status");
5
+ const autostart = service ?? createAutostart();
6
+ if (action === "status") {
7
+ const status = autostart.status();
8
+ console.log(`Auto-start ${status.registered ? "enabled" : "disabled"}: ${status.detail}`);
9
+ if (status.launch) {
10
+ console.log(`Binary: ${status.launch.command}\nEntry point: ${status.launch.args[0]}`);
11
+ const invalid = invalidLaunchPaths(status.launch);
12
+ if (invalid.length) {
13
+ console.error(`Invalid binary path: ${invalid.join(", ")}. Run sessionmem autostart enable.`);
14
+ process.exitCode = 1;
15
+ }
16
+ }
17
+ return;
18
+ }
19
+ if (action === "enable")
20
+ autostart.enable();
21
+ else
22
+ autostart.disable();
23
+ console.log(`Auto-start ${action === "enable" ? "enabled and started" : "disabled and stopped"}`);
24
+ }
@@ -42,6 +42,14 @@ const CONFIG_KEYS = {
42
42
  retentionDays: { field: "retentionDays", coerce: coerceRetentionDays },
43
43
  redactionEnabled: { field: "redactionEnabled", coerce: coerceBool },
44
44
  injectionCap: { field: "injectionCap", coerce: coerceInjectionCap },
45
+ semanticSearch: { field: "semanticSearch", coerce: coerceBool },
46
+ temporalRetrieval: { field: "temporalRetrieval", coerce: coerceBool },
47
+ entityLinkBoost: { field: "entityLinkBoost", coerce: coerceBool },
48
+ retrievalRanking: { field: "retrievalRanking", coerce: raw => {
49
+ if (!["legacy", "adaptive-bm25", "hub-additive", "adaptive-hub-additive"].includes(raw))
50
+ throw new Error("Unknown retrieval ranking");
51
+ return raw;
52
+ } },
45
53
  };
46
54
  function resolvePath(options) {
47
55
  return options?.configPath ?? configFilePath();
@@ -1,8 +1,10 @@
1
+ import { createAutostart, invalidLaunchPaths } from "../autostart/index.js";
2
+ import { inspectWatchCheckpoint } from "../watchCheckpoint.js";
1
3
  import BetterSqlite3 from "better-sqlite3";
2
4
  import { randomUUID } from "node:crypto";
3
5
  import { createRequire } from "node:module";
4
6
  import { resolveInstallAdapters } from "../../adapters/installedHosts.js";
5
- import { checkHooks, readMcpLaunch, probeMcpWiring } from "../../adapters/diagnostics.js";
7
+ import { checkHooks, checkAgentGuidance, readMcpLaunch, probeMcpWiring } from "../../adapters/diagnostics.js";
6
8
  import { ADAPTER_NAMES, AdapterFactory } from "../../adapters/factory.js";
7
9
  import { createMemoryCoreService } from "../../core/api/memoryCoreService.js";
8
10
  import { inspectMigrations } from "../../core/schema/runMigrations.js";
@@ -54,12 +56,33 @@ export async function doctorCommand(options, overrides = {}) {
54
56
  report("Agent hosts", false, message(err), "Install a supported host or select one with sessionmem doctor --adapter <name>");
55
57
  }
56
58
  const dbPath = resolveDbPath(overrides.dbPath);
59
+ let autostartHealthy = false;
60
+ const captureFix = "Run sessionmem autostart enable; inspect logs/watch.log beside the database and OS service logs, then run sessionmem doctor";
61
+ try {
62
+ const registration = createAutostart({ dbPath }).status();
63
+ report("Auto-start registration", registration.registered, registration.detail, registration.registered ? undefined : captureFix);
64
+ const invalid = registration.launch ? invalidLaunchPaths(registration.launch) : ["No registered binary"];
65
+ autostartHealthy = registration.registered && invalid.length === 0;
66
+ report("Auto-start binary", invalid.length === 0, invalid.length ? invalid.join(", ") : "Registered Node binary and sessionmem entry point are readable", invalid.length ? "Reinstall sessionmem if needed, then run sessionmem autostart enable" : undefined);
67
+ }
68
+ catch (err) {
69
+ report("Auto-start registration", false, message(err), captureFix);
70
+ report("Auto-start binary", false, "Cannot inspect the registration", captureFix);
71
+ }
72
+ let watcherHealthy = false;
73
+ try {
74
+ report("Watcher capture", true, inspectWatchCheckpoint(dbPath));
75
+ watcherHealthy = true;
76
+ }
77
+ catch (err) {
78
+ report("Watcher capture", false, message(err), captureFix);
79
+ }
57
80
  for (const host of TRANSCRIPT_HOSTS) {
58
81
  try {
59
82
  report(`${host} transcripts`, true, await inspectTranscriptDiscovery(host));
60
83
  }
61
84
  catch (err) {
62
- report(`${host} transcripts`, false, message(err), `Check transcript directory permissions and format; run sessionmem scan --host ${host}`);
85
+ report(`${host} transcripts`, false, message(err), `Check transcript directory permissions and format; check the watcher logs; imports retry automatically`);
63
86
  }
64
87
  }
65
88
  let db;
@@ -112,8 +135,10 @@ export async function doctorCommand(options, overrides = {}) {
112
135
  report(`${adapter.name} adapter wiring`, false, "Adapter has no installation diagnostics", installFix);
113
136
  continue;
114
137
  }
138
+ let hooksHealthy = false, wiringHealthy = false, guidanceHealthy = false;
115
139
  try {
116
140
  report(`${adapter.name} hooks`, true, `${checkHooks(installation)}; sessionmem ${VERSION}`);
141
+ hooksHealthy = Boolean(installation.hooks);
117
142
  }
118
143
  catch (err) {
119
144
  report(`${adapter.name} hooks`, false, message(err), `Enable hooks in the host settings if disabled, then run ${installFix}`);
@@ -124,10 +149,28 @@ export async function doctorCommand(options, overrides = {}) {
124
149
  throw new Error("MCP launch probe blocked until the store and schema checks pass");
125
150
  await probeMcpWiring(launch, dbPath, VERSION);
126
151
  report(`${adapter.name} adapter wiring`, true, "Configured MCP command starts, matches this version, and answers stats");
152
+ wiringHealthy = true;
127
153
  }
128
154
  catch (err) {
129
155
  report(`${adapter.name} adapter wiring`, false, message(err), `Ensure sessionmem is on PATH, then run ${installFix}`);
130
156
  }
157
+ try {
158
+ report(`${adapter.name} agent instructions`, true, checkAgentGuidance(adapter));
159
+ guidanceHealthy = true;
160
+ }
161
+ catch (err) {
162
+ report(`${adapter.name} agent instructions`, false, message(err), installFix);
163
+ }
164
+ const native = Boolean(installation.hooks);
165
+ const transcript = ["Claude Code", "Codex", "Cursor"].includes(adapter.name);
166
+ const unverifiedHost = ["Antigravity", "QCoder"].includes(adapter.name);
167
+ const capture = !unverifiedHost && schemaOk && (native ? hooksHealthy || (autostartHealthy && watcherHealthy) : transcript ? autostartHealthy && watcherHealthy : guidanceHealthy && wiringHealthy);
168
+ report(`${adapter.name} automatic capture`, capture, unverifiedHost ? "Native adapter paths are unverified; automatic capture is not established" : native ? "Hook capture and transcript watcher configured; see hook and watcher checks"
169
+ : transcript ? "Login watcher imports supported transcripts; a healthy sweep is required"
170
+ : "Agent-driven saves only; no background transcript importer for this host", capture ? undefined : unverifiedHost ? "Verify the native host MCP and guidance paths in its current settings, or select a verified host adapter" : transcript ? captureFix : `Enable memory tool calls and load the instructions from ${adapter.guidanceTargets?.().join(", ")}; ${installFix}`);
171
+ const recall = !unverifiedHost && schemaOk && wiringHealthy && (native ? hooksHealthy : guidanceHealthy);
172
+ report(`${adapter.name} automatic recall`, recall, unverifiedHost ? "Native host loading is unverified; automatic recall is not established" : native ? "SessionStart and UserPromptSubmit hooks inject context"
173
+ : "Instruction-driven startup and prompt recall configured; agent execution is not verified by doctor", recall ? undefined : unverifiedHost ? "Configure the MCP server and startup instructions in the native host settings; these adapter paths are unverified" : `Enable memory tools, load the installed instructions and run ${installFix}`);
131
174
  }
132
175
  if (!adapters.length) {
133
176
  report("Hook configuration", false, "No host selected", "sessionmem doctor --adapter <name>");
@@ -1,3 +1,4 @@
1
+ import { createAutostart } from "../autostart/index.js";
1
2
  import { existsSync } from "fs";
2
3
  import { join } from "path";
3
4
  import { homedir } from "os";
@@ -80,14 +81,24 @@ export async function installCommand(options, contextOverrides) {
80
81
  console.log(`✓ ${adapter.name}: Agent guidance injected (${target})`);
81
82
  }
82
83
  catch {
83
- console.error(`✗ ${adapter.name}: Agent guidance injection failed (non-fatal): ${target}`);
84
+ console.error(`✗ ${adapter.name}: Agent guidance injection failed; automatic recall is not configured: ${target}`);
85
+ failed = true;
84
86
  }
85
87
  }
86
88
  if (adapter.name === "Claude Code") {
87
- console.log("✓ Claude Code: SessionStart, SessionEnd, PostToolUse hooks configured (~/.claude/settings.json)");
89
+ console.log("✓ Claude Code: SessionStart, UserPromptSubmit, SessionEnd, PostToolUse hooks configured (~/.claude/settings.json)");
88
90
  }
89
91
  }
92
+ try {
93
+ createAutostart({ dbPath: dbPath }).enable();
94
+ console.log("Auto-start enabled and started; capture logs are in logs/watch.log beside the database");
95
+ }
96
+ catch (err) {
97
+ console.error(`Auto-start setup failed: ${err instanceof Error ? err.message : String(err)}`);
98
+ console.error("Fix the OS login service error, then run sessionmem autostart enable. Run sessionmem doctor to check capture.");
99
+ failed = true;
100
+ }
90
101
  if (failed)
91
102
  process.exit(1);
92
- console.log("✓ sessionmem ready — restart your editor/agent so it picks up the MCP server");
103
+ console.log("✓ sessionmem ready; restart your editor/agent so it picks up the MCP server");
93
104
  }
@@ -0,0 +1,58 @@
1
+ import { createCliContext } from "../context.js";
2
+ import { deriveProjectId } from "../projectId.js";
3
+ /** Bounded hook input. Invalid input produces no context and never blocks a prompt. */
4
+ export async function promptSubmitCommand(ctx, supplied) {
5
+ let context;
6
+ try {
7
+ let payload = supplied;
8
+ if (payload === undefined) {
9
+ payload = await new Promise(resolve => {
10
+ let text = "", finished = false;
11
+ const done = () => {
12
+ if (finished)
13
+ return;
14
+ finished = true;
15
+ clearTimeout(timer);
16
+ process.stdin.removeListener("data", data);
17
+ process.stdin.removeListener("end", done);
18
+ process.stdin.removeListener("error", done);
19
+ process.stdin.pause();
20
+ try {
21
+ resolve(JSON.parse(text));
22
+ }
23
+ catch {
24
+ resolve({});
25
+ }
26
+ };
27
+ const data = (chunk) => { text += chunk; if (text.length > 65536) {
28
+ text = "";
29
+ done();
30
+ } };
31
+ const timer = setTimeout(done, 500);
32
+ timer.unref();
33
+ process.stdin.setEncoding("utf8");
34
+ process.stdin.on("data", data);
35
+ process.stdin.on("end", done);
36
+ process.stdin.on("error", done);
37
+ });
38
+ }
39
+ if (!payload || typeof payload !== "object")
40
+ return;
41
+ const { prompt, cwd } = payload;
42
+ if (typeof prompt !== "string" || !prompt.trim())
43
+ return;
44
+ context = ctx ?? createCliContext(typeof cwd === "string" ? { projectId: deriveProjectId(cwd) } : {});
45
+ const result = await context.service.call("retrieveMemories", {
46
+ projectId: context.projectId, query: prompt.slice(0, 1000), limit: 5, mode: "on-demand", depth: "default",
47
+ });
48
+ if (result.ok && result.startupInjection.trim())
49
+ process.stdout.write(JSON.stringify({
50
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: result.startupInjection },
51
+ }));
52
+ }
53
+ catch { /* Hook failures do not interrupt the host. Doctor diagnoses the setup. */ }
54
+ finally {
55
+ if (!ctx)
56
+ context?.db.close();
57
+ }
58
+ }
@@ -1,3 +1,4 @@
1
+ import { sweepTranscripts } from "../transcriptSweep.js";
1
2
  import { createCliContext } from "../context.js";
2
3
  /**
3
4
  * Generic startup query. When few memories contain these literal tokens the
@@ -25,8 +26,15 @@ const STARTUP_QUERY = "session startup context recent decisions architecture war
25
26
  * starts clean.
26
27
  */
27
28
  export async function sessionStartCommand(ctx) {
29
+ let context;
28
30
  try {
29
- const context = ctx ?? createCliContext();
31
+ context = ctx ?? createCliContext();
32
+ if (!ctx) {
33
+ try {
34
+ await sweepTranscripts(context, {});
35
+ }
36
+ catch { /* Existing recall still works when transcript discovery fails. */ }
37
+ }
30
38
  const result = await context.service.call("retrieveMemories", {
31
39
  projectId: context.projectId,
32
40
  query: STARTUP_QUERY,
@@ -47,6 +55,10 @@ export async function sessionStartCommand(ctx) {
47
55
  }
48
56
  }
49
57
  catch {
50
- // Never block session start — emit nothing on any failure.
58
+ // Never block session start.
59
+ }
60
+ finally {
61
+ if (!ctx)
62
+ context?.db.close();
51
63
  }
52
64
  }
@@ -1,3 +1,4 @@
1
+ import { createAutostart } from "../autostart/index.js";
1
2
  import { existsSync, rmSync } from "fs";
2
3
  import { join } from "path";
3
4
  import { homedir } from "os";
@@ -28,6 +29,9 @@ export async function uninstallCommand(options = {}) {
28
29
  console.error(err instanceof Error ? err.message : String(err));
29
30
  process.exit(1);
30
31
  }
32
+ // Stop capture before removing integrations or purging an open database.
33
+ createAutostart({ dbPath: options.dbPath }).disable();
34
+ console.log("Auto-start removed and watcher stopped");
31
35
  if (!adapter.uninstall) {
32
36
  console.error(`${adapter.name} does not support automated uninstall. Remove sessionmem from your MCP config manually.`);
33
37
  process.exit(1);