sessionmem 1.0.5 → 1.1.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 (58) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +372 -365
  3. package/dist/adapters/capabilities/fallbackTools.js +33 -18
  4. package/dist/adapters/claudeMdInjector.js +164 -0
  5. package/dist/adapters/factory.js +68 -9
  6. package/dist/adapters/generic.js +221 -15
  7. package/dist/adapters/global/antigravity.js +14 -7
  8. package/dist/adapters/global/claudeCode.js +46 -10
  9. package/dist/adapters/global/codex.js +73 -13
  10. package/dist/adapters/global/qcoder.js +18 -5
  11. package/dist/adapters/ide/cline.js +54 -9
  12. package/dist/adapters/ide/cursor.js +15 -13
  13. package/dist/adapters/ide/installer.js +201 -8
  14. package/dist/adapters/ide/windsurf.js +14 -13
  15. package/dist/adapters/tools/ping.js +4 -1
  16. package/dist/cli/commands/config.js +10 -1
  17. package/dist/cli/commands/import.js +6 -1
  18. package/dist/cli/commands/install.js +63 -5
  19. package/dist/cli/commands/ping.js +42 -8
  20. package/dist/cli/commands/reEmbed.js +48 -0
  21. package/dist/cli/commands/run.js +18 -2
  22. package/dist/cli/commands/savings.js +91 -0
  23. package/dist/cli/commands/sessionEnd.js +124 -0
  24. package/dist/cli/commands/sessionStart.js +52 -0
  25. package/dist/cli/commands/sync.js +39 -9
  26. package/dist/cli/commands/uninstall.js +37 -1
  27. package/dist/cli/context.js +14 -18
  28. package/dist/cli/index.js +30 -4
  29. package/dist/cli/output.js +11 -3
  30. package/dist/cli/projectId.js +69 -0
  31. package/dist/core/api/contracts.js +182 -45
  32. package/dist/core/api/errors.js +4 -7
  33. package/dist/core/api/memoryCoreService.js +409 -240
  34. package/dist/core/api/sessionLifecycleService.js +20 -2
  35. package/dist/core/config/policyConfig.js +53 -6
  36. package/dist/core/injection/formatStartupInjection.js +55 -10
  37. package/dist/core/injection/tokenBudget.js +8 -0
  38. package/dist/core/retrieve/importance.js +4 -3
  39. package/dist/core/retrieve/recencyBands.js +6 -10
  40. package/dist/core/retrieve/retrieveMemories.js +19 -4
  41. package/dist/core/retrieve/score.js +11 -1
  42. package/dist/core/schema/migrations/005_team_provenance.sql +14 -9
  43. package/dist/core/schema/migrations/006_access_pattern_boosting.sql +10 -0
  44. package/dist/core/schema/migrations/007_feedback_manual_delete.sql +23 -0
  45. package/dist/core/schema/migrations/008_fts5_search.sql +37 -0
  46. package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
  47. package/dist/core/schema/runMigrations.js +64 -2
  48. package/dist/core/storage/db.js +6 -0
  49. package/dist/core/storage/memoryFeedbackRepo.js +14 -4
  50. package/dist/core/storage/memoryRepo.js +292 -121
  51. package/dist/core/storage/memorySearchRepo.js +125 -13
  52. package/dist/core/storage/sessionEventsRepo.js +33 -10
  53. package/dist/core/storage/summarizationFailuresRepo.js +36 -26
  54. package/dist/core/storage/tokenSavingsRepo.js +20 -0
  55. package/dist/core/summarize/cloudSummarizer.js +34 -5
  56. package/dist/core/summarize/localSummarizer.js +1 -10
  57. package/dist/core/summarize/redaction.js +45 -8
  58. package/package.json +50 -48
@@ -2,21 +2,28 @@ import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
4
  import { IDEInstaller } from "../ide/installer.js";
