@shodh/memory-mcp 0.1.90 → 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/dist/index.js CHANGED
@@ -4909,10 +4909,41 @@ function nextReconnectDelay(currentDelayMs, maxDelayMs) {
4909
4909
  return Math.min(safeCurrent * 2, maxDelayMs);
4910
4910
  }
4911
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
+
4912
4929
  // index.ts
4913
4930
  var __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath(import.meta.url) : "";
4914
4931
  var __dirname2 = __filename2 ? path.dirname(__filename2) : process.cwd();
4915
- var API_URL = process.env.SHODH_API_URL || "http://127.0.0.1:3030";
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();
4916
4947
  var WS_URL = API_URL.replace(/^http/, "ws") + "/api/stream";
4917
4948
  var USER_ID = process.env.SHODH_USER_ID || "claude-code";
4918
4949
  function isLocalServer() {
@@ -4925,10 +4956,25 @@ function isLocalServer() {
4925
4956
  }
4926
4957
  }
4927
4958
  var SANDBOX_MODE = process.env.SMITHERY_SANDBOX === "true";
4928
- var API_KEY = process.env.SHODH_API_KEY || (SANDBOX_MODE ? "sandbox" : "");
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
+ }
4929
4974
  if (!API_KEY) {
4930
4975
  if (isLocalServer()) {
4931
4976
  API_KEY = crypto.randomBytes(32).toString("hex");
4977
+ apiKeySource = "auto-generated";
4932
4978
  console.error("[shodh-memory] No API key set — auto-generated for local server.");
4933
4979
  } else {
4934
4980
  console.error("ERROR: SHODH_API_KEY is required for remote servers.");
@@ -4941,6 +4987,11 @@ if (!API_KEY) {
4941
4987
  process.exit(1);
4942
4988
  }
4943
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}.`);
4994
+ }
4944
4995
  var RETRY_ATTEMPTS = 3;
4945
4996
  var RETRY_DELAY_MS = 1000;
4946
4997
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -4977,22 +5028,9 @@ var STREAM_MIN_CONTENT_LENGTH = 50;
4977
5028
  var PROACTIVE_SURFACING = process.env.SHODH_PROACTIVE !== "false";
4978
5029
  var PROACTIVE_MIN_CONTEXT_LENGTH = 30;
4979
5030
  var MAX_CONTEXT_LENGTH = 4000;
4980
- function stripSystemNoise(text) {
4981
- let result = text;
4982
- const tagPatterns = [
4983
- /<task-notification>[\s\S]*?<\/task-notification>/g,
4984
- /<system-reminder>[\s\S]*?<\/system-reminder>/g,
4985
- /<shodh-context[\s\S]*?<\/shodh-context>/g,
4986
- /<shodh-memory[\s\S]*?<\/shodh-memory>/g,
4987
- /<command-name>[\s\S]*?<\/command-name>/g
4988
- ];
4989
- for (const pattern of tagPatterns) {
4990
- result = result.replace(pattern, "");
4991
- }
4992
- result = result.replace(/\s{3,}/g, " ").trim();
4993
- return result;
4994
- }
4995
5031
  var lastProactiveResponse = "";
5032
+ var lastUserContext = "";
5033
+ var proactiveCallInFlight = false;
4996
5034
  var streamSocket = null;
4997
5035
  var streamConnecting = false;
4998
5036
  var streamReconnectTimer = null;
@@ -5135,10 +5173,12 @@ async function surfaceRelevant(context, maxResults = 3) {
5135
5173
  }),
5136
5174
  signal: controller.signal
5137
5175
  });
5138
- clearTimeout(timeoutId);
5139
- if (!response.ok)
5176
+ if (!response.ok) {
5177
+ clearTimeout(timeoutId);
5140
5178
  return null;
5179
+ }
5141
5180
  const result = await response.json();
5181
+ clearTimeout(timeoutId);
5142
5182
  return result.memories || null;
5143
5183
  } catch (e) {
5144
5184
  console.error("[Proactive] Failed to surface memories:", e);
@@ -5192,10 +5232,15 @@ async function apiCall(endpoint, method = "GET", body) {
5192
5232
  const errorText = await response.text().catch(() => "Unknown error");
5193
5233
  throw new Error(`API error ${response.status}: ${errorText}`);
5194
5234
  }
5195
- return await response.json();
5235
+ try {
5236
+ return await response.json();
5237
+ } catch {
5238
+ throw new Error(`API returned invalid JSON from ${endpoint}`);
5239
+ }
5196
5240
  } catch (error) {
5197
5241
  lastError = error instanceof Error ? error : new Error(String(error));
5198
- 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) {
5199
5244
  throw lastError;
5200
5245
  }
5201
5246
  if (attempt < RETRY_ATTEMPTS) {
@@ -5297,6 +5342,57 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5297
5342
  parent_id: {
5298
5343
  type: "string",
5299
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"
5300
5396
  }
5301
5397
  },
5302
5398
  required: ["content"]
@@ -5304,7 +5400,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5304
5400
  },
5305
5401
  {
5306
5402
  name: "recall",
5307
- 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).",
5308
5404
  inputSchema: {
5309
5405
  type: "object",
5310
5406
  properties: {
@@ -5319,14 +5415,87 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5319
5415
  },
5320
5416
  mode: {
5321
5417
  type: "string",
5322
- enum: ["semantic", "associative", "hybrid"],
5323
- 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",
5324
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)"
5325
5474
  }
5326
5475
  },
5327
5476
  required: ["query"]
5328
5477
  }
5329
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
+ },
5330
5499
  {
5331
5500
  name: "context_summary",
5332
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.",
@@ -5522,6 +5691,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5522
5691
  type: "boolean",
5523
5692
  description: "Automatically store the context as a Conversation memory (default: true). Set to false to only surface memories without storing.",
5524
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."
5525
5709
  }
5526
5710
  },
5527
5711
  required: ["context"]
@@ -6059,7 +6243,18 @@ To start: cd shodh-memory && cargo run`
6059
6243
  episode_id,
6060
6244
  sequence_number,
6061
6245
  preceding_memory_id,
6062
- 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
6063
6258
  } = args;
