@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 +68 -11
- package/dist/index.js +602 -73
- package/package.json +8 -1
- package/scripts/postinstall.cjs +4 -4
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.
|
|
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**:
|
|
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 (
|
|
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
|
|
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
|
-
| `
|
|
95
|
-
|
|
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
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
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
|
-
|
|
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(
|
|
5103
|
+
console.error(`[Stream] Attempting reconnect (next delay: ${streamReconnectDelay}ms)...`);
|
|
4994
5104
|
connectStream().catch((e) => console.error("[Stream] Reconnect failed:", e));
|
|
4995
|
-
},
|
|
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
|
-
|
|
5067
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
6305
|
+
ID: ${result.id}`;
|
|
6005
6306
|
return {
|
|
6006
6307
|
content: [{ type: "text", text: response }]
|
|
6007
6308
|
};
|
|
6008
6309
|
}
|
|
6009
6310
|
case "recall": {
|
|
6010
|
-
const {
|
|
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
|
|
6080
|
-
const
|
|
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
|
|
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
|
|
6103
|
-
const
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
8487
|
+
try {
|
|
8488
|
+
serverProcess.kill("SIGTERM");
|
|
8489
|
+
} catch (_) {}
|
|
7991
8490
|
}
|
|
7992
8491
|
} else {
|
|
7993
|
-
|
|
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.
|
|
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.
|
|
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",
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const https = require('https');
|
|
12
|
-
const {
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
13
|
|
|
14
|
-
const VERSION = '
|
|
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
|
-
|
|
74
|
+
execFileSync('tar', ['-xzf', archive, '-C', dest], { stdio: 'inherit' });
|
|
75
75
|
} else if (platformInfo.ext === '.zip') {
|
|
76
76
|
// Use PowerShell on Windows
|
|
77
|
-
|
|
77
|
+
execFileSync('powershell', ['-Command', `Expand-Archive -Path '${archive}' -DestinationPath '${dest}' -Force`], { stdio: 'inherit' });
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|