@shodh/memory-mcp 0.1.80 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Shodh-Memory MCP Server</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>v0.1.70</strong> | Persistent cognitive memory for AI agents
8
+ <strong>v0.1.90</strong> | Persistent cognitive memory for AI agents
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -29,9 +29,11 @@
29
29
  - **Semantic Search**: Find memories by meaning using MiniLM-L6 embeddings
30
30
  - **Knowledge Graph**: Entity extraction and relationship tracking
31
31
  - **Memory Consolidation**: Automatic decay, replay, and strengthening
32
+ - **Idempotent**: Content-hash dedup — identical memories are never stored twice
32
33
  - **1-Click Install**: Auto-downloads native server binary for your platform
33
34
  - **Offline-First**: All models auto-downloaded on first run (~38MB total), no internet required after
34
- - **Fast**: Sub-millisecond graph lookup, 30-50ms semantic search
35
+ - **Fast**: <200ms API response, sub-millisecond graph lookup, 30-50ms semantic search
36
+ - **GTD Task Management**: Full todo system with projects, subtasks, comments, and reminders
35
37
 
36
38
  ## Installation
37
39
 
@@ -81,25 +83,80 @@ env = { SHODH_API_KEY = "your-api-key-here" }
81
83
  | `SHODH_STREAM` | Enable/disable streaming ingestion | `true` |
82
84
  | `SHODH_PROACTIVE` | Enable/disable proactive memory surfacing | `true` |
83
85
 
84
- ## MCP Tools (15 total)
86
+ ## MCP Tools (47 total)
87
+
88
+ <details>
89
+ <summary><b>Memory</b> — Store, search, and manage memories</summary>
85
90
 
86
91
  | Tool | Description |
87
92
  |------|-------------|
88
- | `remember` | Store a memory with optional type and tags |
93
+ | `remember` | Store a memory with optional type, tags, and metadata |
89
94
  | `recall` | Semantic search to find relevant memories |
90
95
  | `proactive_context` | Auto-surface relevant memories for current context |
91
96
  | `context_summary` | Get categorized context for session bootstrap |
92
97
  | `list_memories` | List all stored memories |
98
+ | `read_memory` | Read full content of a specific memory by ID |
93
99
  | `forget` | Delete a specific memory by ID |
94
- | `forget_by_tags` | Delete memories matching any of the specified tags |
95
- | `forget_by_date` | Delete memories within a date range |
100
+ | `reinforce` | Reinforce a memory (boost importance) |
101
+ </details>
102
+
103
+ <details>
104
+ <summary><b>Todos (GTD)</b> — Task management with projects and subtasks</summary>
105
+
106
+ | Tool | Description |
107
+ |------|-------------|
108
+ | `add_todo` | Create a task with priority, due date, project, contexts |
109
+ | `list_todos` | List/search todos with semantic or GTD-style filtering |
110
+ | `update_todo` | Update task properties (status, priority, notes) |
111
+ | `complete_todo` | Mark a task as done (auto-creates next for recurring) |
112
+ | `delete_todo` | Permanently delete a task |
113
+ | `reorder_todo` | Move a task up or down within its status group |
114
+ | `list_subtasks` | List subtasks of a parent todo |
115
+ | `add_todo_comment` | Add a comment to a task (progress, resolution) |
116
+ | `list_todo_comments` | List all comments on a task |
117
+ | `update_todo_comment` | Edit an existing comment |
118
+ | `delete_todo_comment` | Delete a comment |
119
+ | `todo_stats` | Get todo statistics by status, overdue items |
120
+ </details>
121
+
122
+ <details>
123
+ <summary><b>Projects</b> — Organize todos into groups</summary>
124
+
125
+ | Tool | Description |
126
+ |------|-------------|
127
+ | `add_project` | Create a project with optional parent (sub-projects) |
128
+ | `list_projects` | List all projects with todo counts |
129
+ | `archive_project` | Archive a project (hidden but restorable) |
130
+ | `delete_project` | Permanently delete a project |
131
+ </details>
132
+
133
+ <details>
134
+ <summary><b>Reminders</b> — Time, duration, and context-triggered reminders</summary>
135
+
136
+ | Tool | Description |
137
+ |------|-------------|
138
+ | `set_reminder` | Set a reminder (time, duration, or keyword trigger) |
139
+ | `list_reminders` | List pending/triggered/dismissed reminders |
140
+ | `dismiss_reminder` | Acknowledge a triggered reminder |
141
+ </details>
142
+
143
+ <details>
144
+ <summary><b>System</b> — Health, backups, and diagnostics</summary>
145
+
146
+ | Tool | Description |
147
+ |------|-------------|
96
148
  | `memory_stats` | Get statistics about stored memories |
97
- | `recall_by_tags` | Find memories by tag |
98
- | `recall_by_date` | Find memories within a date range |
99
- | `verify_index` | Check vector index health |
100
- | `repair_index` | Repair orphaned memories |
149
+ | `verify_index` | Check vector index integrity |
150
+ | `repair_index` | Re-index orphaned memories |
151
+ | `token_status` | Get current session token usage |
152
+ | `reset_token_session` | Reset token counter for new session |
101
153
  | `consolidation_report` | View memory consolidation activity |
102
- | `streaming_status` | Check WebSocket streaming connection status |
154
+ | `backup_create` | Create a backup of all memories |
155
+ | `backup_list` | List available backups |
156
+ | `backup_verify` | Verify backup integrity (SHA-256) |
157
+ | `backup_restore` | Restore from a backup |
158
+ | `backup_purge` | Purge old backups, keep most recent N |
159
+ </details>
103
160
 
104
161
  ## REST API (for Developers)
105
162
 