6064
6259
  if (!content || content.length === 0) {
6065
6260
  return { content: [{ type: "text", text: "Error: 'content' is required and cannot be empty" }], isError: true };
@@ -6081,7 +6276,18 @@ To start: cd shodh-memory && cargo run`
6081
6276
  ...episode_id && { episode_id },
6082
6277
  ...sequence_number !== undefined && { sequence_number },
6083
6278
  ...preceding_memory_id && { preceding_memory_id },
6084
- ...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 }
6085
6291
  });
6086
6292
  let response = `\uD83D\uDC18 Memory Stored
6087
6293
  `;
@@ -6102,14 +6308,31 @@ ID: ${result.id}`;
6102
6308
  };
6103
6309
  }
6104
6310
  case "recall": {
6105
- 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;
6106
6329
  if (!query || query.length === 0) {
6107
6330
  return { content: [{ type: "text", text: "Error: 'query' is required and cannot be empty" }], isError: true };
6108
6331
  }
6109
6332
  if (query.length > MAX_QUERY_LENGTH) {
6110
6333
  return { content: [{ type: "text", text: `Error: 'query' exceeds maximum length of ${MAX_QUERY_LENGTH} characters` }], isError: true };
6111
6334
  }
6112
- const validModes = ["semantic", "associative", "hybrid"];
6335
+ const validModes = ["semantic", "associative", "temporal", "hybrid", "spatial", "mission", "action_outcome"];
6113
6336
  if (!validModes.includes(mode)) {
6114
6337
  return { content: [{ type: "text", text: `Error: 'mode' must be one of: ${validModes.join(", ")}` }], isError: true };
6115
6338
  }
@@ -6118,7 +6341,20 @@ ID: ${result.id}`;
6118
6341
  user_id: USER_ID,
6119
6342
  query,
6120
6343
  limit,
6121
- 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 } : {}
6122
6358
  });
6123
6359
  const memories = result.memories || [];
6124
6360
  const todos = result.todos || [];
@@ -6166,14 +6402,17 @@ ID: ${result.id}`;
6166
6402
  return d.toLocaleDateString([], { month: "short", day: "numeric" });
6167
6403
  }