5
+ // ⚠️ UNVERIFIED: The MCP config path for "Antigravity" has not been confirmed
6
+ // against official documentation. `~/.gemini/config/mcp_config.json` is the
7
+ // real path for Gemini CLI, and may apply if Antigravity is a Gemini-family
8
+ // tool. Verify before relying on this adapter for production installs.
9
+ // The `{ "mcpServers": { ... } }` structure IDEInstaller writes is consistent
10
+ // with Gemini CLI's documented MCP config format.
11
+ const ANTIGRAVITY_MCP_CONFIG = [".gemini", "config", "mcp_config.json"];
5
12
  export class AntigravityAdapter extends GenericMCPAdapter {
6
13
  name = "Antigravity";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
14
+ // Capabilities inherited from GenericMCPAdapter (tools only).
15
+ /** Antigravity reads AGENTS.md-style guidance from its global config dir. */
16
+ guidanceTargets() {
17
+ return [join(homedir(), ".antigravity", "AGENTS.md")];
18
+ }
12
19
  async install() {
13
- const configPath = join(homedir(), ".antigravity", "config.json");
20
+ const configPath = join(homedir(), ...ANTIGRAVITY_MCP_CONFIG);
14
21
  return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
15
22
  "run",
16
23
  ]);
17
24
  }
18
25
  async uninstall() {
19
- const configPath = join(homedir(), ".antigravity", "config.json");
26
+ const configPath = join(homedir(), ...ANTIGRAVITY_MCP_CONFIG);
20
27
  return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
21
28
  }
22
29
  }
@@ -1,22 +1,58 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
- import { IDEInstaller } from "../ide/installer.js";
4
+ import { IDEInstaller, SESSIONMEM_HOOK_COMMAND, SESSIONMEM_SESSION_END_HOOK_COMMAND, } from "../ide/installer.js";
5
5
  export class ClaudeCodeAdapter extends GenericMCPAdapter {
6
6
  name = "Claude Code";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
7
+ // Capabilities inherited from GenericMCPAdapter (tools only) so the
8
+ // fetch_memories fallback tool is registered.
9
+ // The installed SessionStart hook already injects prior context at the start
10
+ // of every session, so suppress the startup_inject_memories tool: an agent
11
+ // calling it on top of the hook would double-inject content and double-count
12
+ // access_count increments.
13
+ suppressStartupInjectionTool = true;
14
+ /**
15
+ * Claude Code reads ~/.claude/CLAUDE.md as global memory on every session
16
+ * across all projects — the right place for the sessionmem guidance block.
17
+ */
18
+ guidanceTargets() {
19
+ return [join(homedir(), ".claude", "CLAUDE.md")];
20
+ }
21
+ settingsPath() {
22
+ return join(homedir(), ".claude", "settings.json");
23
+ }
12
24
  async install() {
13
25
  const configPath = join(homedir(), ".claude.json");
14
- return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
15
- "run",
16
- ]);
26
+ // On Windows, the globally-installed `sessionmem` bin is a `.cmd` shim.
27
+ // Claude Code spawns MCP servers WITHOUT a shell, and `.cmd` shims require
28
+ // cmd.exe to execute, so a bare `command: "sessionmem"` fails to start.
29
+ // Route through `cmd /c sessionmem run` so the shim is resolved correctly.
30
+ const isWindows = process.platform === "win32";
31
+ const command = isWindows ? "cmd" : "sessionmem";
32
+ const args = isWindows ? ["/c", "sessionmem", "run"] : ["run"];
33
+ const mcpOk = await IDEInstaller.injectMcpConfig(configPath, "sessionmem", command, args);
34
+ // Register a SessionStart hook so prior memories are injected automatically
35
+ // at the start of EVERY Claude Code session. This is the deterministic
36
+ // auto-injection path the advisory `startup_inject_memories` tool could
37
+ // never guarantee: a hook runs unconditionally and Claude Code adds its
38
+ // output to the session context without the agent choosing to call a tool.
39
+ // A hook-wiring failure now propagates so the install command reports a
40
+ // failure if the SessionStart/SessionEnd hooks could not be written.
41
+ const startHookOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_HOOK_COMMAND);
42
+ // Register a SessionEnd hook so the session-end pipeline (light retention
43
+ // prune + auto-summarization of ingested session events) runs once when a
44
+ // session ends — the deterministic write-side counterpart to the
45
+ // SessionStart read-side hook.
46
+ const endHookOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
47
+ return mcpOk && startHookOk && endHookOk;
17
48
  }
