sessionmem 1.0.5 → 1.0.6
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/LICENSE +21 -21
- package/README.md +372 -365
- package/dist/adapters/capabilities/fallbackTools.js +33 -18
- package/dist/adapters/claudeMdInjector.js +120 -0
- package/dist/adapters/generic.js +83 -12
- package/dist/adapters/tools/ping.js +4 -1
- package/dist/cli/commands/install.js +18 -1
- package/dist/cli/commands/reEmbed.js +47 -0
- package/dist/cli/commands/run.js +28 -2
- package/dist/cli/commands/savings.js +75 -0
- package/dist/cli/commands/uninstall.js +10 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/output.js +11 -3
- package/dist/core/api/contracts.js +34 -10
- package/dist/core/api/memoryCoreService.js +188 -86
- package/dist/core/api/sessionLifecycleService.js +12 -2
- package/dist/core/config/policyConfig.js +20 -0
- package/dist/core/injection/formatStartupInjection.js +2 -1
- package/dist/core/injection/tokenBudget.js +8 -0
- package/dist/core/retrieve/importance.js +4 -3
- package/dist/core/retrieve/recencyBands.js +3 -10
- package/dist/core/retrieve/retrieveMemories.js +17 -4
- package/dist/core/retrieve/score.js +11 -1
- package/dist/core/schema/migrations/005_team_provenance.sql +9 -9
- package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
- package/dist/core/schema/migrations/007_feedback_manual_delete.sql +23 -0
- package/dist/core/schema/migrations/008_fts5_search.sql +33 -0
- package/dist/core/storage/db.js +6 -0
- package/dist/core/storage/memoryFeedbackRepo.js +14 -4
- package/dist/core/storage/memoryRepo.js +134 -120
- package/dist/core/storage/memorySearchRepo.js +87 -13
- package/dist/core/storage/sessionEventsRepo.js +19 -9
- package/dist/core/storage/summarizationFailuresRepo.js +36 -26
- package/dist/core/storage/tokenSavingsRepo.js +20 -0
- package/dist/core/summarize/cloudSummarizer.js +21 -5
- package/dist/core/summarize/localSummarizer.js +1 -10
- package/package.json +50 -48
|
@@ -1,34 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export class FallbackToolRegistrar {
|
|
2
|
-
static getFallbackTools(capabilities) {
|
|
3
|
+
static getFallbackTools(capabilities, context) {
|
|
3
4
|
const tools = [];
|
|
4
5
|
if (!capabilities.supportsResources) {
|
|
5
6
|
tools.push({
|
|
6
7
|
name: "fetch_memories",
|
|
7
|
-
description: "Fallback
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
},
|
|
13
|
-
required: ["query"],
|
|
8
|
+
description: "Fallback memory retrieval for hosts that do not support MCP resources. Call this instead of accessing the sessionmem:// resource URI directly when the host lacks resource support. Semantically equivalent to retrieveMemories — returns stored memories ranked by relevance to the query. Read-only; no side effects.\n\n" +
|
|
9
|
+
"WHEN TO CALL: At session start and mid-session when you need to retrieve context and the host does not support MCP resources. Do not call if the host supports MCP resources — use the sessionmem:// resource URI or retrieveMemories tool instead.\n\n" +
|
|
10
|
+
"Parameter `query`: natural-language description of what context you need to recall (e.g. 'API design decisions', 'database schema choices').",
|
|
11
|
+
inputShape: {
|
|
12
|
+
query: z.string().describe("Natural-language description of what context you need to recall."),
|
|
14
13
|
},
|
|
15
14
|
execute: async (args) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const result = await context.service.call("retrieveMemories", {
|
|
16
|
+
projectId: context.projectId,
|
|
17
|
+
query: args.query,
|
|
18
|
+
limit: 10,
|
|
19
|
+
mode: "on-demand",
|
|
20
|
+
depth: "default",
|
|
21
|
+
});
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
return `Error: ${result.error.message}`;
|
|
24
|
+
return JSON.stringify(result.memories, null, 2);
|
|
25
|
+
},
|
|
19
26
|
});
|
|
20
27
|
}
|
|
21
28
|
if (!capabilities.supportsPrompts) {
|
|
22
29
|
tools.push({
|
|
23
30
|
name: "startup_inject_memories",
|
|
24
|
-
description: "Fallback
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
31
|
+
description: "Fallback startup-injection for hosts that do not support MCP prompts. Call this once at the very start of a session instead of relying on the automatic sessionmem startup prompt when the host lacks prompt support. Injects the top relevant memories for the current project into the working context. No parameters required.\n\n" +
|
|
32
|
+
"WHEN TO CALL: Once per session start, before any user task work begins, when the host does not surface MCP prompts automatically. Do not call if the host already surfaces the sessionmem startup prompt — calling both duplicates injected context.\n\n" +
|
|
33
|
+
"Read-only; no side effects.",
|
|
34
|
+
inputShape: {},
|
|
29
35
|
execute: async () => {
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const result = await context.service.call("retrieveMemories", {
|
|
37
|
+
projectId: context.projectId,
|
|
38
|
+
query: "session startup context",
|
|
39
|
+
limit: 20,
|
|
40
|
+
mode: "auto",
|
|
41
|
+
depth: "default",
|
|
42
|
+
});
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
return `Error: ${result.error.message}`;
|
|
45
|
+
return result.startupInjection;
|
|
46
|
+
},
|
|
32
47
|
});
|
|
33
48
|
}
|
|
34
49
|
return tools;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
export const SESSIONMEM_BLOCK_START = "<!-- sessionmem:start -->";
|
|
4
|
+
export const SESSIONMEM_BLOCK_END = "<!-- sessionmem:end -->";
|
|
5
|
+
const BLOCK_CONTENT = `
|
|
6
|
+
## sessionmem — Persistent Memory
|
|
7
|
+
|
|
8
|
+
sessionmem is an MCP memory layer that persists context across sessions. Use its tools to store important decisions, facts, and context so they're available in future sessions without the user re-explaining.
|
|
9
|
+
|
|
10
|
+
### When to store memories (storeMemory)
|
|
11
|
+
- User makes an architectural or design decision
|
|
12
|
+
- You discover a non-obvious project constraint or convention
|
|
13
|
+
- A debugging session reveals an important root cause
|
|
14
|
+
- User states a preference about how they want things done
|
|
15
|
+
- A warning or pitfall is discovered that future sessions should know about
|
|
16
|
+
|
|
17
|
+
### When to retrieve memories (retrieveMemories)
|
|
18
|
+
- At the start of a session or task to check for relevant prior context
|
|
19
|
+
- Before making architectural decisions (check if prior decisions exist)
|
|
20
|
+
- When the user references something from a previous session
|
|
21
|
+
- When working in an area of the codebase that may have stored warnings or decisions
|
|
22
|
+
|
|
23
|
+
### Memory kinds
|
|
24
|
+
- \`decision\` — architectural or design choices (importance: 7-9)
|
|
25
|
+
- \`fact\` — project constraints, conventions, patterns (importance: 5-7)
|
|
26
|
+
- \`warning\` — pitfalls, gotchas, things that broke before (importance: 8-10)
|
|
27
|
+
- \`preference\` — how the user likes things done (importance: 5-7)
|
|
28
|
+
- \`summary\` — session summaries (auto-generated, importance: 3-5)
|
|
29
|
+
|
|
30
|
+
### Other tools
|
|
31
|
+
- \`listMemories\` — browse all stored memories for this project
|
|
32
|
+
- \`getMemory\` — fetch a specific memory by ID
|
|
33
|
+
- \`forgetMemory\` — delete an outdated or incorrect memory
|
|
34
|
+
- \`stats\` — check memory count and health
|
|
35
|
+
|
|
36
|
+
### Guidelines
|
|
37
|
+
- Don't store trivial or easily re-derivable information
|
|
38
|
+
- Don't retrieve memories every single turn — retrieve at task boundaries
|
|
39
|
+
- Keep memory content concise (1-3 sentences)
|
|
40
|
+
- Use appropriate importance scores (see kinds above)
|
|
41
|
+
`;
|
|
42
|
+
export function generateClaudeMdBlock() {
|
|
43
|
+
return `${SESSIONMEM_BLOCK_START}\n${BLOCK_CONTENT}\n${SESSIONMEM_BLOCK_END}`;
|
|
44
|
+
}
|
|
45
|
+
export function injectClaudeMdBlock(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
const dir = dirname(filePath);
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
let content = "";
|
|
52
|
+
if (existsSync(filePath)) {
|
|
53
|
+
content = readFileSync(filePath, "utf8");
|
|
54
|
+
}
|
|
55
|
+
const block = generateClaudeMdBlock();
|
|
56
|
+
if (hasClaudeMdBlock(filePath)) {
|
|
57
|
+
// Replace existing block
|
|
58
|
+
const startIdx = content.indexOf(SESSIONMEM_BLOCK_START);
|
|
59
|
+
const endIdx = content.indexOf(SESSIONMEM_BLOCK_END) + SESSIONMEM_BLOCK_END.length;
|
|
60
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Append to file
|
|
64
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
65
|
+
content += "\n";
|
|
66
|
+
}
|
|
67
|
+
content += "\n" + block + "\n";
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(filePath, content, "utf8");
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function removeClaudeMdBlock(filePath) {
|
|
77
|
+
try {
|
|
78
|
+
if (!existsSync(filePath)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const content = readFileSync(filePath, "utf8");
|
|
82
|
+
const startIdx = content.indexOf(SESSIONMEM_BLOCK_START);
|
|
83
|
+
if (startIdx === -1) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const endIdx = content.indexOf(SESSIONMEM_BLOCK_END);
|
|
87
|
+
if (endIdx === -1) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const endOfBlock = endIdx + SESSIONMEM_BLOCK_END.length;
|
|
91
|
+
// Remove the block and any trailing newline
|
|
92
|
+
let before = content.slice(0, startIdx);
|
|
93
|
+
let after = content.slice(endOfBlock);
|
|
94
|
+
// Clean up extra blank lines around the removed block
|
|
95
|
+
if (after.startsWith("\n")) {
|
|
96
|
+
after = after.slice(1);
|
|
97
|
+
}
|
|
98
|
+
if (before.endsWith("\n\n")) {
|
|
99
|
+
before = before.slice(0, -1);
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(filePath, before + after, "utf8");
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function hasClaudeMdBlock(filePath) {
|
|
109
|
+
try {
|
|
110
|
+
if (!existsSync(filePath)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const content = readFileSync(filePath, "utf8");
|
|
114
|
+
return (content.includes(SESSIONMEM_BLOCK_START) &&
|
|
115
|
+
content.includes(SESSIONMEM_BLOCK_END));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/adapters/generic.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { forgetMemoryRequestSchema, getMemoryRequestSchema, listMemoriesRequestSchema, retrieveMemoriesRequestSchema, statsRequestSchema, storeMemoryRequestSchema, } from "../core/api/contracts.js";
|
|
3
|
+
import { batchStoreMemoryRequestSchema, forgetMemoryRequestSchema, getMemoryRequestSchema, listMemoriesRequestSchema, resetAccessCountsRequestSchema, retrieveMemoriesRequestSchema, statsRequestSchema, storeMemoryRequestSchema, } from "../core/api/contracts.js";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { createCliContext } from "../cli/context.js";
|
|
6
6
|
import { IDEInstaller } from "./ide/installer.js";
|
|
7
|
+
import { FallbackToolRegistrar } from "./capabilities/fallbackTools.js";
|
|
7
8
|
/**
|
|
8
9
|
* Diagnostic logging sink for the stdio server. CRITICAL: the MCP protocol
|
|
9
10
|
* frames are written to STDOUT by StdioServerTransport, so anything this server
|
|
@@ -24,34 +25,90 @@ function shapeWithoutProjectId(shape) {
|
|
|
24
25
|
const TOOL_DEFINITIONS = [
|
|
25
26
|
{
|
|
26
27
|
method: "retrieveMemories",
|
|
27
|
-
description: "
|
|
28
|
-
|
|
28
|
+
description: "Semantically search stored memories and return the top matches ranked by a weighted combination of relevance, recency, and importance. Read-only; no side effects.\n\n" +
|
|
29
|
+
"WHEN TO CALL: (1) At the start of every session — pass the current task or file as the query to pre-load relevant context. (2) Mid-session whenever a new topic, file, or decision area arises that may have prior context. Do NOT call on every user turn.\n\n" +
|
|
30
|
+
"WHEN NOT TO CALL: If you already retrieved memories for this topic this session. Use getMemory if you have a specific memoryId. Use listMemories only to audit the full store, not for context loading.\n\n" +
|
|
31
|
+
"Returns up to `limit` results (default 20). `mode='auto'` is the standard startup path; `mode='on-demand'` signals an explicit mid-session lookup. `depth='deep'` runs a broader semantic sweep at higher latency — use when the topic is unfamiliar. Phrase `query` as what you need to recall, not what you are about to do.",
|
|
32
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
33
|
+
inputShape: {
|
|
34
|
+
query: retrieveMemoriesRequestSchema.shape.query.describe("Natural-language description of what you need to recall. Phrase as a topic or question (e.g. 'database connection settings', 'auth flow decisions') — not an action ('store info about...')."),
|
|
35
|
+
limit: retrieveMemoriesRequestSchema.shape.limit.describe("Maximum number of memories to return. Integer 1-100, default 20. Increase for broad topic sweeps; keep at default for focused lookups."),
|
|
36
|
+
mode: retrieveMemoriesRequestSchema.shape.mode.describe("'auto' for the standard startup context-load path. 'on-demand' for an explicit mid-session retrieval triggered by a specific task or question."),
|
|
37
|
+
depth: retrieveMemoriesRequestSchema.shape.depth.describe("'default' for standard semantic search. 'deep' for a broader sweep that surfaces less-similar memories — use when the topic is new or unfamiliar."),
|
|
38
|
+
},
|
|
29
39
|
},
|
|
30
40
|
{
|
|
31
41
|
method: "storeMemory",
|
|
32
|
-
description: "
|
|
33
|
-
|
|
42
|
+
description: "Persist a single memory unit to the local SQLite store. Accepts decisions, facts, architectural choices, warnings, and session summaries. NOT idempotent — each call creates a new record even with identical content. Writes to disk immediately.\n\n" +
|
|
43
|
+
"WHEN TO CALL: After any significant decision, discovery, or conclusion that should be available in a future session. Good candidates: technology choices, non-obvious constraints, bug root-causes, architectural decisions, key facts about the codebase.\n\n" +
|
|
44
|
+
"WHEN NOT TO CALL: For trivial observations, transient state, or content that duplicates what was just retrieved. Do not store entire files or full conversation transcripts.\n\n" +
|
|
45
|
+
"`kind` categories: 'decision', 'fact', 'summary', 'warning', 'architecture'. Write `content` to be self-contained — it must be useful without any surrounding conversation context. `importance` 1-10 (10 = most critical); directly affects retrieval ranking in future sessions.",
|
|
46
|
+
annotations: { destructiveHint: false, idempotentHint: false },
|
|
47
|
+
inputShape: {
|
|
48
|
+
memoryId: storeMemoryRequestSchema.shape.memoryId.describe("Caller-supplied unique UUID for this memory (e.g. crypto.randomUUID()). Used for deduplication and for later retrieval by ID via getMemory."),
|
|
49
|
+
sessionId: storeMemoryRequestSchema.shape.sessionId.describe("Identifier for the current session. Used to group memories by session for diagnostics. Use a consistent ID within a single session."),
|
|
50
|
+
sourceAdapter: storeMemoryRequestSchema.shape.sourceAdapter.describe("Name of the adapter or host creating this memory (e.g. 'claude-code', 'cursor', 'generic'). Used for provenance tracking."),
|
|
51
|
+
kind: storeMemoryRequestSchema.shape.kind.describe("Category of this memory. Recommended values: 'decision', 'fact', 'summary', 'warning', 'architecture'. Any non-empty string is valid."),
|
|
52
|
+
content: storeMemoryRequestSchema.shape.content.describe("The memory text. Must be self-contained and specific — written so it is useful without surrounding conversation context. Avoid vague phrases like 'the user decided to...'."),
|
|
53
|
+
importance: storeMemoryRequestSchema.shape.importance.describe("Integer 1-10 indicating criticality (10 = most important). Directly affects ranking in future retrieveMemories calls. Use 8-10 for decisions that must not be forgotten; 3-5 for useful but non-critical facts."),
|
|
54
|
+
redactionEnabled: storeMemoryRequestSchema.shape.redactionEnabled.describe("If true, PII is stripped from content before storage. Omit to use the project-level redaction setting from config.json."),
|
|
55
|
+
},
|
|
34
56
|
},
|
|
35
57
|
{
|
|
36
58
|
method: "listMemories",
|
|
37
|
-
description: "
|
|
59
|
+
description: "Return every memory stored for the current project, unfiltered and without ranking. Read-only; no side effects.\n\n" +
|
|
60
|
+
"WHEN TO CALL: When you need a complete inventory of stored memories — to audit what has been saved, detect duplicates, or build a full summary of all known context.\n\n" +
|
|
61
|
+
"WHEN NOT TO CALL: For normal context loading at session start — use retrieveMemories instead, which ranks by relevance. listMemories returns the entire store unfiltered and can be very large.",
|
|
62
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
38
63
|
inputShape: shapeWithoutProjectId(listMemoriesRequestSchema.shape),
|
|
39
64
|
},
|
|
40
65
|
{
|
|
41
66
|
method: "getMemory",
|
|
42
|
-
description: "Fetch a single
|
|
43
|
-
|
|
67
|
+
description: "Fetch a single memory record by its exact ID. Returns the full record: content, kind, importance, timestamps, and session metadata. Read-only; no side effects.\n\n" +
|
|
68
|
+
"WHEN TO CALL: When you already have a specific memoryId from a prior retrieveMemories or listMemories result and need its full detail.\n\n" +
|
|
69
|
+
"WHEN NOT TO CALL: For topic-based search — use retrieveMemories for that. This tool requires an exact ID and does not search by content.",
|
|
70
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
71
|
+
inputShape: {
|
|
72
|
+
memoryId: getMemoryRequestSchema.shape.memoryId.describe("Exact UUID of the memory to fetch. Obtain from a prior retrieveMemories or listMemories result."),
|
|
73
|
+
},
|
|
44
74
|
},
|
|
45
75
|
{
|
|
46
76
|
method: "forgetMemory",
|
|
47
|
-
description: "
|
|
48
|
-
|
|
77
|
+
description: "Permanently delete a single memory by ID. The record is removed from the local SQLite store immediately and CANNOT be recovered. Destructive and irreversible.\n\n" +
|
|
78
|
+
"WHEN TO CALL: Only when a memory is known to be incorrect, dangerously outdated, or a duplicate that would mislead future sessions.\n\n" +
|
|
79
|
+
"WHEN NOT TO CALL: If there is any doubt. A memory that is merely old or low-relevance does not need deletion — retrieval ranking deprioritizes it automatically.",
|
|
80
|
+
annotations: { destructiveHint: true, idempotentHint: false },
|
|
81
|
+
inputShape: {
|
|
82
|
+
memoryId: forgetMemoryRequestSchema.shape.memoryId.describe("Exact UUID of the memory to permanently delete. Obtain from a prior listMemories or retrieveMemories call. Deletion is immediate and irreversible."),
|
|
83
|
+
},
|
|
49
84
|
},
|
|
50
85
|
{
|
|
51
86
|
method: "stats",
|
|
52
|
-
description: "
|
|
87
|
+
description: "Return aggregate statistics for the current project: total stored memory count and total ingested session event count. Read-only; no side effects.\n\n" +
|
|
88
|
+
"WHEN TO CALL: For diagnostic or monitoring purposes — to confirm memories were stored after a session, check store health, or report usage numbers.\n\n" +
|
|
89
|
+
"WHEN NOT TO CALL: As part of normal context loading. stats returns counts only, not content; use retrieveMemories to load actual context.",
|
|
90
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
53
91
|
inputShape: shapeWithoutProjectId(statsRequestSchema.shape),
|
|
54
92
|
},
|
|
93
|
+
{
|
|
94
|
+
method: "resetAccessCounts",
|
|
95
|
+
description: "Reset access-pattern counters for all memories in the current project. Sets access_count to 0 and clears last_accessed timestamps without deleting any memories. Useful after large refactors when old access patterns no longer reflect current relevance.\n\n" +
|
|
96
|
+
"WHEN TO CALL: After major codebase restructuring, project pivots, or when access-boosted rankings no longer reflect current relevance.\n\n" +
|
|
97
|
+
"WHEN NOT TO CALL: During normal operation — access patterns self-correct as usage shifts.",
|
|
98
|
+
annotations: { destructiveHint: false, idempotentHint: true },
|
|
99
|
+
inputShape: shapeWithoutProjectId(resetAccessCountsRequestSchema.shape),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
method: "batchStoreMemory",
|
|
103
|
+
description: "Persist multiple memory units in a single atomic SQLite transaction. Significantly faster than calling storeMemory repeatedly for session-end writes of 10-20 memories.\n\n" +
|
|
104
|
+
"WHEN TO CALL: At session end or whenever you have multiple memories to store at once. Reduces overhead from per-insert fsync by wrapping all writes in one transaction.\n\n" +
|
|
105
|
+
"WHEN NOT TO CALL: For a single memory — use storeMemory instead. For imports from external files — use importMemories.\n\n" +
|
|
106
|
+
"Each item in the `memories` array follows the same schema as storeMemory (memoryId, sessionId, sourceAdapter, kind, content, importance). Invalid items are reported individually; valid items are still stored atomically.",
|
|
107
|
+
annotations: { destructiveHint: false, idempotentHint: false },
|
|
108
|
+
inputShape: {
|
|
109
|
+
memories: batchStoreMemoryRequestSchema.shape.memories.describe("Array of memory objects to store. Each must include: memoryId (unique UUID), sessionId, sourceAdapter, kind, content (self-contained text), importance (1-10). Minimum 1 item."),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
55
112
|
];
|
|
56
113
|
export class GenericMCPAdapter {
|
|
57
114
|
name = "Generic MCP";
|
|
@@ -91,12 +148,13 @@ export class GenericMCPAdapter {
|
|
|
91
148
|
const { service, projectId } = ctx;
|
|
92
149
|
const server = new McpServer({
|
|
93
150
|
name: "sessionmem",
|
|
94
|
-
version: "1.0.
|
|
151
|
+
version: "1.0.5",
|
|
95
152
|
});
|
|
96
153
|
for (const def of TOOL_DEFINITIONS) {
|
|
97
154
|
server.registerTool(def.method, {
|
|
98
155
|
description: def.description,
|
|
99
156
|
inputSchema: def.inputShape,
|
|
157
|
+
...(def.annotations ? { annotations: def.annotations } : {}),
|
|
100
158
|
}, async (args) => {
|
|
101
159
|
// Inject the server-resolved projectId; clients never set it.
|
|
102
160
|
const request = { ...args, projectId };
|
|
@@ -122,6 +180,19 @@ export class GenericMCPAdapter {
|
|
|
122
180
|
};
|
|
123
181
|
});
|
|
124
182
|
}
|
|
183
|
+
// Register fallback tools for hosts that lack resource or prompt support.
|
|
184
|
+
// These provide fetch_memories and startup_inject_memories as tool-based
|
|
185
|
+
// alternatives, wired to the same service instance used by TOOL_DEFINITIONS.
|
|
186
|
+
const fallbackTools = FallbackToolRegistrar.getFallbackTools(this.capabilities, {
|
|
187
|
+
service,
|
|
188
|
+
projectId,
|
|
189
|
+
});
|
|
190
|
+
for (const fallback of fallbackTools) {
|
|
191
|
+
server.registerTool(fallback.name, { description: fallback.description, inputSchema: fallback.inputShape }, async (args) => {
|
|
192
|
+
const result = await fallback.execute(args);
|
|
193
|
+
return { content: [{ type: "text", text: result }] };
|
|
194
|
+
});
|
|
195
|
+
}
|
|
125
196
|
logDiagnostic(`Starting Generic MCP server over stdio (project: ${projectId})`);
|
|
126
197
|
await server.connect(new StdioServerTransport());
|
|
127
198
|
}
|
|
@@ -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:
|
|
14
|
+
version: pkg.version,
|
|
12
15
|
message: "sessionmem MCP server is operational.",
|
|
13
16
|
};
|
|
14
17
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
2
4
|
import { AdapterFactory } from "../../adapters/factory.js";
|
|
5
|
+
import { injectClaudeMdBlock } from "../../adapters/claudeMdInjector.js";
|
|
3
6
|
import { createCliContext } from "../context.js";
|
|
4
7
|
import { configFilePath, writePolicyConfig, DEFAULT_POLICY_CONFIG, } from "../../core/config/policyConfig.js";
|
|
5
8
|
export const MANUAL_CONFIG_BLOCK = JSON.stringify({
|
|
@@ -52,6 +55,20 @@ export async function installCommand(_options, contextOverrides) {
|
|
|
52
55
|
process.exit(1);
|
|
53
56
|
}
|
|
54
57
|
console.log(`✓ ${adapter.name} config updated`);
|
|
55
|
-
// Step 3:
|
|
58
|
+
// Step 3: CLAUDE.md injection — non-fatal
|
|
59
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
60
|
+
try {
|
|
61
|
+
const injected = injectClaudeMdBlock(claudeMdPath);
|
|
62
|
+
if (injected) {
|
|
63
|
+
console.log(`✓ CLAUDE.md instructions injected (${claudeMdPath})`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error("✗ CLAUDE.md injection failed (non-fatal)");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
console.error("✗ CLAUDE.md injection failed (non-fatal)");
|
|
71
|
+
}
|
|
72
|
+
// Step 4: Full success checklist
|
|
56
73
|
console.log("✓ sessionmem ready");
|
|
57
74
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createCliContext } from "../context.js";
|
|
2
|
+
import { deterministicEmbed } from "../../core/embed/deterministicEmbed.js";
|
|
3
|
+
import { EMBEDDING_VERSION } from "../../core/embed/embeddingVersion.js";
|
|
4
|
+
const DEFAULT_EMBEDDING_DIMENSION = 32;
|
|
5
|
+
/**
|
|
6
|
+
* `sessionmem re-embed`
|
|
7
|
+
*
|
|
8
|
+
* Bulk-update embeddings for all memories whose embedding_version does not
|
|
9
|
+
* match the current EMBEDDING_VERSION. Recomputes each embedding with
|
|
10
|
+
* deterministicEmbed and writes the new vector + version back to the row.
|
|
11
|
+
*/
|
|
12
|
+
export async function reEmbedCommand(ctx) {
|
|
13
|
+
const context = ctx ?? createCliContext();
|
|
14
|
+
const { db } = context;
|
|
15
|
+
const stale = db
|
|
16
|
+
.prepare(`
|
|
17
|
+
SELECT id, content, embedding_dim
|
|
18
|
+
FROM memories
|
|
19
|
+
WHERE embedding_version IS NULL OR embedding_version != ?
|
|
20
|
+
`)
|
|
21
|
+
.all(EMBEDDING_VERSION);
|
|
22
|
+
const total = stale.length;
|
|
23
|
+
if (total === 0) {
|
|
24
|
+
console.log("All embeddings are up to date.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(`Found ${total} memories with stale embeddings. Re-embedding...`);
|
|
28
|
+
const updateStmt = db.prepare(`
|
|
29
|
+
UPDATE memories
|
|
30
|
+
SET embedding = ?, embedding_dim = ?, embedding_version = ?
|
|
31
|
+
WHERE id = ?
|
|
32
|
+
`);
|
|
33
|
+
let count = 0;
|
|
34
|
+
const runAll = db.transaction(() => {
|
|
35
|
+
for (const row of stale) {
|
|
36
|
+
const dim = row.embedding_dim ?? DEFAULT_EMBEDDING_DIMENSION;
|
|
37
|
+
const result = deterministicEmbed(row.content, dim);
|
|
38
|
+
updateStmt.run(JSON.stringify(result.vector), result.dimension, EMBEDDING_VERSION, row.id);
|
|
39
|
+
count += 1;
|
|
40
|
+
if (count % 100 === 0 || count === total) {
|
|
41
|
+
console.log(` ${count}/${total}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
runAll();
|
|
46
|
+
console.log(`Re-embedded ${count} memories to version ${EMBEDDING_VERSION}.`);
|
|
47
|
+
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -4,10 +4,32 @@ import { join } from "path";
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
export async function runMcpServer() {
|
|
6
6
|
const adapter = AdapterFactory.detectAdapter();
|
|
7
|
-
//
|
|
7
|
+
// Startup diagnostics: written to ~/.sessionmem/logs/mcp.log
|
|
8
8
|
const logDir = join(homedir(), ".sessionmem", "logs");
|
|
9
9
|
const logPath = join(logDir, "mcp.log");
|
|
10
|
-
|
|
10
|
+
// Derive db path for diagnostics (mirrors context.ts defaultDbPath)
|
|
11
|
+
const envDbPath = process.env.SESSIONMEM_DB_PATH;
|
|
12
|
+
const dbPath = envDbPath && envDbPath.trim() !== ""
|
|
13
|
+
? envDbPath
|
|
14
|
+
: join(homedir(), ".sessionmem", "memories.db");
|
|
15
|
+
// Derive project ID for diagnostics (mirrors context.ts deriveProjectId)
|
|
16
|
+
const envProjectId = process.env.SESSIONMEM_PROJECT_ID;
|
|
17
|
+
let projectId;
|
|
18
|
+
if (envProjectId && envProjectId.trim() !== "") {
|
|
19
|
+
projectId = envProjectId;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
const parts = cwd.replace(/\\/g, "/").split("/");
|
|
24
|
+
const raw = parts[parts.length - 1] || "default";
|
|
25
|
+
const sanitized = raw.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
26
|
+
projectId =
|
|
27
|
+
sanitized === "" || sanitized === "." || sanitized === ".."
|
|
28
|
+
? "default"
|
|
29
|
+
: sanitized;
|
|
30
|
+
}
|
|
31
|
+
const adapterName = adapter.name;
|
|
32
|
+
const logMessage = `[${new Date().toISOString()}] Started sessionmem | adapter=${adapterName} db=${dbPath} project=${projectId}\n`;
|
|
11
33
|
try {
|
|
12
34
|
mkdirSync(logDir, { recursive: true });
|
|
13
35
|
writeFileSync(logPath, logMessage, { flag: "a" });
|
|
@@ -15,6 +37,10 @@ export async function runMcpServer() {
|
|
|
15
37
|
catch {
|
|
16
38
|
// best-effort logging; ignore failures
|
|
17
39
|
}
|
|
40
|
+
// Debug output to stderr (never stdout — that's the MCP protocol channel)
|
|
41
|
+
if (process.env.SESSIONMEM_DEBUG === "1") {
|
|
42
|
+
process.stderr.write(`[sessionmem] db=${dbPath} project=${projectId} adapter=${adapterName}\n`);
|
|
43
|
+
}
|
|
18
44
|
// Start the server
|
|
19
45
|
if (adapter.startMcpServer) {
|
|
20
46
|
await adapter.startMcpServer();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createCliContext } from "../context.js";
|
|
2
|
+
import { countTokens } from "../../core/injection/tokenBudget.js";
|
|
3
|
+
import { listMemoriesByProject } from "../../core/storage/memoryRepo.js";
|
|
4
|
+
import { countDistinctSessions, listEventPayloads, } from "../../core/storage/tokenSavingsRepo.js";
|
|
5
|
+
import { readPolicyConfig, configFilePath, } from "../../core/config/policyConfig.js";
|
|
6
|
+
/** Default injection token cap, mirroring formatStartupInjection.ts. */
|
|
7
|
+
const DEFAULT_INJECTION_CAP = 450;
|
|
8
|
+
export function savingsCommand(ctx, options) {
|
|
9
|
+
const context = ctx ?? createCliContext();
|
|
10
|
+
const { db, projectId } = context;
|
|
11
|
+
// --- gather raw numbers ---
|
|
12
|
+
const memoryTokens = listMemoriesByProject(db, projectId).reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
13
|
+
const rawEventTokens = listEventPayloads(db, projectId).reduce((sum, p) => sum + countTokens(p), 0);
|
|
14
|
+
const sessions = countDistinctSessions(db, projectId);
|
|
15
|
+
// Read the injection cap from policy config if it exists; fall back to 450.
|
|
16
|
+
const _config = readPolicyConfig(options?.configPath ?? configFilePath());
|
|
17
|
+
const injectionCap = _config.injectionCap ??
|
|
18
|
+
DEFAULT_INJECTION_CAP;
|
|
19
|
+
// --- calculations ---
|
|
20
|
+
const tokensSaved = rawEventTokens - memoryTokens;
|
|
21
|
+
const savingsPct = rawEventTokens > 0 ? (tokensSaved / rawEventTokens) * 100 : 0;
|
|
22
|
+
const estimatedReexplainTokens = memoryTokens * 3;
|
|
23
|
+
const injectionSavings = estimatedReexplainTokens - sessions * injectionCap;
|
|
24
|
+
const overallSaved = tokensSaved + Math.max(0, injectionSavings);
|
|
25
|
+
const overallPct = rawEventTokens + estimatedReexplainTokens > 0
|
|
26
|
+
? (overallSaved / (rawEventTokens + estimatedReexplainTokens)) * 100
|
|
27
|
+
: 0;
|
|
28
|
+
const avgInjectionCost = sessions > 0
|
|
29
|
+
? Math.round((sessions * injectionCap) / sessions)
|
|
30
|
+
: 0;
|
|
31
|
+
// --- JSON output ---
|
|
32
|
+
if (options?.json) {
|
|
33
|
+
const payload = {
|
|
34
|
+
memoryTokens,
|
|
35
|
+
rawEventTokens,
|
|
36
|
+
tokensSaved,
|
|
37
|
+
savingsPct: Math.round(savingsPct * 10) / 10,
|
|
38
|
+
sessions,
|
|
39
|
+
injectionCap,
|
|
40
|
+
estimatedReexplainTokens,
|
|
41
|
+
injectionSavings,
|
|
42
|
+
overallSaved,
|
|
43
|
+
overallPct: Math.round(overallPct * 10) / 10,
|
|
44
|
+
};
|
|
45
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// --- empty state ---
|
|
49
|
+
if (rawEventTokens === 0 && memoryTokens === 0) {
|
|
50
|
+
process.stdout.write("No session data yet. Token savings will appear after your first session.\n");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// --- formatted report ---
|
|
54
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
55
|
+
const lines = [
|
|
56
|
+
"sessionmem token savings",
|
|
57
|
+
"",
|
|
58
|
+
"Storage compression:",
|
|
59
|
+
` Raw session tokens: ${fmt(rawEventTokens).padStart(10)}`,
|
|
60
|
+
` Memory tokens: ${fmt(memoryTokens).padStart(10)}`,
|
|
61
|
+
` Tokens saved: ${fmt(tokensSaved).padStart(10)} (${(Math.round(savingsPct * 10) / 10).toFixed(1)}%)`,
|
|
62
|
+
"",
|
|
63
|
+
"Session injection:",
|
|
64
|
+
` Total sessions: ${fmt(sessions).padStart(10)}`,
|
|
65
|
+
` Avg injection cost: ${fmt(avgInjectionCost).padStart(10)} tokens/session`,
|
|
66
|
+
` Est. re-explain cost:${fmt(estimatedReexplainTokens).padStart(10)} tokens (without sessionmem)`,
|
|
67
|
+
` Injection savings: ${fmt(Math.max(0, injectionSavings)).padStart(10)} tokens across ${fmt(sessions)} sessions`,
|
|
68
|
+
"",
|
|
69
|
+
"Overall:",
|
|
70
|
+
` Total tokens saved: ${fmt(overallSaved).padStart(10)}`,
|
|
71
|
+
` Efficiency: ${(Math.round(overallPct * 10) / 10).toFixed(1)}%`,
|
|
72
|
+
"",
|
|
73
|
+
];
|
|
74
|
+
process.stdout.write(lines.join("\n"));
|
|
75
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, rmSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { AdapterFactory } from "../../adapters/factory.js";
|
|
5
|
+
import { removeClaudeMdBlock } from "../../adapters/claudeMdInjector.js";
|
|
5
6
|
export async function uninstallCommand(options = {}) {
|
|
6
7
|
const adapter = AdapterFactory.detectAdapter();
|
|
7
8
|
if (!adapter.uninstall) {
|
|
@@ -14,6 +15,15 @@ export async function uninstallCommand(options = {}) {
|
|
|
14
15
|
process.exit(1);
|
|
15
16
|
}
|
|
16
17
|
console.log(`✓ ${adapter.name} config removed`);
|
|
18
|
+
// CLAUDE.md cleanup — non-fatal
|
|
19
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
20
|
+
try {
|
|
21
|
+
removeClaudeMdBlock(claudeMdPath);
|
|
22
|
+
console.log("✓ CLAUDE.md instructions removed");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error("✗ CLAUDE.md cleanup failed (non-fatal)");
|
|
26
|
+
}
|
|
17
27
|
// Resolve dbPath: use injected override (for tests) or the default location
|
|
18
28
|
const dbPath = options.dbPath ?? join(homedir(), ".sessionmem", "memories.db");
|
|
19
29
|
if (options.purge) {
|
package/dist/cli/index.js
CHANGED
|
@@ -12,11 +12,13 @@ import { forgetCommand } from "./commands/forget.js";
|
|
|
12
12
|
import { exportCommand } from "./commands/export.js";
|
|
13
13
|
import { importCommand } from "./commands/import.js";
|
|
14
14
|
import { statsCommand } from "./commands/stats.js";
|
|
15
|
+
import { savingsCommand } from "./commands/savings.js";
|
|
15
16
|
import { redactScanCommand } from "./commands/redactScan.js";
|
|
16
17
|
import { retentionPruneCommand } from "./commands/retention.js";
|
|
17
18
|
import { configGetCommand, configSetCommand } from "./commands/config.js";
|
|
18
19
|
import { teamEnableCommand, teamDisableCommand, teamStatusCommand, } from "./commands/team.js";
|
|
19
20
|
import { syncCommand } from "./commands/sync.js";
|
|
21
|
+
import { reEmbedCommand } from "./commands/reEmbed.js";
|
|
20
22
|
// Source the version from package.json (single source of truth) so `--version`
|
|
21
23
|
// never drifts from the published manifest. createRequire + resolveJsonModule
|
|
22
24
|
// reads the manifest relative to this module; from dist/cli/index.js the
|
|
@@ -81,6 +83,11 @@ program
|
|
|
81
83
|
.command("stats")
|
|
82
84
|
.description("Show memory statistics for the current project")
|
|
83
85
|
.action(() => statsCommand());
|
|
86
|
+
program
|
|
87
|
+
.command("savings")
|
|
88
|
+
.description("Show token savings from sessionmem compression and injection")
|
|
89
|
+
.option("--json", "Output raw metrics as JSON")
|
|
90
|
+
.action((options) => savingsCommand(undefined, options));
|
|
84
91
|
// redact-scan — one-time scrub over existing memories. Scan is the
|
|
85
92
|
// non-destructive default; --apply redacts matching rows in place.
|
|
86
93
|
program
|
|
@@ -134,6 +141,13 @@ team
|
|
|
134
141
|
.command("status")
|
|
135
142
|
.description("Show team mode state and shared-path availability")
|
|
136
143
|
.action(() => teamStatusCommand());
|
|
144
|
+
// re-embed — bulk-update stale embeddings to the current version.
|
|
145
|
+
// reEmbedCommand declares a trailing `ctx?` test seam, so arrow-wrap to drop
|
|
146
|
+
// commander's trailing Command argument (NOTE above).
|
|
147
|
+
program
|
|
148
|
+
.command("re-embed")
|
|
149
|
+
.description("Re-embed all memories with stale embedding versions")
|
|
150
|
+
.action(() => reEmbedCommand());
|
|
137
151
|
// sync — push a local snapshot to the shared path and pull every
|
|
138
152
|
// teammate snapshot back. syncCommand declares a trailing `ctx?` test seam, so
|
|
139
153
|
// arrow-wrap to drop commander's trailing Command argument (NOTE above).
|