6168
6404
  };
6405
+ const memoryDisplayScores = memories.map((m) => m.score || 0);
6406
+ const todoDisplayScores = todos.map((t) => t.score || 0);
6169
6407
  if (memories.length > 0) {
6170
6408
  response += `\uD83D\uDCDD MEMORIES
6171
6409
  `;
6172
6410
  for (let i = 0;i < memories.length; i++) {
6173
6411
  const m = memories[i];
6174
6412
  const content = getContent(m);
6175
- const score = ((m.score || 0) * 100).toFixed(0);
6176
- 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)));
6177
6416
  const matchBar = "█".repeat(filled) + "░".repeat(10 - filled);
6178
6417
  const timeStr = formatTime(m.created_at);
6179
6418
  response += `• ${matchBar} ${score}% │ ${timeStr}
@@ -6195,8 +6434,9 @@ ID: ${result.id}`;
6195
6434
  `;
6196
6435
  for (let i = 0;i < todos.length; i++) {
6197
6436
  const t = todos[i];
6198
- const score = ((t.score || 0) * 100).toFixed(0);
6199
- 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)));
6200
6440
  const matchBar = "█".repeat(filled) + "░".repeat(10 - filled);
6201
6441
  const statusIcon = t.status === "done" ? "✓" : t.status === "in_progress" ? "▶" : t.status === "blocked" ? "⊗" : "○";
6202
6442
  const timeStr = formatTime(t.created_at);
@@ -6254,6 +6494,47 @@ ID: ${result.id}`;
6254
6494
  content: [{ type: "text", text: response }]
6255
6495
  };
6256
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
+ }
6257
6538
  case "context_summary": {
6258
6539
  const {
6259
6540
  include_decisions = true,
@@ -6698,7 +6979,8 @@ By Type:
6698
6979
  recency_weight = 0.2,
6699
6980
  max_results = 5,
6700
6981
  memory_types = [],
6701
- auto_ingest = true
6982
+ auto_ingest = true,
6983
+ tool_actions = []
6702
6984
  } = args;
6703
6985
  const cleanedContext = stripSystemNoise(context).slice(0, MAX_CONTEXT_LENGTH);
6704
6986
  if (cleanedContext.length < PROACTIVE_MIN_CONTEXT_LENGTH) {
@@ -6708,6 +6990,10 @@ By Type:
6708
6990
  [Latency: 0.0ms]` }]
6709
6991
  };
6710
6992
  }
6993
+ const skipFeedback = proactiveCallInFlight;
6994
+ proactiveCallInFlight = true;
6995
+ const previousUserContext = skipFeedback ? "" : lastUserContext;
6996
+ lastUserContext = cleanedContext;
6711
6997
  const result = await apiCall("/api/proactive_context", "POST", {
6712
6998
  user_id: USER_ID,
6713
6999
  context: cleanedContext,
@@ -6717,8 +7003,9 @@ By Type:
6717
7003
  recency_weight,
6718
7004
  memory_types,
6719
7005
  auto_ingest,
6720
- previous_response: lastProactiveResponse || undefined,
6721
- user_followup: lastProactiveResponse ? cleanedContext : undefined
7006
+ previous_response: skipFeedback ? undefined : lastProactiveResponse || undefined,
7007
+ user_followup: skipFeedback || !lastProactiveResponse ? undefined : previousUserContext || undefined,
7008
+ ...tool_actions.length > 0 ? { tool_actions } : {}
6722
7009
  });
6723
7010
  const memories = result.memories || [];
6724
7011
  const entities = result.detected_entities || [];
@@ -6729,10 +7016,13 @@ By Type:
6729
7016
  Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(", ")}` : "";
6730
7017
  const feedbackNote2 = result.feedback_processed ? `
6731
7018
  [Feedback: ${result.feedback_processed.memories_evaluated} evaluated, ${result.feedback_processed.reinforced.length} reinforced, ${result.feedback_processed.weakened.length} weakened]` : "";
6732
- 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}
6733
7022
 
6734
7023
  [Latency: ${(result.latency_ms ?? 0).toFixed(1)}ms]`;
6735
7024
  lastProactiveResponse = emptyText;
7025
+ proactiveCallInFlight = false;
6736
7026
  return {
6737
7027
  content: [{ type: "text", text: emptyText }]
6738
7028
  };
@@ -6852,6 +7142,8 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6852
7142
  `);