18
49
  async uninstall() {
19
50
  const configPath = join(homedir(), ".claude.json");
20
- return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
51
+ const mcpOk = await IDEInstaller.removeMcpConfig(configPath, "sessionmem");
52
+ // Propagate hook-removal results so a failure to clean up either hook is
53
+ // reported as an uninstall failure rather than silently ignored.
54
+ const startOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_HOOK_COMMAND);
55
+ const endOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
56
+ return mcpOk && startOk && endOk;
21
57
  }
22
58
  }
@@ -1,22 +1,82 @@
1
- import { join } from "path";
1
+ import { join, dirname } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
- import { IDEInstaller } from "../ide/installer.js";
4
+ /**
5
+ * The TOML block Codex expects in ~/.codex/config.toml. Codex uses TOML, not
6
+ * JSON, so the JSON-based IDEInstaller helpers cannot be reused here.
7
+ */
8
+ const CODEX_MCP_BLOCK = `
9
+ [mcp_servers.sessionmem]
10
+ command = "sessionmem"
11
+ args = ["run"]
12
+ `;
5
13
  export class CodexAdapter extends GenericMCPAdapter {
6
14
  name = "Codex";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
15
+ // Capabilities inherited from GenericMCPAdapter (tools only).
16
+ /** Codex reads AGENTS.md; the global one lives in ~/.codex/AGENTS.md. */
17
+ guidanceTargets() {
18
+ return [join(homedir(), ".codex", "AGENTS.md")];
19
+ }
12
20
  async install() {
13
- const configPath = join(homedir(), ".codex", "config.json");
14
- return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
15
- "run",
16
- ]);
21
+ try {
22
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
23
+ const configPath = join(homedir(), ".codex", "config.toml");
24
+ const existing = existsSync(configPath)
25
+ ? readFileSync(configPath, "utf-8")
26
+ : "";
27
+ // Idempotent: skip if the sessionmem server section already exists.
28
+ // Anchor on line start so a commented/substring mention doesn't match.
29
+ if (existing.split("\n").some((line) => line.trimStart().startsWith("[mcp_servers.sessionmem]")))
30
+ return true;
31
+ const dir = dirname(configPath);
32
+ if (!existsSync(dir)) {
33
+ mkdirSync(dir, { recursive: true });
34
+ }
35
+ // Append the TOML block, trimming trailing whitespace to keep a single
36
+ // blank-line separator before our section.
37
+ const updated = existing.trimEnd() + "\n" + CODEX_MCP_BLOCK;
38
+ writeFileSync(configPath, updated, "utf-8");
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
17
44
  }
18
45
  async uninstall() {
19
- const configPath = join(homedir(), ".codex", "config.json");
20
- return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
46
+ try {
47
+ const { readFileSync, writeFileSync, existsSync } = await import("fs");
48
+ const configPath = join(homedir(), ".codex", "config.toml");
49
+ if (!existsSync(configPath))
50
+ return true;
51
+ const existing = readFileSync(configPath, "utf-8");
52
+ if (!existing.includes("[mcp_servers.sessionmem]"))
53
+ return true;
54
+ const lines = existing.split("\n");
55
+ const result = [];
56
+ let i = 0;
57
+ while (i < lines.length) {
58
+ if (lines[i].trim() === "[mcp_servers.sessionmem]") {
59
+ // Skip this section's header and body until the next section header
60
+ // (a line starting with "[") or end of file.
61
+ i++;
62
+ while (i < lines.length && !lines[i].trimStart().startsWith("[")) {
63
+ i++;
64
+ }
65
+ // Drop any trailing blank lines accumulated before the next section.
66
+ while (result.length > 0 && result[result.length - 1].trim() === "") {
67
+ result.pop();
68
+ }
69
+ continue;
70
+ }
71
+ result.push(lines[i]);
72
+ i++;
73
+ }
74
+ const updated = result.join("\n").trimEnd() + "\n";
75
+ writeFileSync(configPath, updated, "utf-8");
76
+ return true;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
21
81
  }
22
82
  }
