sessionmem 1.0.6 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/capabilities/fallbackTools.js +2 -2
- package/dist/adapters/claudeMdInjector.js +49 -5
- package/dist/adapters/factory.js +68 -9
- package/dist/adapters/generic.js +147 -12
- package/dist/adapters/global/antigravity.js +14 -7
- package/dist/adapters/global/claudeCode.js +46 -10
- package/dist/adapters/global/codex.js +73 -13
- package/dist/adapters/global/qcoder.js +18 -5
- package/dist/adapters/ide/cline.js +56 -9
- package/dist/adapters/ide/cursor.js +15 -13
- package/dist/adapters/ide/installer.js +201 -8
- package/dist/adapters/ide/windsurf.js +14 -13
- package/dist/cli/commands/config.js +10 -1
- package/dist/cli/commands/import.js +6 -1
- package/dist/cli/commands/install.js +57 -16
- package/dist/cli/commands/ping.js +42 -8
- package/dist/cli/commands/reEmbed.js +4 -3
- package/dist/cli/commands/run.js +7 -17
- package/dist/cli/commands/savings.js +33 -17
- package/dist/cli/commands/sessionEnd.js +124 -0
- package/dist/cli/commands/sessionStart.js +52 -0
- package/dist/cli/commands/sync.js +39 -9
- package/dist/cli/commands/uninstall.js +35 -9
- package/dist/cli/context.js +17 -18
- package/dist/cli/index.js +16 -4
- package/dist/cli/projectId.js +69 -0
- package/dist/core/api/contracts.js +155 -42
- package/dist/core/api/errors.js +4 -7
- package/dist/core/api/memoryCoreService.js +319 -252
- package/dist/core/api/sessionLifecycleService.js +8 -0
- package/dist/core/config/policyConfig.js +33 -6
- package/dist/core/injection/formatStartupInjection.js +53 -9
- package/dist/core/retrieve/recencyBands.js +4 -1
- package/dist/core/retrieve/retrieveMemories.js +10 -8
- package/dist/core/schema/migrations/005_team_provenance.sql +5 -0
- package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
- package/dist/core/schema/migrations/008_fts5_search.sql +6 -2
- package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
- package/dist/core/schema/runMigrations.js +64 -2
- package/dist/core/storage/memoryRepo.js +164 -7
- package/dist/core/storage/memorySearchRepo.js +45 -7
- package/dist/core/storage/sessionEventsRepo.js +15 -2
- package/dist/core/summarize/cloudSummarizer.js +15 -2
- package/dist/core/summarize/redaction.js +45 -8
- 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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,67 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
54
|
+
// `name` is from the hardcoded VSCODE_EDITOR_VARIANTS constant above — not user input.
|
|
55
|
+
const editorName = VSCODE_EDITOR_VARIANTS.find((name) => existsSync(join(base, name, "User", "globalStorage"))) ?? "Code";
|
|
56
|
+
return join(base, editorName, ...rest);
|
|
57
|
+
}
|
|
12
58
|
async install() {
|
|
13
|
-
|
|
14
|
-
|
|
59
|
+
// Cline's server schema carries `disabled` and `autoApprove` alongside
|
|
60
|
+
// command/args; pass them through so the written block matches what Cline
|
|
61
|
+
// expects to read.
|
|
62
|
+
return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"], { disabled: false, autoApprove: [] });
|
|
15
63
|
}
|
|
16
64
|
async uninstall() {
|
|
17
|
-
|
|
18
|
-
return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
|
|
65
|
+
return IDEInstaller.removeMcpConfig(this.configPath, "sessionmem");
|
|
19
66
|
}
|
|
20
67
|
}
|
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|