6853
7143
  const feedbackNote = result.feedback_processed ? `
6854
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]` : "";
6855
7147
  const ingestNote = result.ingested_memory_id ? `
6856
7148
  [Context ingested: ${result.ingested_memory_id}]` : "";
6857
7149
  const summaryParts = [];
@@ -6866,10 +7158,13 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6866
7158
  const summary = summaryParts.length > 0 ? `Surfaced ${summaryParts.join(", ")}` : "No relevant context found";
6867
7159
  const responseText = `${temporalHeader}${summary}:
6868
7160
 
6869
- ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${feedbackNote}${ingestNote}
7161
+ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${feedbackNote}${temporalNote}${ingestNote}
6870
7162
 
6871
7163
  [Latency: ${(result.latency_ms ?? 0).toFixed(1)}ms | Threshold: ${(semantic_threshold * 100).toFixed(0)}%]`;
6872
- lastProactiveResponse = responseText;
7164
+ const cleanContent = memories.map((m) => m.content || "").filter((c) => c.length > 0).join(`
7165
+ `);
7166
+ lastProactiveResponse = cleanContent || responseText;
7167
+ proactiveCallInFlight = false;
6873
7168
  return {
6874
7169
  content: [{ type: "text", text: responseText }]
6875
7170
  };
@@ -7420,13 +7715,13 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
7420
7715
  const context = contextParts.join(" ").slice(0, 1000);
7421
7716
  if (context.length >= PROACTIVE_MIN_CONTEXT_LENGTH) {
7422
7717
  const surfaced = await surfaceRelevant(context, 3);
7423
- if (surfaced && surfaced.length > 0) {
7718
+ if (surfaced && surfaced.length > 0 && result.content.length > 0) {
7424
7719
  const surfacedText = formatSurfacedMemories(surfaced);
7425
7720
  result.content[result.content.length - 1].text += surfacedText;
7426
7721
  }
7427
7722
  }
7428
7723
  }
7429
- if (tokenStatus.alert) {
7724
+ if (tokenStatus.alert && result.content.length > 0) {
7430
7725
  const percentUsed = Math.round(tokenStatus.percent * 100);
7431
7726
  const warning = `⚠️ CONTEXT ALERT: ${percentUsed}% of token budget used (${tokenStatus.tokens.toLocaleString()}/${tokenStatus.budget.toLocaleString()}). Consider starting a new session or running consolidation.
7432
7727
 
@@ -8172,16 +8467,31 @@ async function ensureServerRunning() {
8172
8467
  }
8173
8468
  }
8174
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
+ }
8175
8481
  if (serverProcess && !serverProcess.killed) {
8176
8482
  if (process.platform !== "win32" && serverProcess.pid) {
8177
8483
  try {
8178
8484
  process.kill(-serverProcess.pid, "SIGTERM");
8179
8485
  } catch (e) {
8180
8486
  console.error("[Cleanup] Process group kill failed, falling back to direct kill:", e);
8181
- serverProcess.kill("SIGTERM");
8487
+ try {
8488
+ serverProcess.kill("SIGTERM");
8489
+ } catch (_) {}
8182
8490
  }
8183
8491
  } else {
8184
- serverProcess.kill();
8492
+ try {
8493
+ serverProcess.kill();
8494
+ } catch (_) {}
8185
8495
  }
8186
8496
  }
8187
8497
  }
@@ -8196,6 +8506,25 @@ process.on("SIGTERM", () => {
8196
8506
  cleanupServer();
8197
8507
  process.exit(0);
8198
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
+ });
8199
8528
  function createSandboxServer() {
8200
8529
  process.env.SMITHERY_SANDBOX = "true";
8201
8530
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shodh/memory-mcp",
3
- "version": "0.1.90",
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",
@@ -9,7 +9,7 @@
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
14
  const VERSION = require('../package.json').version;
15
15
  const REPO = 'varun29ankuS/shodh-memory';
@@ -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