sessionmem 1.0.6 → 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 (45) hide show
  1. package/dist/adapters/capabilities/fallbackTools.js +2 -2
  2. package/dist/adapters/claudeMdInjector.js +49 -5
  3. package/dist/adapters/factory.js +68 -9
  4. package/dist/adapters/generic.js +147 -12
  5. package/dist/adapters/global/antigravity.js +14 -7
  6. package/dist/adapters/global/claudeCode.js +46 -10
  7. package/dist/adapters/global/codex.js +73 -13
  8. package/dist/adapters/global/qcoder.js +18 -5
  9. package/dist/adapters/ide/cline.js +54 -9
  10. package/dist/adapters/ide/cursor.js +15 -13
  11. package/dist/adapters/ide/installer.js +201 -8
  12. package/dist/adapters/ide/windsurf.js +14 -13
  13. package/dist/cli/commands/config.js +10 -1
  14. package/dist/cli/commands/import.js +6 -1
  15. package/dist/cli/commands/install.js +57 -16
  16. package/dist/cli/commands/ping.js +42 -8
  17. package/dist/cli/commands/reEmbed.js +4 -3
  18. package/dist/cli/commands/run.js +7 -17
  19. package/dist/cli/commands/savings.js +33 -17
  20. package/dist/cli/commands/sessionEnd.js +124 -0
  21. package/dist/cli/commands/sessionStart.js +52 -0
  22. package/dist/cli/commands/sync.js +39 -9
  23. package/dist/cli/commands/uninstall.js +35 -9
  24. package/dist/cli/context.js +14 -18
  25. package/dist/cli/index.js +16 -4
  26. package/dist/cli/projectId.js +69 -0
  27. package/dist/core/api/contracts.js +155 -42
  28. package/dist/core/api/errors.js +4 -7
  29. package/dist/core/api/memoryCoreService.js +319 -252
  30. package/dist/core/api/sessionLifecycleService.js +8 -0
  31. package/dist/core/config/policyConfig.js +33 -6
  32. package/dist/core/injection/formatStartupInjection.js +53 -9
  33. package/dist/core/retrieve/recencyBands.js +4 -1
  34. package/dist/core/retrieve/retrieveMemories.js +10 -8
  35. package/dist/core/schema/migrations/005_team_provenance.sql +5 -0
  36. package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
  37. package/dist/core/schema/migrations/008_fts5_search.sql +6 -2
  38. package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
  39. package/dist/core/schema/runMigrations.js +64 -2
  40. package/dist/core/storage/memoryRepo.js +164 -7
  41. package/dist/core/storage/memorySearchRepo.js +45 -7
  42. package/dist/core/storage/sessionEventsRepo.js +15 -2
  43. package/dist/core/summarize/cloudSummarizer.js +15 -2
  44. package/dist/core/summarize/redaction.js +45 -8
  45. package/package.json +2 -2
@@ -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"]);
@@ -20,6 +20,13 @@ function coerceRetentionDays(raw) {
20
20
  }
21
21
  return n;
22
22
  }
23
+ function coerceInjectionCap(raw) {
24
+ const n = coerceInt(raw);
25
+ if (n < 100 || n > 10000) {
26
+ throw new Error(`injectionCap must be an integer between 100 and 10000, got "${raw}"`);
27
+ }
28
+ return n;
29
+ }
23
30
  function coerceBool(raw) {
24
31
  const v = raw.trim().toLowerCase();
25
32
  if (v === "true")
@@ -34,6 +41,7 @@ const CONFIG_KEYS = {
34
41
  "retention.days": { field: "retentionDays", coerce: coerceRetentionDays },
35
42
  retentionDays: { field: "retentionDays", coerce: coerceRetentionDays },
36
43
  redactionEnabled: { field: "redactionEnabled", coerce: coerceBool },
44
+ injectionCap: { field: "injectionCap", coerce: coerceInjectionCap },
37
45
  };
38
46
  function resolvePath(options) {
39
47
  return options?.configPath ?? configFilePath();
@@ -50,7 +58,8 @@ export function configGetCommand(key, options) {
50
58
  return;
51
59
  }
52
60
  const config = readPolicyConfig(resolvePath(options));
53
- console.log(String(config[def.field]));
61
+ const val = config[def.field];
62
+ console.log(val === undefined ? "(not set)" : String(val));
54
63
  }
55
64
  /**
56
65
  * `sessionmem config set <key> <value>` — coerce and persist to config.json.
@@ -73,7 +73,9 @@ export async function importCommand(pathArg, options, ctx) {
73
73
  invalidCount += 1;
74
74
  continue;
75
75
  }
76
- validMemories.push(mapped[i]);
76
+ // Use the parsed/transformed record so `kind` is narrowed to the
77
+ // canonical enum (memoryKindSchema maps legacy values like `architecture`).
78
+ validMemories.push(check.data);
77
79
  }
78
80
  if (validMemories.length === 0) {
79
81
  if (invalidCount > 0) {
@@ -100,6 +102,9 @@ export async function importCommand(pathArg, options, ctx) {
100
102
  let suffix = result.skippedCrossProject > 0
101
103
  ? ` (${result.skippedCrossProject} skipped: id belongs to another project)`
102
104
  : "";
105
+ if (result.skippedExisting > 0) {
106
+ suffix += ` (${result.skippedExisting} skipped: id already exists in this project)`;
107
+ }
103
108
  if (invalidCount > 0) {
104
109
  suffix += ` (${invalidCount} invalid record(s) skipped)`;
105
110
  }