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.
Files changed (37) 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 +120 -0
  5. package/dist/adapters/generic.js +83 -12
  6. package/dist/adapters/tools/ping.js +4 -1
  7. package/dist/cli/commands/install.js +18 -1
  8. package/dist/cli/commands/reEmbed.js +47 -0
  9. package/dist/cli/commands/run.js +28 -2
  10. package/dist/cli/commands/savings.js +75 -0
  11. package/dist/cli/commands/uninstall.js +10 -0
  12. package/dist/cli/index.js +14 -0
  13. package/dist/cli/output.js +11 -3
  14. package/dist/core/api/contracts.js +34 -10
  15. package/dist/core/api/memoryCoreService.js +188 -86
  16. package/dist/core/api/sessionLifecycleService.js +12 -2
  17. package/dist/core/config/policyConfig.js +20 -0
  18. package/dist/core/injection/formatStartupInjection.js +2 -1
  19. package/dist/core/injection/tokenBudget.js +8 -0
  20. package/dist/core/retrieve/importance.js +4 -3
  21. package/dist/core/retrieve/recencyBands.js +3 -10
  22. package/dist/core/retrieve/retrieveMemories.js +17 -4
  23. package/dist/core/retrieve/score.js +11 -1
  24. package/dist/core/schema/migrations/005_team_provenance.sql +9 -9
  25. package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
  26. package/dist/core/schema/migrations/007_feedback_manual_delete.sql +23 -0
  27. package/dist/core/schema/migrations/008_fts5_search.sql +33 -0
  28. package/dist/core/storage/db.js +6 -0
  29. package/dist/core/storage/memoryFeedbackRepo.js +14 -4
  30. package/dist/core/storage/memoryRepo.js +134 -120
  31. package/dist/core/storage/memorySearchRepo.js +87 -13
  32. package/dist/core/storage/sessionEventsRepo.js +19 -9
  33. package/dist/core/storage/summarizationFailuresRepo.js +36 -26
  34. package/dist/core/storage/tokenSavingsRepo.js +20 -0
  35. package/dist/core/summarize/cloudSummarizer.js +21 -5
  36. package/dist/core/summarize/localSummarizer.js +1 -10
  37. 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 tool to fetch memories because host lacks MCP resource support.",
8
- schema: {
9
- type: "object",
10
- properties: {
11
- query: { type: "string" },
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
- // Wrap core retrieval logic
17
- return `Fetched memories for ${args.query}`;
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 tool to manually request startup injection because host lacks MCP prompt support.",
25
- schema: {
26
- type: "object",
27
- properties: {},
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
- return "Startup memories injected.";
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
+ }
@@ -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: "Retrieve the most relevant stored memories for a semantic query, ranked by relevance, recency, and importance.",
28
- inputShape: shapeWithoutProjectId(retrieveMemoriesRequestSchema.shape),
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: "Store a memory (decision, fact, summary, or warning) for the current project.",
33
- inputShape: shapeWithoutProjectId(storeMemoryRequestSchema.shape),
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: "List all stored memories for the current project.",
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 stored memory by its ID.",
43
- inputShape: shapeWithoutProjectId(getMemoryRequestSchema.shape),
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: "Delete a stored memory by its ID.",
48
- inputShape: shapeWithoutProjectId(forgetMemoryRequestSchema.shape),
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: "Report memory statistics (total memories and session events) for the current project.",
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.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: "0.1.0",
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: Full success checklist
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
+ }
@@ -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
- // Setup rudimentary log for debugging manual configs
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
- const logMessage = `[${new Date().toISOString()}] Started sessionmem via ${adapter.name}\n`;
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).