package/dist/index.js CHANGED
@@ -4881,28 +4881,125 @@ class StdioServerTransport {
4881
4881
  import { spawn } from "child_process";
4882
4882
  import * as path from "path";
4883
4883
  import * as fs from "fs";
4884
+ import * as crypto from "crypto";
4884
4885
  import { fileURLToPath } from "url";
4885
- var __filename2 = fileURLToPath(import.meta.url);
4886
- var __dirname2 = path.dirname(__filename2);
4887
- var API_URL = process.env.SHODH_API_URL || "http://127.0.0.1:3030";
4886
+
4887
+ // security-utils.ts
4888
+ function isLocalHostFromUrl(apiUrl) {
4889
+ try {
4890
+ const url = new URL(apiUrl);
4891
+ const host = url.hostname;
4892
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "0.0.0.0";
4893
+ } catch {
4894
+ return false;
4895
+ }
4896
+ }
4897
+ function shouldWarnInsecureApiUrl(apiUrl, allowHttpEnv) {
4898
+ return !isLocalHostFromUrl(apiUrl) && apiUrl.startsWith("http://") && allowHttpEnv !== "true";
4899
+ }
4900
+ function serializeAndValidateBody(body, maxLength) {
4901
+ const serialized = JSON.stringify(body);
4902
+ if (serialized.length > maxLength) {
4903
+ return { ok: false, error: `Request body exceeds maximum length of ${maxLength} characters` };
4904
+ }
4905
+ return { ok: true, serialized };
4906
+ }
4907
+ function nextReconnectDelay(currentDelayMs, maxDelayMs) {
4908
+ const safeCurrent = Math.max(currentDelayMs, 1000);
4909
+ return Math.min(safeCurrent * 2, maxDelayMs);
4910
+ }
4911
+
4912
+ // string-utils.ts
4913
+ function stripSystemNoise(text) {
4914
+ let result = text;
4915
+ const tagPatterns = [
4916
+ /<task-notification>[\s\S]*?<\/task-notification>/g,
4917
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
4918
+ /<shodh-context[\s\S]*?<\/shodh-context>/g,
4919
+ /<shodh-memory[\s\S]*?<\/shodh-memory>/g,
4920
+ /<command-name>[\s\S]*?<\/command-name>/g
4921
+ ];
4922
+ for (const pattern of tagPatterns) {
4923
+ result = result.replace(pattern, "");
4924
+ }
4925
+ result = result.replace(/\s{3,}/g, " ").trim();
4926
+ return result;
4927
+ }
4928
+
4929
+ // index.ts
4930
+ var __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath(import.meta.url) : "";
4931
+ var __dirname2 = __filename2 ? path.dirname(__filename2) : process.cwd();
4932
+ function resolveApiUrl() {
4933
+ if (process.env.SHODH_API_URL)
4934
+ return process.env.SHODH_API_URL;
4935
+ const host = process.env.SHODH_HOST;
4936
+ const port = process.env.SHODH_PORT;
4937
+ if (host) {
4938
+ const scheme = port === "443" ? "https" : "http";
4939
+ const portSuffix = port && port !== "443" && port !== "80" ? `:${port}` : "";
4940
+ return `${scheme}://${host}${portSuffix}`;
4941
+ }
4942
+ if (port)
4943
+ return `http://127.0.0.1:${port}`;
4944
+ return "http://127.0.0.1:3030";
4945
+ }
4946
+ var API_URL = resolveApiUrl();
4888
4947
  var WS_URL = API_URL.replace(/^http/, "ws") + "/api/stream";
4889
4948
  var USER_ID = process.env.SHODH_USER_ID || "claude-code";
4890
- var API_KEY = process.env.SHODH_API_KEY;
4949
+ function isLocalServer() {
4950
+ try {
4951
+ const url = new URL(API_URL);
4952
+ const host = url.hostname;
4953
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "0.0.0.0";
4954
+ } catch {
4955
+ return false;
4956
+ }
4957
+ }
4958
+ var SANDBOX_MODE = process.env.SMITHERY_SANDBOX === "true";
4959
+ var API_KEY = "";
4960
+ var apiKeySource = "";
4961
+ if (process.env.SHODH_API_KEY) {
4962
+ API_KEY = process.env.SHODH_API_KEY;
4963
+ apiKeySource = "SHODH_API_KEY";
4964
+ } else if (process.env.SHODH_DEV_API_KEY) {
4965
+ API_KEY = process.env.SHODH_DEV_API_KEY;
4966
+ apiKeySource = "SHODH_DEV_API_KEY";
4967
+ } else if (process.env.SHODH_API_KEYS?.split(",")[0]?.trim()) {
4968
+ API_KEY = process.env.SHODH_API_KEYS.split(",")[0].trim();
4969
+ apiKeySource = "SHODH_API_KEYS";
4970
+ } else if (SANDBOX_MODE) {
4971
+ API_KEY = "sandbox";
4972
+ apiKeySource = "sandbox";
4973
+ }
4891
4974
  if (!API_KEY) {
4892
- console.error("ERROR: SHODH_API_KEY environment variable not set.");
4893
- console.error("");
4894
- console.error("To fix, add to your MCP config (claude_desktop_config.json or mcp.json):");
4895
- console.error(` "env": { "SHODH_API_KEY": "your-api-key" }`);
4896
- console.error("");
4897
- console.error("Or set in your shell:");
4898
- console.error(" export SHODH_API_KEY=your-api-key");
4899
- console.error("");
4900
- console.error("For local development, use the same key set in SHODH_DEV_API_KEY on the server.");
4901
- process.exit(1);
4975
+ if (isLocalServer()) {
4976
+ API_KEY = crypto.randomBytes(32).toString("hex");
4977
+ apiKeySource = "auto-generated";
4978
+ console.error("[shodh-memory] No API key set — auto-generated for local server.");
4979
+ } else {
4980
+ console.error("ERROR: SHODH_API_KEY is required for remote servers.");
4981
+ console.error("");
4982
+ console.error("To fix, add to your MCP config (claude_desktop_config.json or mcp.json):");
4983
+ console.error(` "env": { "SHODH_API_KEY": "your-api-key" }`);
4984
+ console.error("");
4985
+ console.error("Or set in your shell:");
4986
+ console.error(" export SHODH_API_KEY=your-api-key");
4987
+ process.exit(1);
4988
+ }
4989
+ }
4990
+ if (apiKeySource === "SHODH_DEV_API_KEY") {
4991
+ console.error("[shodh-memory] WARNING: API key loaded from SHODH_DEV_API_KEY — this is a development key. Use SHODH_API_KEY for production.");
4992
+ } else if (apiKeySource && apiKeySource !== "auto-generated" && apiKeySource !== "sandbox") {
4993
+ console.error(`[shodh-memory] API key loaded from ${apiKeySource}.`);
4902
4994
  }
4903
4995
  var RETRY_ATTEMPTS = 3;
4904
4996
  var RETRY_DELAY_MS = 1000;
4905
4997
  var REQUEST_TIMEOUT_MS = 1e4;
4998
+ var WRITE_TIMEOUT_MS = 30000;
4999
+ if (shouldWarnInsecureApiUrl(API_URL, process.env.SHODH_ALLOW_HTTP)) {
5000
+ console.error("[shodh-memory] WARNING: Using HTTP for a non-localhost server is insecure.");
5001
+ console.error("[shodh-memory] Set SHODH_API_URL to an https:// URL, or set SHODH_ALLOW_HTTP=true to suppress this warning.");
5002
+ }
4906
5003
  var MAX_CONTENT_LENGTH = 1e5;
4907
5004
  var MAX_QUERY_LENGTH = 1e4;
4908
5005
  var MAX_LIMIT = 250;
@@ -4930,10 +5027,15 @@ var STREAM_ENABLED = process.env.SHODH_STREAM !== "false";
4930
5027
  var STREAM_MIN_CONTENT_LENGTH = 50;
4931
5028
  var PROACTIVE_SURFACING = process.env.SHODH_PROACTIVE !== "false";
4932
5029
  var PROACTIVE_MIN_CONTEXT_LENGTH = 30;
5030
+ var MAX_CONTEXT_LENGTH = 4000;
4933
5031
  var lastProactiveResponse = "";
5032
+ var lastUserContext = "";
5033
+ var proactiveCallInFlight = false;
4934
5034
  var streamSocket = null;
4935
5035
  var streamConnecting = false;
4936
5036
  var streamReconnectTimer = null;
5037
+ var streamReconnectDelay = 1000;
5038
+ var STREAM_RECONNECT_MAX_DELAY = 60000;
4937
5039
  var streamBuffer = [];
4938
5040
  var MAX_BUFFER_SIZE = 100;
4939
5041
  var streamHandshakeComplete = false;
@@ -4944,9 +5046,15 @@ async function connectStream() {
4944
5046
  streamConnecting = true;
4945
5047
  streamHandshakeComplete = false;
4946
5048
  try {
4947
- streamSocket = new WebSocket(WS_URL);
5049
+ const wsUrlWithAuth = WS_URL + (WS_URL.includes("?") ? "&" : "?") + "api_key=" + encodeURIComponent(API_KEY);
5050
+ streamSocket = new WebSocket(wsUrlWithAuth, {
5051
+ headers: {
5052
+ "X-API-Key": API_KEY
5053
+ }
5054
+ });
4948
5055
  streamSocket.onopen = () => {
4949
5056
  streamConnecting = false;
5057
+ streamReconnectDelay = 1000;
4950
5058
  console.error("[Stream] WebSocket connected to", WS_URL);
4951
5059
  const handshake = JSON.stringify({
4952
5060
  user_id: USER_ID,
@@ -4988,11 +5096,13 @@ async function connectStream() {
4988
5096
  streamConnecting = false;
4989
5097
  streamHandshakeComplete = false;
4990
5098
  if (STREAM_ENABLED && !streamReconnectTimer) {
5099
+ const delay = streamReconnectDelay;
5100
+ streamReconnectDelay = nextReconnectDelay(streamReconnectDelay, STREAM_RECONNECT_MAX_DELAY);
4991
5101
  streamReconnectTimer = setTimeout(() => {
4992
5102
  streamReconnectTimer = null;
4993
- console.error("[Stream] Attempting reconnect...");
5103
+ console.error(`[Stream] Attempting reconnect (next delay: ${streamReconnectDelay}ms)...`);
4994
5104
  connectStream().catch((e) => console.error("[Stream] Reconnect failed:", e));
4995
- }, 5000);
5105
+ }, delay);
4996
5106
  }
4997
5107
  };
4998
5108
  streamSocket.onerror = (error) => {
@@ -5063,10 +5173,12 @@ async function surfaceRelevant(context, maxResults = 3) {
5063
5173
  }),
5064
5174
  signal: controller.signal
5065
5175
  });
5066
- clearTimeout(timeoutId);
5067
- if (!response.ok)
5176
+ if (!response.ok) {
5177
+ clearTimeout(timeoutId);
5068
5178
  return null;
5179
+ }
5069
5180
  const result = await response.json();
5181
+ clearTimeout(timeoutId);
5070
5182
  return result.memories || null;
5071
5183
  } catch (e) {
5072
5184
  console.error("[Proactive] Failed to surface memories:", e);
@@ -5076,7 +5188,7 @@ async function surfaceRelevant(context, maxResults = 3) {
5076
5188
  function formatSurfacedMemories(memories) {
5077
5189
  if (!memories || memories.length === 0)
5078
5190
  return "";
5079
- const formatted = memories.map((m, i) => ` ${i + 1}. [${(m.relevance_score * 100).toFixed(0)}%] ${m.content.slice(0, 80)}...`).join(`
5191
+ const formatted = memories.map((m, i) => ` ${i + 1}. [${((m.relevance_score ?? 0) * 100).toFixed(0)}%] ${m.content.slice(0, 80)}...`).join(`
5080
5192
  `);
5081
5193
  return `
5082
5194
 
@@ -5097,7 +5209,8 @@ async function apiCall(endpoint, method = "GET", body) {
5097
5209
  for (let attempt = 1;attempt <= RETRY_ATTEMPTS; attempt++) {
5098
5210
  try {
5099
5211
  const controller = new AbortController;
5100
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
5212
+ const timeout = method === "GET" ? REQUEST_TIMEOUT_MS : WRITE_TIMEOUT_MS;
5213
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
5101
5214
  const options = {
5102
5215
  method,
5103
5216
  headers: {
@@ -5107,7 +5220,11 @@ async function apiCall(endpoint, method = "GET", body) {
5107
5220
  signal: controller.signal
5108
5221
  };
5109
5222
  if (body) {
5110
- options.body = JSON.stringify(body);
5223
+ const bodyValidation = serializeAndValidateBody(body, MAX_CONTENT_LENGTH);
5224
+ if (!bodyValidation.ok) {
5225
+ throw new Error(bodyValidation.error);
5226
+ }
5227
+ options.body = bodyValidation.serialized;
5111
5228
  }
5112
5229
  const response = await fetch(`${API_URL}${endpoint}`, options);
5113
5230
  clearTimeout(timeoutId);
@@ -5115,10 +5232,15 @@ async function apiCall(endpoint, method = "GET", body) {
5115
5232
  const errorText = await response.text().catch(() => "Unknown error");
5116
5233
  throw new Error(`API error ${response.status}: ${errorText}`);
5117
5234
  }
5118
- return await response.json();
5235
+ try {
5236
+ return await response.json();
5237
+ } catch {
5238
+ throw new Error(`API returned invalid JSON from ${endpoint}`);
5239
+ }
5119
5240
  } catch (error) {
5120
5241
  lastError = error instanceof Error ? error : new Error(String(error));
5121
- if (lastError.message.includes("API error 4")) {
5242
+ const statusMatch = lastError.message.match(/API error (\d+)/);
5243
+ if (statusMatch && parseInt(statusMatch[1], 10) >= 400 && parseInt(statusMatch[1], 10) < 500) {
5122
5244
  throw lastError;
5123
5245
  }
5124
5246
  if (attempt < RETRY_ATTEMPTS) {
@@ -5148,7 +5270,7 @@ async function isServerAvailable() {
5148
5270
  }
5149
5271
  var server = new Server({
5150
5272
  name: "shodh-memory",
5151
- version: "0.1.61"
5273
+ version: "0.1.90"
5152
5274
  }, {
5153
5275
  capabilities: {
5154
5276
  tools: {},
@@ -5220,6 +5342,57 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5220
5342
  parent_id: {
5221
5343
  type: "string",
5222
5344
  description: "Parent memory ID for hierarchical organization. Creates memory trees (e.g., '71-research' -> 'algebraic' -> '21×27≡-1')"
5345
+ },
5346
+ importance: {
5347
+ type: "number",
5348
+ description: "Optional importance override (0.0-1.0). Bypasses auto-calculation. Use for memories where importance is known: Decision=0.8, Learning=0.7, Error=0.7, Discovery=0.6, Observation=0.3"
5349
+ },
5350
+ robot_id: {
5351
+ type: "string",
5352
+ description: "Robot/drone identifier for multi-robot systems"
5353
+ },
5354
+ mission_id: {
5355
+ type: "string",
5356
+ description: "Mission identifier for grouping experiences"
5357
+ },
5358
+ geo_location: {
5359
+ type: "array",
5360
+ items: { type: "number" },
5361
+ minItems: 3,
5362
+ maxItems: 3,
5363
+ description: "GPS coordinates [latitude, longitude, altitude] in WGS84"
5364
+ },
5365
+ local_position: {
5366
+ type: "array",
5367
+ items: { type: "number" },
5368
+ minItems: 3,
5369
+ maxItems: 3,
5370
+ description: "Local position [x, y, z] in meters (robot-local frame)"
5371
+ },
5372
+ heading: {
5373
+ type: "number",
5374
+ description: "Heading in degrees (0-360)"
5375
+ },
5376
+ action_type: {
5377
+ type: "string",
5378
+ description: "Action type name (e.g., 'navigate', 'grasp', 'dock')"
5379
+ },
5380
+ reward: {
5381
+ type: "number",
5382
+ description: "Reinforcement learning reward signal (-1.0 to 1.0)"
5383
+ },
5384
+ sensor_data: {
5385
+ type: "object",
5386
+ additionalProperties: { type: "number" },
5387
+ description: "Raw sensor readings (e.g., {battery: 72.5, temperature: 23.1})"
5388
+ },
5389
+ outcome_type: {
5390
+ type: "string",
5391
+ description: "Outcome type: success, failure, partial, aborted, timeout"
5392
+ },
5393
+ terrain_type: {
5394
+ type: "string",
5395
+ description: "Terrain type: indoor, outdoor, urban, rural, water, aerial"
5223
5396
  }
5224
5397
  },
5225
5398
  required: ["content"]
@@ -5227,7 +5400,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5227
5400
  },
5228
5401
  {
5229
5402
  name: "recall",
5230
- description: "Search memories AND todos using semantic similarity. Returns both relevant memories and matching todos. Use this to find past experiences, decisions, context, or pending work. Modes: 'semantic' (vector similarity), 'associative' (graph traversal), 'hybrid' (combined).",
5403
+ description: "Search memories AND todos using semantic similarity. Returns both relevant memories and matching todos. Use this to find past experiences, decisions, context, or pending work. Modes: 'semantic' (vector similarity), 'associative' (graph traversal), 'temporal' (time-based retrieval), 'hybrid' (combined), 'spatial' (geo-location based), 'mission' (mission context), 'action_outcome' (reward-based learning).",
5231
5404
  inputSchema: {
5232
5405
  type: "object",
5233
5406
  properties: {
@@ -5242,14 +5415,87 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5242
5415
  },
5243
5416
  mode: {
5244
5417
  type: "string",
5245
- enum: ["semantic", "associative", "hybrid"],
5246
- description: "Retrieval mode: 'semantic' for pure vector similarity, 'associative' for graph-based traversal (follows learned connections), 'hybrid' for density-dependent combination (default)",
5418
+ enum: ["semantic", "associative", "temporal", "hybrid", "spatial", "mission", "action_outcome"],
5419
+ description: "Retrieval mode: 'semantic' for pure vector similarity, 'associative' for graph-based traversal (follows learned connections), 'temporal' for time-based retrieval, 'hybrid' for density-dependent combination (default), 'spatial' for geo-location based, 'mission' for mission context, 'action_outcome' for reward-based learning",
5247
5420
  default: "hybrid"
5421
+ },
5422
+ session_id: {
5423
+ type: "string",
5424
+ description: "Session ID for session-scoped retrieval. When provided, retrieves memories from that session's time window. Forces temporal mode."
5425
+ },
5426
+ robot_id: {
5427
+ type: "string",
5428
+ description: "Filter by robot/drone identifier (for multi-robot systems)"
5429
+ },
5430
+ mission_id: {
5431
+ type: "string",
5432
+ description: "Filter by mission identifier"
5433
+ },
5434
+ geo_lat: {
5435
+ type: "number",
5436
+ description: "Spatial filter: center latitude (-90 to 90). Requires geo_lon and geo_radius_meters."
5437
+ },
5438
+ geo_lon: {
5439
+ type: "number",
5440
+ description: "Spatial filter: center longitude (-180 to 180). Requires geo_lat and geo_radius_meters."
5441
+ },
5442
+ geo_radius_meters: {
5443
+ type: "number",
5444
+ description: "Spatial filter: search radius in meters. Requires geo_lat and geo_lon."
5445
+ },
5446
+ action_type: {
5447
+ type: "string",
5448
+ description: "Filter by action type (e.g., 'navigate', 'grasp', 'dock')"
5449
+ },
5450
+ reward_min: {
5451
+ type: "number",
5452
+ description: "Filter by minimum reward value (-1.0 to 1.0)"
5453
+ },
5454
+ reward_max: {
5455
+ type: "number",
5456
+ description: "Filter by maximum reward value (-1.0 to 1.0)"
5457
+ },
5458
+ outcome_type: {
5459
+ type: "string",
5460
+ description: "Filter by outcome type: success, failure, partial, aborted, timeout"
5461
+ },
5462
+ failures_only: {
5463
+ type: "boolean",
5464
+ description: "If true, only return failure/error experiences"
5465
+ },
5466
+ terrain_type: {
5467
+ type: "string",
5468
+ description: "Filter by terrain type: indoor, outdoor, urban, rural, water, aerial"
5469
+ },
5470
+ tags: {
5471
+ type: "array",
5472
+ items: { type: "string" },
5473
+ description: "Filter by tags (any match)"
5248
5474
  }
5249
5475
  },
5250
5476
  required: ["query"]
5251
5477
  }
5252
5478
  },
5479
+ {
5480
+ name: "recall_by_tags",
5481
+ description: "Find memories by tags. Returns memories matching ANY of the provided tags. Useful for finding memories by category (e.g., 'tool:Edit', 'file:src/main.rs', 'source:hook', 'error', 'session-summary').",
5482
+ inputSchema: {
5483
+ type: "object",
5484
+ properties: {
5485
+ tags: {
5486
+ type: "array",
5487
+ items: { type: "string" },
5488
+ description: "Tags to search for (returns memories matching ANY of these tags)"
5489
+ },
5490
+ limit: {
5491
+ type: "number",
5492
+ description: "Maximum number of results (default: 50)",
5493
+ default: 50
5494
+ }
5495
+ },
5496
+ required: ["tags"]
5497
+ }
5498
+ },
5253
5499
  {
5254
5500
  name: "context_summary",
5255
5501
  description: "Get a condensed summary of recent learnings, decisions, and context. Use this at the start of a session to quickly understand what you've learned before.",
@@ -5375,6 +5621,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5375
5621
  }
5376
5622
  }
5377
5623
  },
5624
+ {
5625
+ name: "backup_restore",
5626
+ description: "Restore a previously created backup by ID. This replaces all current data for the user with the backup contents. Server restart is recommended after restore.",
5627
+ inputSchema: {
5628
+ type: "object",
5629
+ properties: {
5630
+ backup_id: {
5631
+ type: "number",
5632
+ description: "The backup ID to restore (from backup_list)"
5633
+ }
5634
+ },
5635
+ required: ["backup_id"]
5636
+ }
5637
+ },
5378
5638
  {
5379
5639
  name: "consolidation_report",
5380
5640
  description: "Get a report of what the memory system has been learning. Shows memory strengthening/decay events, edge formation, fact extraction, and maintenance cycles. Use this to understand how your memories are evolving.",
@@ -5431,6 +5691,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5431
5691
  type: "boolean",
5432
5692
  description: "Automatically store the context as a Conversation memory (default: true). Set to false to only surface memories without storing.",
5433
5693
  default: true
5694
+ },
5695
+ tool_actions: {
5696
+ type: "array",
5697
+ items: {
5698
+ type: "object",
5699
+ properties: {
5700
+ tool_name: { type: "string", description: "Tool or actuator name (e.g., 'Edit', 'Bash', 'navigate', 'grasp')" },
5701
+ inputs: { type: "object", additionalProperties: { type: "string" }, description: "Key-value input parameters" },
5702
+ success: { type: "boolean", description: "Whether the action succeeded" },
5703
+ output_snippet: { type: "string", description: "First 200 chars of output" },
5704
+ reward: { type: "number", description: "Reward signal for robotics (-1.0 to 1.0)" }
5705
+ },
5706
+ required: ["tool_name", "success"]
5707
+ },
5708
+ description: "Tool/actuator actions performed since last proactive_context call. Used for causal feedback attribution."
5434
5709
  }
5435
5710
  },
5436
5711
  required: ["context"]
@@ -5489,6 +5764,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5489
5764
  type: "array",
5490
5765
  items: { type: "string" },
5491
5766
  description: "Optional tags for categorization"
5767
+ },
5768
+ threshold: {
5769
+ type: "number",
5770
+ description: "Semantic similarity threshold for 'context' trigger (0.0-1.0, default: 0.7). Lower values match more broadly, higher values require closer semantic match."
5492
5771
  }
5493
5772
  },
5494
5773
  required: ["content", "trigger_type"]
@@ -5964,7 +6243,18 @@ To start: cd shodh-memory && cargo run`
5964
6243
  episode_id,
5965
6244
  sequence_number,
5966
6245
  preceding_memory_id,
5967
- parent_id
6246
+ parent_id,
6247
+ importance,
6248
+ robot_id,
6249
+ mission_id,
6250
+ geo_location,
6251
+ local_position,
6252
+ heading,
6253
+ action_type,
6254
+ reward,
6255
+ sensor_data,
6256
+ outcome_type,
6257
+ terrain_type
5968
6258
  } = args;
5969
6259
  if (!content || content.length === 0) {
5970
6260
  return { content: [{ type: "text", text: "Error: 'content' is required and cannot be empty" }], isError: true };
@@ -5986,7 +6276,18 @@ To start: cd shodh-memory && cargo run`
5986
6276
  ...episode_id && { episode_id },
5987
6277
  ...sequence_number !== undefined && { sequence_number },
5988
6278
  ...preceding_memory_id && { preceding_memory_id },
5989
- ...parent_id && { parent_id }
6279
+ ...parent_id && { parent_id },
6280
+ ...importance !== undefined && { importance },
6281
+ ...robot_id && { robot_id },
6282
+ ...mission_id && { mission_id },
6283
+ ...geo_location && geo_location.length === 3 && { geo_location },
6284
+ ...local_position && local_position.length === 3 && { local_position },
6285
+ ...heading !== undefined && { heading },
6286
+ ...action_type && { action_type },
6287
+ ...reward !== undefined && { reward },
6288
+ ...sensor_data && Object.keys(sensor_data).length > 0 && { sensor_data },
6289
+ ...outcome_type && { outcome_type },
6290
+ ...terrain_type && { terrain_type }
5990
6291
  });
5991
6292
  let response = `\uD83D\uDC18 Memory Stored
5992
6293
  `;
@@ -6001,20 +6302,37 @@ To start: cd shodh-memory && cargo run`
6001
6302
  response += ` │ Tags: ${tags.join(", ")}`;
6002
6303
  }
6003
6304
  response += `
6004
- ID: ${result.id.slice(0, 8)}...`;
6305
+ ID: ${result.id}`;
6005
6306
  return {
6006
6307
  content: [{ type: "text", text: response }]
6007
6308
  };
6008
6309
  }
6009
6310
  case "recall": {
6010
- const { query, limit: rawLimit = 5, mode = "hybrid" } = args;
6311
+ const {
6312
+ query,
6313
+ limit: rawLimit = 5,
6314
+ mode = "hybrid",
6315
+ session_id,
6316
+ robot_id,
6317
+ mission_id,
6318
+ geo_lat,
6319
+ geo_lon,
6320
+ geo_radius_meters,
6321
+ action_type,
6322
+ reward_min,
6323
+ reward_max,
6324
+ outcome_type,
6325
+ failures_only,
6326
+ terrain_type,
6327
+ tags
6328
+ } = args;
6011
6329
  if (!query || query.length === 0) {
6012
6330
  return { content: [{ type: "text", text: "Error: 'query' is required and cannot be empty" }], isError: true };
6013
6331
  }
6014
6332
  if (query.length > MAX_QUERY_LENGTH) {
6015
6333
  return { content: [{ type: "text", text: `Error: 'query' exceeds maximum length of ${MAX_QUERY_LENGTH} characters` }], isError: true };
6016
6334
  }
6017
- const validModes = ["semantic", "associative", "hybrid"];
6335
+ const validModes = ["semantic", "associative", "temporal", "hybrid", "spatial", "mission", "action_outcome"];
6018
6336
  if (!validModes.includes(mode)) {
6019
6337
  return { content: [{ type: "text", text: `Error: 'mode' must be one of: ${validModes.join(", ")}` }], isError: true };
6020
6338
  }
@@ -6023,11 +6341,25 @@ ID: ${result.id.slice(0, 8)}...`;
6023
6341
  user_id: USER_ID,
6024
6342
  query,
6025
6343
  limit,
6026
- mode
6344
+ mode,
6345
+ ...session_id ? { session_id } : {},
6346
+ ...robot_id ? { robot_id } : {},
6347
+ ...mission_id ? { mission_id } : {},
6348
+ ...geo_lat !== undefined ? { geo_lat } : {},
6349
+ ...geo_lon !== undefined ? { geo_lon } : {},
6350
+ ...geo_radius_meters !== undefined ? { geo_radius_meters } : {},
6351
+ ...action_type ? { action_type } : {},
6352
+ ...reward_min !== undefined ? { reward_min } : {},
6353
+ ...reward_max !== undefined ? { reward_max } : {},
6354
+ ...outcome_type ? { outcome_type } : {},
6355
+ ...failures_only !== undefined ? { failures_only } : {},
6356
+ ...terrain_type ? { terrain_type } : {},
6357
+ ...tags && tags.length > 0 ? { tags } : {}
6027
6358
  });
6028
6359
  const memories = result.memories || [];
6029
6360
  const todos = result.todos || [];
6030
6361
  const stats = result.retrieval_stats;
6362
+ const lineage = result.lineage || [];
6031
6363
  if (memories.length === 0 && todos.length === 0) {
6032
6364
  return {
6033
6365
  content: [
@@ -6070,21 +6402,24 @@ ID: ${result.id.slice(0, 8)}...`;
6070
6402
  return d.toLocaleDateString([], { month: "short", day: "numeric" });
6071
6403
  }
6072
6404
  };
6405
+ const memoryDisplayScores = memories.map((m) => m.score || 0);
6406
+ const todoDisplayScores = todos.map((t) => t.score || 0);
6073
6407
  if (memories.length > 0) {
6074
6408
  response += `\uD83D\uDCDD MEMORIES
6075
6409
  `;
6076
6410
  for (let i = 0;i < memories.length; i++) {
6077
6411
  const m = memories[i];
6078
6412
  const content = getContent(m);
6079
- const score = ((m.score || 0) * 100).toFixed(0);
6080
- const filled = Math.max(0, Math.min(10, Math.round((m.score || 0) * 10)));
6413
+ const displayScore = memoryDisplayScores[i];
6414
+ const score = (displayScore * 100).toFixed(0);
6415
+ const filled = Math.max(0, Math.min(10, Math.round(displayScore * 10)));
6081
6416
  const matchBar = "█".repeat(filled) + "░".repeat(10 - filled);
6082
6417
  const timeStr = formatTime(m.created_at);
6083
6418
  response += `• ${matchBar} ${score}% │ ${timeStr}
6084
6419
  `;
6085
6420
  response += ` ${content.slice(0, 200)}${content.length > 200 ? "..." : ""}
6086
6421
  `;
6087
- response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id.slice(0, 8)}...
6422
+ response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id}
6088
6423
  `;
6089
6424
  if (i < memories.length - 1)
6090
6425
  response += `
@@ -6099,8 +6434,9 @@ ID: ${result.id.slice(0, 8)}...`;
6099
6434
  `;
6100
6435
  for (let i = 0;i < todos.length; i++) {
6101
6436
  const t = todos[i];
6102
- const score = ((t.score || 0) * 100).toFixed(0);
6103
- const filled = Math.max(0, Math.min(10, Math.round((t.score || 0) * 10)));
6437
+ const displayScore = todoDisplayScores[i];
6438
+ const score = (displayScore * 100).toFixed(0);
6439
+ const filled = Math.max(0, Math.min(10, Math.round(displayScore * 10)));
6104
6440
  const matchBar = "█".repeat(filled) + "░".repeat(10 - filled);
6105
6441
  const statusIcon = t.status === "done" ? "✓" : t.status === "in_progress" ? "▶" : t.status === "blocked" ? "⊗" : "○";
6106
6442
  const timeStr = formatTime(t.created_at);
@@ -6126,16 +6462,79 @@ ID: ${result.id.slice(0, 8)}...`;
6126
6462
  `;
6127
6463
  const graphPct = (stats.graph_weight * 100).toFixed(0);
6128
6464
  const semPct = (stats.semantic_weight * 100).toFixed(0);
6129
- response += ` Graph: ${graphPct}% │ Semantic: ${semPct}% │ Density: ${stats.graph_density.toFixed(2)}
6465
+ response += ` Graph: ${graphPct}% │ Semantic: ${semPct}% │ Density: ${(stats.graph_density ?? 0).toFixed(2)}
6130
6466
  `;
6131
6467
  response += ` Candidates: ${stats.graph_candidates} graph + ${stats.semantic_candidates} semantic
6132
6468
  `;
6133
6469
  response += ` Entities: ${stats.entities_activated} │ Time: ${(stats.retrieval_time_us / 1000).toFixed(1)}ms`;
6134
6470
  }
6471
+ if (lineage.length > 0) {
6472
+ const idShort = (id) => id;
6473
+ const idToContent = new Map;
6474
+ for (const m of memories) {
6475
+ idToContent.set(m.id, getContent(m).slice(0, 40));
6476
+ }
6477
+ response += `
6478
+
6479
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6480
+ `;
6481
+ response += `\uD83D\uDD17 LINEAGE (${lineage.length} causal edge${lineage.length > 1 ? "s" : ""})
6482
+ `;
6483
+ for (const edge of lineage) {
6484
+ const fromLabel = idToContent.get(edge.from) || idShort(edge.from);
6485
+ const toLabel = idToContent.get(edge.to) || idShort(edge.to);
6486
+ const conf = (edge.confidence * 100).toFixed(0);
6487
+ response += ` ${idShort(edge.from)} ──${edge.relation}──▶ ${idShort(edge.to)} (${conf}%)
6488
+ `;
6489
+ response += ` "${fromLabel}..." → "${toLabel}..."
6490
+ `;
6491
+ }
6492
+ }
6135
6493
  return {
6136
6494
  content: [{ type: "text", text: response }]
6137
6495
  };
6138
6496
  }
6497
+ case "recall_by_tags": {
6498
+ const { tags, limit: rawTagLimit = 50 } = args;
6499
+ if (!tags || tags.length === 0) {
6500
+ return {
6501
+ content: [{ type: "text", text: "Error: 'tags' is required and must contain at least one tag" }],
6502
+ isError: true
6503
+ };
6504
+ }
6505
+ const tagLimit = Math.max(1, Math.min(Math.floor(rawTagLimit), MAX_LIMIT));
6506
+ const tagResult = await apiCall("/api/recall/tags", "POST", {
6507
+ user_id: USER_ID,
6508
+ tags,
6509
+ limit: tagLimit
6510
+ });
6511
+ const tagMemories = tagResult.memories || [];
6512
+ if (tagMemories.length === 0) {
6513
+ return {
6514
+ content: [{ type: "text", text: `No memories found matching tags: ${tags.join(", ")}` }]
6515
+ };
6516
+ }
6517
+ let tagResponse = `\uD83C\uDFF7️ Recall by Tags: ${tags.join(", ")}
6518
+ `;
6519
+ tagResponse += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6520
+ `;
6521
+ tagResponse += `Found ${tagMemories.length} memories
6522
+
6523
+ `;
6524
+ for (let i = 0;i < tagMemories.length; i++) {
6525
+ const m = tagMemories[i];
6526
+ const content = getContent(m);
6527
+ const memTags = (m.experience?.tags || []).join(", ");
6528
+ tagResponse += `${String(i + 1).padStart(2)}. ${content.slice(0, 150)}${content.length > 150 ? "..." : ""}
6529
+ `;
6530
+ tagResponse += ` ┗━ ${getType(m)} │ tags: [${memTags}] │ ${m.id}
6531
+
6532
+ `;
6533
+ }
6534
+ return {
6535
+ content: [{ type: "text", text: tagResponse.trimEnd() }]
6536
+ };
6537
+ }
6139
6538
  case "context_summary": {
6140
6539
  const {
6141
6540
  include_decisions = true,
@@ -6303,7 +6702,7 @@ ID: ${result.id.slice(0, 8)}...`;
6303
6702
  }[getType(m)] || "\uD83D\uDCE6";
6304
6703
  response += `${String(i + 1).padStart(2)}. ${typeIcon} ${content.slice(0, 150)}${content.length > 150 ? "..." : ""}
6305
6704
  `;
6306
- response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id.slice(0, 8)}...
6705
+ response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id}
6307
6706
  `;
6308
6707
  }
6309
6708
  return {
@@ -6317,7 +6716,7 @@ ID: ${result.id.slice(0, 8)}...`;
6317
6716
  `;
6318
6717
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6319
6718
  `;
6320
- response += `✓ Removed: ${id.slice(0, 8)}...`;
6719
+ response += `✓ Removed: ${id}`;
6321
6720
  return {
6322
6721
  content: [{ type: "text", text: response }]
6323
6722
  };
@@ -6463,7 +6862,7 @@ By Type:
6463
6862
  response += `Created: ${new Date(b.created_at).toLocaleString()}
6464
6863
  `;
6465
6864
  } else {
6466
- response += `✗ Failed: ${result.message}
6865
+ response += `✗ Failed: ${result.message || "Unknown backup creation error"}
6467
6866
  `;
6468
6867
  }
6469
6868
  return {
@@ -6520,7 +6919,7 @@ By Type:
6520
6919
  `;
6521
6920
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6522
6921
  `;
6523
- response += result.message;
6922
+ response += result.message || "No verification details provided";
6524
6923
  return {
6525
6924
  content: [{ type: "text", text: response }]
6526
6925
  };
@@ -6546,6 +6945,32 @@ By Type:
6546
6945
  content: [{ type: "text", text: response }]
6547
6946
  };
6548
6947
  }
6948
+ case "backup_restore": {
6949
+ const { backup_id } = args;
6950
+ const result = await apiCall("/api/backup/restore", "POST", {
6951
+ user_id: USER_ID,
6952
+ backup_id
6953
+ });
6954
+ let response = `\uD83D\uDD04 Backup Restore
6955
+ `;
6956
+ response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6957
+ `;
6958
+ if (result.success) {
6959
+ response += `✓ Backup #${backup_id} restored successfully
6960
+ `;
6961
+ if (result.restored_stores.length > 0) {
6962
+ response += `Restored stores: ${result.restored_stores.join(", ")}
6963
+ `;
6964
+ }
6965
+ response += `
6966
+ ⚠️ ${result.message || "Restore completed with no additional details"}`;
6967
+ } else {
6968
+ response += `✗ Restore failed: ${result.message || "Unknown restore error"}`;
6969
+ }
6970
+ return {
6971
+ content: [{ type: "text", text: response }]
6972
+ };
6973
+ }
6549
6974
  case "proactive_context": {
6550
6975
  const {
6551
6976
  context,
@@ -6554,19 +6979,33 @@ By Type:
6554
6979
  recency_weight = 0.2,
6555
6980
  max_results = 5,
6556
6981
  memory_types = [],
6557
- auto_ingest = true
6982
+ auto_ingest = true,
6983
+ tool_actions = []
6558
6984
  } = args;
6985
+ const cleanedContext = stripSystemNoise(context).slice(0, MAX_CONTEXT_LENGTH);
6986
+ if (cleanedContext.length < PROACTIVE_MIN_CONTEXT_LENGTH) {
6987
+ return {
6988
+ content: [{ type: "text", text: `No relevant memories surfaced (context too short after cleaning).
6989
+
6990
+ [Latency: 0.0ms]` }]
6991
+ };
6992
+ }
6993
+ const skipFeedback = proactiveCallInFlight;
6994
+ proactiveCallInFlight = true;
6995
+ const previousUserContext = skipFeedback ? "" : lastUserContext;
6996
+ lastUserContext = cleanedContext;
6559
6997
  const result = await apiCall("/api/proactive_context", "POST", {
6560
6998
  user_id: USER_ID,
6561
- context,
6999
+ context: cleanedContext,
6562
7000
  max_results,
6563
7001
  semantic_threshold,
6564
7002
  entity_match_weight,
6565
7003
  recency_weight,
6566
7004
  memory_types,
6567
7005
  auto_ingest,
6568
- previous_response: lastProactiveResponse || undefined,
6569
- user_followup: lastProactiveResponse ? context : undefined
7006
+ previous_response: skipFeedback ? undefined : lastProactiveResponse || undefined,
7007
+ user_followup: skipFeedback || !lastProactiveResponse ? undefined : previousUserContext || undefined,
7008
+ ...tool_actions.length > 0 ? { tool_actions } : {}
6570
7009
  });
6571
7010
  const memories = result.memories || [];
6572
7011
  const entities = result.detected_entities || [];
@@ -6577,10 +7016,13 @@ By Type:
6577
7016
  Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(", ")}` : "";
6578
7017
  const feedbackNote2 = result.feedback_processed ? `
6579
7018
  [Feedback: ${result.feedback_processed.memories_evaluated} evaluated, ${result.feedback_processed.reinforced.length} reinforced, ${result.feedback_processed.weakened.length} weakened]` : "";
6580
- const emptyText = `No relevant memories surfaced for this context.${entityList}${feedbackNote2}
7019
+ const temporalNote2 = result.temporal_credits_applied ? `
7020
+ [Temporal credits: ${result.temporal_credits_applied} multi-turn signals applied]` : "";
7021
+ const emptyText = `No relevant memories surfaced for this context.${entityList}${feedbackNote2}${temporalNote2}
6581
7022
 
6582
- [Latency: ${result.latency_ms.toFixed(1)}ms]`;
7023
+ [Latency: ${(result.latency_ms ?? 0).toFixed(1)}ms]`;
6583
7024
  lastProactiveResponse = emptyText;
7025
+ proactiveCallInFlight = false;
6584
7026
  return {
6585
7027
  content: [{ type: "text", text: emptyText }]
6586
7028
  };
@@ -6605,7 +7047,7 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6605
7047
  for (const r of uniqueReminders) {
6606
7048
  const icon = r.overdue_seconds && r.overdue_seconds > 0 ? "⏰" : "\uD83D\uDCCC";
6607
7049
  const contentText = r.content.slice(0, 38);
6608
- reminderBlock += `┃ ${icon} ${contentText.padEnd(44)} [${r.id.slice(0, 8)}] ┃
7050
+ reminderBlock += `┃ ${icon} ${contentText.padEnd(44)} [${r.id}] ┃
6609
7051
  `;
6610
7052
  if (r.overdue_seconds && r.overdue_seconds > 0) {
6611
7053
  const mins = Math.round(r.overdue_seconds / 60);
@@ -6700,8 +7142,10 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6700
7142
  `);
6701
7143
  const feedbackNote = result.feedback_processed ? `
6702
7144
  [Feedback loop: ${result.feedback_processed.memories_evaluated} evaluated, ${result.feedback_processed.reinforced.length} reinforced, ${result.feedback_processed.weakened.length} weakened]` : "";
7145
+ const temporalNote = result.temporal_credits_applied ? `
7146
+ [Temporal credits: ${result.temporal_credits_applied} multi-turn signals applied]` : "";
6703
7147
  const ingestNote = result.ingested_memory_id ? `
6704
- [Context ingested: ${result.ingested_memory_id.slice(0, 8)}]` : "";
7148
+ [Context ingested: ${result.ingested_memory_id}]` : "";
6705
7149
  const summaryParts = [];
6706
7150
  if (memories.length > 0)
6707
7151
  summaryParts.push(`${memories.length} memories`);
@@ -6714,10 +7158,13 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6714
7158
  const summary = summaryParts.length > 0 ? `Surfaced ${summaryParts.join(", ")}` : "No relevant context found";
6715
7159
  const responseText = `${temporalHeader}${summary}:
6716
7160
 
6717
- ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${feedbackNote}${ingestNote}
7161
+ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${feedbackNote}${temporalNote}${ingestNote}
6718
7162
 
6719
- [Latency: ${result.latency_ms.toFixed(1)}ms | Threshold: ${(semantic_threshold * 100).toFixed(0)}%]`;
6720
- lastProactiveResponse = responseText;
7163
+ [Latency: ${(result.latency_ms ?? 0).toFixed(1)}ms | Threshold: ${(semantic_threshold * 100).toFixed(0)}%]`;
7164
+ const cleanContent = memories.map((m) => m.content || "").filter((c) => c.length > 0).join(`
7165
+ `);
7166
+ lastProactiveResponse = cleanContent || responseText;
7167
+ proactiveCallInFlight = false;
6721
7168
  return {
6722
7169
  content: [{ type: "text", text: responseText }]
6723
7170
  };
@@ -6855,7 +7302,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6855
7302
  };
6856
7303
  }
6857
7304
  case "set_reminder": {
6858
- const { content, trigger_type, trigger_at, after_seconds, keywords, priority = 3, tags = [] } = args;
7305
+ const { content, trigger_type, trigger_at, after_seconds, keywords, priority = 3, tags = [], threshold } = args;
6859
7306
  if (!content || content.length === 0) {
6860
7307
  return { content: [{ type: "text", text: "Error: 'content' is required and cannot be empty" }], isError: true };
6861
7308
  }
@@ -6892,7 +7339,8 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6892
7339
  isError: true
6893
7340
  };
6894
7341
  }
6895
- trigger = { type: "context", keywords, threshold: 0.7 };
7342
+ const ctxThreshold = threshold !== undefined && threshold >= 0 && threshold <= 1 ? threshold : 0.7;
7343
+ trigger = { type: "context", keywords, threshold: ctxThreshold };
6896
7344
  break;
6897
7345
  default:
6898
7346
  return {
@@ -6911,7 +7359,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6911
7359
  `;
6912
7360
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6913
7361
  `;
6914
- response += `ID: ${result.id.slice(0, 8)}...
7362
+ response += `ID: ${result.id}
6915
7363
  `;
6916
7364
  response += `Content: ${content}
6917
7365
  `;
@@ -6954,7 +7402,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6954
7402
  const statusBadge = r.status === "triggered" ? " [TRIGGERED]" : "";
6955
7403
  response += `${icon} ${r.content.slice(0, 50)}${r.content.length > 50 ? "..." : ""}${statusBadge}
6956
7404
  `;
6957
- response += ` Type: ${r.trigger_type} | Priority: ${"★".repeat(r.priority)} | ID: ${r.id.slice(0, 8)}...
7405
+ response += ` Type: ${r.trigger_type} | Priority: ${"★".repeat(r.priority)} | ID: ${r.id}
6958
7406
  `;
6959
7407
  if (r.due_at) {
6960
7408
  response += ` Due: ${new Date(r.due_at).toLocaleString()}
@@ -6981,7 +7429,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6981
7429
  content: [
6982
7430
  {
6983
7431
  type: "text",
6984
- text: result.success ? `✓ Reminder dismissed: ${reminder_id.slice(0, 8)}...` : `⚠️ ${result.message}`
7432
+ text: result.success ? `✓ Reminder dismissed: ${reminder_id}` : `⚠️ ${result.message || "No message returned"}`
6985
7433
  }
6986
7434
  ]
6987
7435
  };
@@ -7199,7 +7647,13 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
7199
7647
  };
7200
7648
  }
7201
7649
  case "read_memory": {
7202
- const { memory_id } = args;
7650
+ const memory_id = args.memory_id || args.id;
7651
+ if (!memory_id || typeof memory_id !== "string" || memory_id.trim().length === 0) {
7652
+ return {
7653
+ content: [{ type: "text", text: "Error: 'memory_id' is required. Pass the full UUID or 8+ character prefix from recall results." }],
7654
+ isError: true
7655
+ };
7656
+ }
7203
7657
  let memory = null;
7204
7658
  try {
7205
7659
  memory = await apiCall(`/api/memory/${memory_id}?user_id=${encodeURIComponent(USER_ID)}`, "GET");
@@ -7222,11 +7676,11 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
7222
7676
  response += `Tier: ${memory.tier || "Unknown"} | Created: ${created} | Importance: ${(memory.importance * 100).toFixed(0)}%
7223
7677
  `;
7224
7678
  if (memory.parent_id) {
7225
- response += `Parent: ${memory.parent_id.slice(0, 8)}...
7679
+ response += `Parent: ${memory.parent_id}
7226
7680
  `;
7227
7681
  }
7228
7682
  if (memory.children_count > 0) {
7229
- response += `Children: ${memory.children_count} (${memory.children_ids.map((id) => id.slice(0, 8)).join(", ")})
7683
+ response += `Children: ${memory.children_count} (${memory.children_ids.join(", ")})
7230
7684
  `;
7231
7685
  }
7232
7686
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -7261,13 +7715,13 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
7261
7715
  const context = contextParts.join(" ").slice(0, 1000);
7262
7716
  if (context.length >= PROACTIVE_MIN_CONTEXT_LENGTH) {
7263
7717
  const surfaced = await surfaceRelevant(context, 3);
7264
- if (surfaced && surfaced.length > 0) {
7718
+ if (surfaced && surfaced.length > 0 && result.content.length > 0) {
7265
7719
  const surfacedText = formatSurfacedMemories(surfaced);
7266
7720
  result.content[result.content.length - 1].text += surfacedText;
7267
7721
  }
7268
7722
  }
7269
7723
  }
7270
- if (tokenStatus.alert) {
7724
+ if (tokenStatus.alert && result.content.length > 0) {
7271
7725
  const percentUsed = Math.round(tokenStatus.percent * 100);
7272
7726
  const warning = `⚠️ CONTEXT ALERT: ${percentUsed}% of token budget used (${tokenStatus.tokens.toLocaleString()}/${tokenStatus.budget.toLocaleString()}). Consider starting a new session or running consolidation.
7273
7727
 
@@ -7878,14 +8332,14 @@ function getBinaryPath() {
7878
8332
  wrapperName = "shodh-memory";
7879
8333
  fallbackName = "shodh-memory-server";
7880
8334
  }
7881
- const wrapperPath = path.join(binDir, wrapperName);
7882
- if (fs.existsSync(wrapperPath)) {
7883
- return wrapperPath;
7884
- }
7885
8335
  const binaryPath = path.join(binDir, fallbackName);
7886
8336
  if (fs.existsSync(binaryPath)) {
7887
8337
  return binaryPath;
7888
8338
  }
8339
+ const wrapperPath = path.join(binDir, wrapperName);
8340
+ if (fs.existsSync(wrapperPath)) {
8341
+ return wrapperPath;
8342
+ }
7889
8343
  return null;
7890
8344
  }
7891
8345
  async function isServerRunning() {
@@ -7910,9 +8364,32 @@ async function waitForServer(maxAttempts = 30) {
7910
8364
  }
7911
8365
  return false;
7912
8366
  }
8367
+ async function validateApiKey() {
8368
+ try {
8369
+ const controller = new AbortController;
8370
+ const timeout = setTimeout(() => controller.abort(), 3000);
8371
+ const response = await fetch(`${API_URL}/api/health`, {
8372
+ headers: { "X-API-Key": API_KEY },
8373
+ signal: controller.signal
8374
+ });
8375
+ clearTimeout(timeout);
8376
+ return response.ok;
8377
+ } catch {
8378
+ return false;
8379
+ }
8380
+ }
7913
8381
  async function ensureServerRunning() {
7914
8382
  if (await isServerRunning()) {
7915
8383
  console.error("[shodh-memory] Backend server already running at", API_URL);
8384
+ if (!process.env.SHODH_API_KEY && isLocalServer()) {
8385
+ const keyWorks = await validateApiKey();
8386
+ if (!keyWorks) {
8387
+ console.error("[shodh-memory] WARNING: Auto-generated key rejected by running server.");
8388
+ console.error("[shodh-memory] The server was started with a different API key.");
8389
+ console.error("[shodh-memory] Set SHODH_API_KEY to match the server's key, or restart");
8390
+ console.error("[shodh-memory] the server without SHODH_DEV_API_KEY to use auto-generated keys.");
8391
+ }
8392
+ }
7916
8393
  return;
7917
8394
  }
7918
8395
  if (!AUTO_SPAWN_ENABLED) {
@@ -7930,6 +8407,13 @@ async function ensureServerRunning() {
7930
8407
  console.error("[shodh-memory] Or download from: https://github.com/varun29ankuS/shodh-memory/releases");
7931
8408
  return;
7932
8409
  }
8410
+ const expectedBinDir = fs.realpathSync(path.join(__dirname2, "..", "bin"));
8411
+ const resolvedBinary = fs.realpathSync(binaryPath);
8412
+ if (!resolvedBinary.startsWith(expectedBinDir + path.sep) && resolvedBinary !== expectedBinDir) {
8413
+ console.error(`[shodh-memory] WARNING: Binary path resolves outside expected directory: ${resolvedBinary}`);
8414
+ console.error(`[shodh-memory] Expected: ${expectedBinDir}`);
8415
+ return;
8416
+ }
7933
8417
  console.error("[shodh-memory] Starting backend server...");
7934
8418
  const serverEnv = {};
7935
8419
  const SERVER_ENV_ALLOWLIST = new Set([
@@ -7966,10 +8450,12 @@ async function ensureServerRunning() {
7966
8450
  }
7967
8451
  }
7968
8452
  serverEnv["SHODH_DEV_API_KEY"] = API_KEY;
8453
+ const isBat = binaryPath.endsWith(".bat");
7969
8454
  serverProcess = spawn(binaryPath, [], {
7970
8455
  detached: true,
7971
8456
  stdio: "ignore",
7972
- env: serverEnv
8457
+ env: serverEnv,
8458
+ ...isBat && { shell: true }
7973
8459
  });
7974
8460
  serverProcess.unref();
7975
8461
  console.error("[shodh-memory] Waiting for server to start...");
@@ -7981,16 +8467,31 @@ async function ensureServerRunning() {
7981
8467
  }
7982
8468
  }
7983
8469
  function cleanupServer() {
8470
+ if (streamReconnectTimer) {
8471
+ clearTimeout(streamReconnectTimer);
8472
+ streamReconnectTimer = null;
8473
+ }
8474
+ STREAM_ENABLED = false;
8475
+ if (streamSocket) {
8476
+ try {
8477
+ streamSocket.close();
8478
+ } catch (_) {}
8479
+ streamSocket = null;
8480
+ }
7984
8481
  if (serverProcess && !serverProcess.killed) {
7985
8482
  if (process.platform !== "win32" && serverProcess.pid) {
7986
8483
  try {
7987
8484
  process.kill(-serverProcess.pid, "SIGTERM");
7988
8485
  } catch (e) {
7989
8486
  console.error("[Cleanup] Process group kill failed, falling back to direct kill:", e);
7990
- serverProcess.kill("SIGTERM");
8487
+ try {
8488
+ serverProcess.kill("SIGTERM");
8489
+ } catch (_) {}
7991
8490
  }
7992
8491
  } else {
7993
- serverProcess.kill();
8492
+ try {
8493
+ serverProcess.kill();
8494
+ } catch (_) {}
7994
8495
  }
7995
8496
  }
7996
8497
  }
@@ -8005,14 +8506,42 @@ process.on("SIGTERM", () => {
8005
8506
  cleanupServer();
8006
8507
  process.exit(0);
8007
8508
  });
8509
+ var shuttingDown = false;
8510
+ function gracefulShutdown(reason, code = 0) {
8511
+ if (shuttingDown)
8512
+ return;
8513
+ shuttingDown = true;
8514
+ console.error(`[shodh-memory] ${reason}`);
8515
+ cleanupServer();
8516
+ setTimeout(() => process.exit(code), 100);
8517
+ }
8518
+ process.stdin.on("end", () => gracefulShutdown("stdin closed (MCP session ended), shutting down..."));
8519
+ process.stdin.on("close", () => gracefulShutdown("stdin pipe closed, shutting down..."));
8520
+ process.on("uncaughtException", (err) => {
8521
+ console.error("[shodh-memory] Uncaught exception:", err);
8522
+ gracefulShutdown("Shutting down after uncaught exception", 1);
8523
+ });
8524
+ process.on("unhandledRejection", (reason) => {
8525
+ console.error("[shodh-memory] Unhandled rejection:", reason);
8526
+ gracefulShutdown("Shutting down after unhandled rejection", 1);
8527
+ });
8528
+ function createSandboxServer() {
8529
+ process.env.SMITHERY_SANDBOX = "true";
8530
+ return server;
8531
+ }
8008
8532
  async function main() {
8533
+ if (SANDBOX_MODE)
8534
+ return;
8009
8535
  await ensureServerRunning();
8010
8536
  const transport = new StdioServerTransport;
8011
8537
  await server.connect(transport);
8012
- console.error("Shodh-Memory MCP server v0.1.80 running");
8538
+ console.error("Shodh-Memory MCP server v0.1.90 running");
8013
8539
  console.error(`Connecting to: ${API_URL}`);
8014
8540
  console.error(`User ID: ${USER_ID}`);
8015
8541
  console.error(`Streaming: ${STREAM_ENABLED ? "enabled" : "disabled"}`);
8016
8542
  console.error(`Proactive surfacing: ${PROACTIVE_SURFACING ? "enabled" : "disabled (SHODH_PROACTIVE=false)"}`);
8017
8543
  }
8018
8544
  main().catch(console.error);
8545
+ export {
8546
+ createSandboxServer
8547
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shodh/memory-mcp",
3
- "version": "0.1.80",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.varun29ankuS/shodh-memory",
5
5
  "description": "MCP server for persistent AI memory - store and recall context across sessions",
6
6
  "type": "module",
@@ -11,6 +11,9 @@
11
11
  "scripts": {
12
12
  "start": "bun run index.ts",
13
13
  "build": "bun build index.ts --outdir dist --target node",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage",
14
17
  "postinstall": "node scripts/postinstall.cjs",
15
18
  "prepublishOnly": "npm run build"
16
19
  },
@@ -22,6 +25,10 @@
22
25
  "dependencies": {
23
26
  "@modelcontextprotocol/sdk": "^1.24.0"
24
27
  },
28
+ "devDependencies": {
29
+ "@vitest/coverage-v8": "^2.1.9",
30
+ "vitest": "^2.1.8"
31
+ },
25
32
  "keywords": [
26
33
  "mcp",
27
34
  "model-context-protocol",
@@ -9,9 +9,9 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const https = require('https');
12
- const { execSync } = require('child_process');
12
+ const { execFileSync } = require('child_process');
13
13
 
14
- const VERSION = '0.1.74';
14
+ const VERSION = require('../package.json').version;
15
15
  const REPO = 'varun29ankuS/shodh-memory';
16
16
  const BIN_DIR = path.join(__dirname, '..', 'bin');
17
17
 
@@ -71,10 +71,10 @@ function download(url, dest) {
71
71
  // Extract archive
72
72
  function extract(archive, dest, platformInfo) {
73
73
  if (platformInfo.ext === '.tar.gz') {
74
- execSync(`tar -xzf "${archive}" -C "${dest}"`, { stdio: 'inherit' });
74
+ execFileSync('tar', ['-xzf', archive, '-C', dest], { stdio: 'inherit' });
75
75
  } else if (platformInfo.ext === '.zip') {
76
76
  // Use PowerShell on Windows
77
- execSync(`powershell -Command "Expand-Archive -Path '${archive}' -DestinationPath '${dest}' -Force"`, { stdio: 'inherit' });
77
+ execFileSync('powershell', ['-Command', `Expand-Archive -Path '${archive}' -DestinationPath '${dest}' -Force`], { stdio: 'inherit' });
78
78
  }
79
79
  }
80
80