@@ -2,13 +2,26 @@ import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
4
  import { IDEInstaller } from "../ide/installer.js";
5
+ // ⚠️ UNVERIFIED MCP CONFIG PATH — see audit round 18.
6
+ // The real product is "Qoder" (Alibaba's agentic AI IDE, a VS Code fork), not
7
+ // "QCoder", and it is NOT a Google tool / not "Project IDX" (IDX became Firebase
8
+ // Studio). Qoder configures MCP servers through its in-IDE Settings UI; the
9
+ // underlying global config FILE PATH is NOT publicly documented as of 2026-06.
10
+ // Qoder's published config snippet does use the standard
11
+ // `{ "mcpServers": { command, args, env } }` schema that
12
+ // IDEInstaller.injectMcpConfig writes, so the JSON STRUCTURE here is correct —
13
+ // but `~/.qoder/config.json` (let alone the current misspelled `~/.qcoder/...`)
14
+ // is an EDUCATED GUESS. If Qoder reads MCP config from a different file, this
15
+ // install() is a silent no-op for the host. TODO: confirm against a real Qoder
16
+ // install (Qoder Settings → MCP → "View raw config") and correct the path +
17
+ // directory spelling, or wire it through the UI-managed store if no file exists.
5
18
  export class QCoderAdapter extends GenericMCPAdapter {
6
19
  name = "QCoder";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
20
+ // Capabilities inherited from GenericMCPAdapter (tools only).
21
+ /** QCoder reads AGENTS.md-style guidance from its global config dir. */
22
+ guidanceTargets() {
23
+ return [join(homedir(), ".qcoder", "AGENTS.md")];
24
+ }
12
25
  async install() {
13
26
  const configPath = join(homedir(), ".qcoder", "config.json");
14
27
  return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
@@ -1,20 +1,65 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
+ import { existsSync } from "fs";
3
4
  import { GenericMCPAdapter } from "../generic.js";
4
5
  import { IDEInstaller } from "./installer.js";
6
+ /**
7
+ * VS Code variants Cline can be installed into, in detection-priority order.
8
+ * The globalStorage path differs only by this editor-name segment.
9
+ */
10
+ const VSCODE_EDITOR_VARIANTS = [
11
+ "Code",
12
+ "Code - Insiders",
13
+ "VSCodium",
14
+ "Cursor",
15
+ ];
5
16
  export class ClineAdapter extends GenericMCPAdapter {
6
17
  name = "Cline";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
18
+ // Capabilities inherited from GenericMCPAdapter (tools only).
19
+ /** Cline reads global rules from ~/Documents/Cline/Rules/. */
20
+ guidanceTargets() {
21
+ return [join(homedir(), "Documents", "Cline", "Rules", "sessionmem.md")];
22
+ }
23
+ /**
24
+ * Cline (the VS Code extension `saoudrizwan.claude-dev`) stores MCP servers in
25
+ * `cline_mcp_settings.json` under the editor's globalStorage — NOT in a
26
+ * `~/.cline/config.json`. The base dir differs per platform:
27
+ * - macOS: ~/Library/Application Support/Code/User/globalStorage/...
28
+ * - Windows: %APPDATA%\Code\User\globalStorage\...
29
+ * - Linux: ~/.config/Code/User/globalStorage/...
30
+ */
31
+ get configPath() {
32
+ const home = homedir();
33
+ let base;
34
+ if (process.platform === "win32") {
35
+ base = process.env.APPDATA ?? join(home, "AppData", "Roaming");
36
+ }
37
+ else if (process.platform === "darwin") {
38
+ base = join(home, "Library", "Application Support");
39
+ }
40
+ else {
41
+ base = join(home, ".config");
42
+ }
43
+ const rest = [
44
+ "User",
45
+ "globalStorage",
46
+ "saoudrizwan.claude-dev",
47
+ "settings",
48
+ "cline_mcp_settings.json",
49
+ ];
50
+ // "Code" is VS Code stable, but Cline also installs into Insiders, VSCodium,
51
+ // and Cursor. Pick the first variant whose globalStorage dir already exists;
52
+ // fall back to stable "Code" when none is found (fresh install).
53
+ const editorName = VSCODE_EDITOR_VARIANTS.find((name) => existsSync(join(base, name, "User", "globalStorage"))) ?? "Code";
54
+ return join(base, editorName, ...rest);
55
+ }
12
56
  async install() {
13
- const configPath = join(homedir(), ".cline", "config.json");
14
- return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", ["run"]);
57
+ // Cline's server schema carries `disabled` and `autoApprove` alongside
58
+ // command/args; pass them through so the written block matches what Cline
59
+ // expects to read.
60
+ return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"], { disabled: false, autoApprove: [] });
15
61
  }
16
62
  async uninstall() {
17
- const configPath = join(homedir(), ".cline", "config.json");
18
- return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
63
+ return IDEInstaller.removeMcpConfig(this.configPath, "sessionmem");
19
64
  }
20
65
  }
@@ -4,20 +4,22 @@ import { GenericMCPAdapter } from "../generic.js";
4
4
  import { IDEInstaller } from "./installer.js";
5
5
  export class CursorAdapter extends GenericMCPAdapter {
6
6
  name = "Cursor";
7
- capabilities = {
8
- supportsPrompts: false,
9
- supportsResources: false,
10
- supportsTools: true,
11
- };
7
+ // Capabilities inherited from GenericMCPAdapter (tools only).
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
11
+ * `alwaysApply: true` frontmatter on creation so the rule is always active.
12
+ */
13
+ guidanceTargets() {
14
+ return [join(homedir(), ".cursor", "rules", "sessionmem.mdc")];
15
+ }
16
+ /**
17
+ * Cursor reads MCP servers from `~/.cursor/mcp.json` (global) — NOT from the
18
+ * VS Code-style `Cursor/User/settings.json`, which Cursor ignores for MCP.
19
+ * The path is the same on every platform.
20
+ */
12
21
  get configPath() {
13
- const home = homedir();
14
- if (process.platform === "win32") {
15
- return join(process.env.APPDATA ?? home, "Cursor", "User", "settings.json");
16
- }
17
- if (process.platform === "darwin") {
18
- return join(home, "Library", "Application Support", "Cursor", "User", "settings.json");
19
- }
20
- return join(home, ".config", "Cursor", "User", "settings.json");
22
+ return join(homedir(), ".cursor", "mcp.json");
21
23
  }
22
24
  async install() {
23
25
  return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"]);
@@ -1,11 +1,99 @@
1
+ /**
2
+ * The exact command Claude Code runs at session start. Installed as a
3
+ * `SessionStart` hook in ~/.claude/settings.json so prior memories are injected
4
+ * into every session automatically — the deterministic counterpart to the
5
+ * advisory `startup_inject_memories` tool (which the agent must choose to call).
6
+ */
7
+ export const SESSIONMEM_HOOK_COMMAND = "sessionmem session-start";
8
+ /**
9
+ * The command Claude Code runs when a session ends. Installed as a `SessionEnd`
10
+ * hook so the session-end pipeline (light retention prune + auto-summarization
11
+ * of any ingested session events) runs automatically once per session, without
12
+ * relying on the agent choosing to call a tool.
13
+ */
14
+ export const SESSIONMEM_SESSION_END_HOOK_COMMAND = "sessionmem session-end";
15
+ /** Default Claude Code hook event for the legacy 2-arg hook helpers. */
16
+ const DEFAULT_HOOK_EVENT = "SessionStart";
1
17
  export class IDEInstaller {
2
18
  static parseJsonc(content) {
3
- const stripped = content
4
- .replace(/\/\/[^\n]*/g, "")
5
- .replace(/,(\s*[}\]])/g, "$1");
6
- return JSON.parse(stripped);
19
+ // Strip single-line comments (//) that appear outside of string literals.
20
+ // A naive regex strips // inside strings (e.g. URLs). This implementation
21
+ // tracks whether we are inside a double-quoted string to avoid that.
22
+ let result = "";
23
+ let inString = false;
24
+ let i = 0;
25
+ while (i < content.length) {
26
+ const ch = content[i];
27
+ if (ch === "\\" && inString) {
28
+ result += content[i] + (content[i + 1] ?? "");
29
+ i += 2;
30
+ continue;
31
+ }
32
+ if (ch === '"') {
33
+ inString = !inString;
34
+ result += ch;
35
+ i++;
36
+ continue;
37
+ }
38
+ if (!inString && ch === "/" && content[i + 1] === "/") {
39
+ while (i < content.length && content[i] !== "\n")
40
+ i++;
41
+ continue;
42
+ }
43
+ if (!inString && ch === "/" && content[i + 1] === "*") {
44
+ i += 2;
45
+ while (i < content.length) {
46
+ if (content[i] === "*" && content[i + 1] === "/") {
47
+ i += 2;
48
+ break;
49
+ }
50
+ i++;
51
+ }
52
+ continue;
53
+ }
54
+ result += ch;
55
+ i++;
56
+ }
57
+ // Strip trailing commas outside of string literals. Doing this with a single
58
+ // regex over the whole string corrupts string values that contain ",]" or
59
+ // ",}"; mirror the inString tracking so only structural commas are removed.
60
+ let cleaned = "";
61
+ let inStr = false;
62
+ let i2 = 0;
63
+ while (i2 < result.length) {
64
+ const c = result[i2];
65
+ if (c === "\\" && inStr) {
66
+ cleaned += result[i2] + (result[i2 + 1] ?? "");
67
+ i2 += 2;
68
+ continue;
69
+ }
70
+ if (c === '"') {
71
+ inStr = !inStr;
72
+ cleaned += c;
73
+ i2++;
74
+ continue;
75
+ }
76
+ if (!inStr && c === ",") {
77
+ // look ahead for optional whitespace then } or ]
78
+ let j = i2 + 1;
79
+ while (j < result.length &&
80
+ (result[j] === " " ||
81
+ result[j] === "\n" ||
82
+ result[j] === "\r" ||
83
+ result[j] === "\t"))
84
+ j++;
85
+ if (j < result.length && (result[j] === "}" || result[j] === "]")) {
86
+ i2++; // skip the comma
87
+ continue;
88
+ }
89
+ }
90
+ cleaned += c;
91
+ i2++;
92
+ }
93
+ result = cleaned;
94
+ return JSON.parse(result);
7
95
  }
8
- static injectMcpBlock(content, serverName, command, args) {
96
+ static injectMcpBlock(content, serverName, command, args, extraFields) {
9
97
  const config = content.trim()
10
98
  ? this.parseJsonc(content)
11
99
  : {};
@@ -14,6 +102,10 @@ export class IDEInstaller {
14
102
  config.mcpServers[serverName] = {
15
103
  command,
16
104
  args,
105
+ // Hosts with a richer server schema (e.g. Cline's
106
+ // `disabled` / `autoApprove`) pass those fields through here so the
107
+ // written block matches what the host expects to read.
108
+ ...extraFields,
17
109
  };
18
110
  return JSON.stringify(config, null, 2);
19
111
  }
@@ -26,13 +118,22 @@ export class IDEInstaller {
26
118
  }
27
119
  return JSON.stringify(config, null, 2);
28
120
  }
29
- static async injectMcpConfig(filePath, serverName, command, args) {
121
+ static async injectMcpConfig(filePath, serverName, command, args, extraFields) {
30
122
  try {
31
- const { readFileSync, writeFileSync, existsSync } = await import("fs");
123
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
124
+ const { dirname } = await import("path");
32
125
  const existing = existsSync(filePath)
33
126
  ? readFileSync(filePath, "utf-8")
34
127
  : "{}";
35
- const updated = this.injectMcpBlock(existing, serverName, command, args);
128
+ const updated = this.injectMcpBlock(existing, serverName, command, args, extraFields);
129
+ // Create the host's config directory if it doesn't exist yet (fresh
130
+ // install, or a config path the host hasn't created). writeFileSync alone
131
+ // throws ENOENT on a missing parent, which previously surfaced as an
132
+ // opaque "config update failed".
133
+ const dir = dirname(filePath);
134
+ if (!existsSync(dir)) {
135
+ mkdirSync(dir, { recursive: true });
136
+ }
36
137
  writeFileSync(filePath, updated, "utf-8");
37
138
  return true;
38
139
  }
@@ -54,4 +155,96 @@ export class IDEInstaller {
54
155
  return false;
55
156
  }
56
157
  }
158
+ /** True when a SessionStart entry contains a command-hook running `command`. */
159
+ static hookEntryHasCommand(entry, command) {
160
+ if (!entry || !Array.isArray(entry.hooks))
161
+ return false;
162
+ return entry.hooks.some((hook) => hook?.command === command);
163
+ }
164
+ /**
165
+ * Merge a Claude Code command hook into a settings.json string, preserving
166
+ * every other key (mcpServers, other hook events, user settings). Defaults to
167
+ * the `SessionStart` event; pass `eventName` for another event (e.g.
168
+ * `SessionEnd`). Idempotent: re-injecting the same command on the same event
169
+ * never produces a duplicate entry.
170
+ */
171
+ static injectClaudeHookBlock(content, command, eventName = DEFAULT_HOOK_EVENT) {
172
+ const config = content.trim()
173
+ ? this.parseJsonc(content)
174
+ : {};
175
+ const hooks = config.hooks && typeof config.hooks === "object"
176
+ ? config.hooks
177
+ : {};
178
+ const eventEntries = Array.isArray(hooks[eventName])
179
+ ? hooks[eventName]
180
+ : [];
181
+ // Drop any pre-existing sessionmem entry so re-install stays idempotent and
182
+ // a stale command form is replaced rather than duplicated.
183
+ const filtered = eventEntries.filter((entry) => !this.hookEntryHasCommand(entry, command));
184
+ filtered.push({ hooks: [{ type: "command", command }] });
185
+ hooks[eventName] = filtered;
186
+ config.hooks = hooks;
187
+ return JSON.stringify(config, null, 2);
188
+ }
189
+ /**
190
+ * Remove a sessionmem command hook from a settings.json string, leaving all
191
+ * other hooks and settings intact. Defaults to the `SessionStart` event.
192
+ * Cleans up the event array and the hooks object when they become empty.
193
+ */
194
+ static removeClaudeHookBlock(content, command, eventName = DEFAULT_HOOK_EVENT) {
195
+ const config = content.trim()
196
+ ? this.parseJsonc(content)
197
+ : {};
198
+ const hooks = config.hooks && typeof config.hooks === "object"
199
+ ? config.hooks
200
+ : undefined;
201
+ if (!hooks)
202
+ return JSON.stringify(config, null, 2);
203
+ if (Array.isArray(hooks[eventName])) {
204
+ const filtered = hooks[eventName].filter((entry) => !this.hookEntryHasCommand(entry, command));
205
+ if (filtered.length === 0) {
206
+ delete hooks[eventName];
207
+ }
208
+ else {
209
+ hooks[eventName] = filtered;
210
+ }
211
+ }
212
+ if (Object.keys(hooks).length === 0) {
213
+ delete config.hooks;
214
+ }
215
+ return JSON.stringify(config, null, 2);
216
+ }
217
+ static async injectClaudeHook(filePath, command, eventName = DEFAULT_HOOK_EVENT) {
218
+ try {
219
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
220
+ const { dirname } = await import("path");
221
+ const existing = existsSync(filePath)
222
+ ? readFileSync(filePath, "utf-8")
223
+ : "{}";
224
+ const updated = this.injectClaudeHookBlock(existing, command, eventName);
225
+ const dir = dirname(filePath);
226
+ if (!existsSync(dir)) {
227
+ mkdirSync(dir, { recursive: true });
228
+ }
229
+ writeFileSync(filePath, updated, "utf-8");
230
+ return true;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ static async removeClaudeHook(filePath, command, eventName = DEFAULT_HOOK_EVENT) {
237
+ try {
238
+ const { readFileSync, writeFileSync, existsSync } = await import("fs");
239
+ if (!existsSync(filePath))
240
+ return true;
241
+ const existing = readFileSync(filePath, "utf-8");
242
+ const updated = this.removeClaudeHookBlock(existing, command, eventName);
243
+ writeFileSync(filePath, updated, "utf-8");
244
+ return true;
245
+ }
246
+ catch {
247
+ return false;
248
+ }
249
+ }
57
250
  }
@@ -4,20 +4,21 @@ import { GenericMCPAdapter } from "../generic.js";
4
4
  import { IDEInstaller } from "./installer.js";
5
5
  export class WindsurfAdapter extends GenericMCPAdapter {
6
6
  name = "Windsurf";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
7
+ // Capabilities inherited from GenericMCPAdapter (tools only).
8
+ /** Windsurf reads global rules from ~/.codeium/windsurf/memories/global_rules.md. */
9
+ guidanceTargets() {
10
+ return [
11
+ join(homedir(), ".codeium", "windsurf", "memories", "global_rules.md"),
12
+ ];
13
+ }
14
+ /**
15
+ * Windsurf reads MCP servers from `~/.codeium/windsurf/mcp_config.json` — NOT
16
+ * from the VS Code-style `Windsurf/User/settings.json`. Same path on every
17
+ * platform (mirrors the `.codeium/windsurf` root the guidance file already
18
+ * uses).
19
+ */
12
20
  get configPath() {
13
- const home = homedir();
14
- if (process.platform === "win32") {
15
- return join(process.env.APPDATA ?? home, "Windsurf", "User", "settings.json");
16
- }
17
- if (process.platform === "darwin") {
18
- return join(home, "Library", "Application Support", "Windsurf", "User", "settings.json");
19
- }
20
- return join(home, ".config", "Windsurf", "User", "settings.json");
21
+ return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
21
22
  }
22
23
  async install() {
23
24
  return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"]);
@@ -1,3 +1,6 @@
1
+ import { createRequire } from "module";
2
+ const require = createRequire(import.meta.url);
3
+ const pkg = require("../../../package.json");
1
4
  export const pingTool = {
2
5
  name: "sessionmem_ping",
3
6
  description: "Ping the sessionmem MCP server to verify it is running correctly.",
@@ -8,7 +11,7 @@ export const pingTool = {
8
11
  execute: async () => {
9
12
  return {
10
13
  status: "ok",
11
- version: "0.1.0",
14
+ version: pkg.version,
12
15
  message: "sessionmem MCP server is operational.",
13
16
  };
14
17
  }