shabti 1.14.0 → 2.1.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/BENCHMARKS.md +32 -0
- package/README.md +39 -0
- package/package.json +15 -7
- package/src/mcp/server.js +302 -0
package/BENCHMARKS.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Shabti Benchmarks
|
|
2
|
+
|
|
3
|
+
Measured on real Qdrant (localhost:6334) + fastembed-rs (MultilingualE5Small, 384 dimensions).
|
|
4
|
+
|
|
5
|
+
## Environment
|
|
6
|
+
|
|
7
|
+
- OS: Windows 11 Pro
|
|
8
|
+
- Rust: release build (`--release`)
|
|
9
|
+
- Qdrant: Docker container (v1.13+), gRPC port 6334
|
|
10
|
+
- Embedding model: `MultilingualE5Small` (384-dim, local inference via ONNX Runtime)
|
|
11
|
+
- Corpus: 30 diverse English sentences
|
|
12
|
+
|
|
13
|
+
## Results
|
|
14
|
+
|
|
15
|
+
| Metric | Target | Measured | Status |
|
|
16
|
+
| ----------------------- | -------- | ---------------------- | ------ |
|
|
17
|
+
| **Recall@10** | >= 0.85 | **1.000** (11/11 hits) | PASS |
|
|
18
|
+
| **Insert latency p99** | <= 100ms | **60.22ms** | PASS |
|
|
19
|
+
| **Insert latency p50** | — | 53.64ms | — |
|
|
20
|
+
| **Insert latency mean** | — | 51.69ms | — |
|
|
21
|
+
| **Search latency p99** | <= 100ms | **58.71ms** | PASS |
|
|
22
|
+
| **Search latency p50** | — | 51.43ms | — |
|
|
23
|
+
| **Search latency mean** | — | 52.19ms | — |
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
|
|
27
|
+
- Latency is dominated by local embedding computation (~50ms per text via ONNX Runtime).
|
|
28
|
+
Qdrant round-trip adds ~1-10ms.
|
|
29
|
+
- Recall@10 achieves perfect score on the benchmark corpus, indicating strong semantic
|
|
30
|
+
matching quality with the MultilingualE5Small model.
|
|
31
|
+
- Score explanation breakdown confirms `time_decay = 1.0` (freshly inserted) and
|
|
32
|
+
`access_boost = 1.0` (no prior accesses), so composite score equals raw cosine similarity.
|
package/README.md
CHANGED
|
@@ -117,6 +117,45 @@ you> /help
|
|
|
117
117
|
|
|
118
118
|
Requires `OPENAI_API_KEY` in `.env` for chat functionality. Memory commands (`/remember`, `/recall`) work with the local Qdrant engine.
|
|
119
119
|
|
|
120
|
+
## MCP Server
|
|
121
|
+
|
|
122
|
+
shabti includes an MCP (Model Context Protocol) server for integration with Claude Code, Cursor, and other MCP-compatible tools.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Start the MCP server (stdio transport)
|
|
126
|
+
npx shabti-mcp
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Claude Code configuration
|
|
130
|
+
|
|
131
|
+
Add to your MCP settings:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"shabti-memory": {
|
|
137
|
+
"command": "npx",
|
|
138
|
+
"args": ["shabti-mcp"]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Available tools
|
|
145
|
+
|
|
146
|
+
| Tool | Description |
|
|
147
|
+
| --------------- | -------------------------------------- |
|
|
148
|
+
| `memory_store` | Store a memory entry |
|
|
149
|
+
| `memory_search` | Search memories by semantic similarity |
|
|
150
|
+
| `memory_status` | Get engine status |
|
|
151
|
+
|
|
152
|
+
### Available resources
|
|
153
|
+
|
|
154
|
+
| URI | Description |
|
|
155
|
+
| ----------------- | ---------------------------- |
|
|
156
|
+
| `shabti://status` | Engine status and statistics |
|
|
157
|
+
| `shabti://config` | Current configuration |
|
|
158
|
+
|
|
120
159
|
## Node.js API
|
|
121
160
|
|
|
122
161
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shabti",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Agent Memory OS — semantic memory for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "native.cjs",
|
|
7
7
|
"types": "native.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"shabti": "src/index.js"
|
|
9
|
+
"shabti": "src/index.js",
|
|
10
|
+
"shabti-mcp": "src/mcp/server.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"src/",
|
|
13
14
|
"native.cjs",
|
|
14
15
|
"native.d.ts",
|
|
15
|
-
"*.node"
|
|
16
|
+
"*.node",
|
|
17
|
+
"BENCHMARKS.md"
|
|
16
18
|
],
|
|
17
19
|
"napi": {
|
|
18
20
|
"binaryName": "shabti",
|
|
@@ -34,9 +36,15 @@
|
|
|
34
36
|
"prepare": "husky"
|
|
35
37
|
},
|
|
36
38
|
"keywords": [
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
39
|
+
"ai",
|
|
40
|
+
"agent",
|
|
41
|
+
"memory",
|
|
42
|
+
"semantic-search",
|
|
43
|
+
"vector-database",
|
|
44
|
+
"qdrant",
|
|
45
|
+
"embeddings",
|
|
46
|
+
"mcp",
|
|
47
|
+
"cli"
|
|
40
48
|
],
|
|
41
49
|
"author": "Kaga Hinata",
|
|
42
50
|
"engines": {
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { createEngine, loadConfig } from "../core/engine.js";
|
|
4
|
+
|
|
5
|
+
const SERVER_INFO = {
|
|
6
|
+
name: "shabti-memory",
|
|
7
|
+
version: "2.0.0",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TOOLS = [
|
|
11
|
+
{
|
|
12
|
+
name: "memory_store",
|
|
13
|
+
description: "Store a memory entry in shabti",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
content: { type: "string", description: "The text content to store" },
|
|
18
|
+
namespace: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Target namespace (default: 'default')",
|
|
21
|
+
},
|
|
22
|
+
tags: {
|
|
23
|
+
type: "array",
|
|
24
|
+
items: { type: "string" },
|
|
25
|
+
description: "Tags to associate with the memory",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ["content"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "memory_search",
|
|
33
|
+
description: "Search stored memories by semantic similarity",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
query: { type: "string", description: "Search query text" },
|
|
38
|
+
limit: {
|
|
39
|
+
type: "integer",
|
|
40
|
+
description: "Maximum number of results (default: 10)",
|
|
41
|
+
},
|
|
42
|
+
namespace: { type: "string", description: "Namespace to search within" },
|
|
43
|
+
},
|
|
44
|
+
required: ["query"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "memory_status",
|
|
49
|
+
description: "Get the current status of the memory engine",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const RESOURCES = [
|
|
58
|
+
{
|
|
59
|
+
uri: "shabti://status",
|
|
60
|
+
name: "Engine status",
|
|
61
|
+
description: "Current shabti engine status and statistics",
|
|
62
|
+
mimeType: "application/json",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
uri: "shabti://config",
|
|
66
|
+
name: "Engine configuration",
|
|
67
|
+
description: "Current engine configuration",
|
|
68
|
+
mimeType: "application/json",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
let engine = null;
|
|
73
|
+
|
|
74
|
+
function initEngine() {
|
|
75
|
+
if (engine) return engine;
|
|
76
|
+
try {
|
|
77
|
+
engine = createEngine();
|
|
78
|
+
} catch {
|
|
79
|
+
// engine stays null — tools will return errors
|
|
80
|
+
}
|
|
81
|
+
return engine;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function respond(id, result) {
|
|
85
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
86
|
+
process.stdout.write(msg + "\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function respondError(id, code, message) {
|
|
90
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
91
|
+
process.stdout.write(msg + "\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleInitialize(id) {
|
|
95
|
+
respond(id, {
|
|
96
|
+
protocolVersion: "2024-11-05",
|
|
97
|
+
serverInfo: SERVER_INFO,
|
|
98
|
+
capabilities: {
|
|
99
|
+
tools: {},
|
|
100
|
+
resources: {},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleToolsList(id) {
|
|
106
|
+
respond(id, { tools: TOOLS });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleResourcesList(id) {
|
|
110
|
+
respond(id, { resources: RESOURCES });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function handleToolsCall(id, params) {
|
|
114
|
+
const { name, arguments: args } = params;
|
|
115
|
+
|
|
116
|
+
const eng = initEngine();
|
|
117
|
+
|
|
118
|
+
if (name === "memory_status") {
|
|
119
|
+
if (!eng) {
|
|
120
|
+
return respond(id, {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: JSON.stringify(
|
|
125
|
+
{ status: "unavailable", entry_count: 0, model_id: "unknown" },
|
|
126
|
+
null,
|
|
127
|
+
2,
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const status = eng.status();
|
|
134
|
+
return respond(id, {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: JSON.stringify(
|
|
139
|
+
{
|
|
140
|
+
status: "ok",
|
|
141
|
+
entry_count: status.entryCount,
|
|
142
|
+
tier: status.tier,
|
|
143
|
+
model_id: status.modelId,
|
|
144
|
+
qdrant_url: status.qdrantUrl,
|
|
145
|
+
},
|
|
146
|
+
null,
|
|
147
|
+
2,
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (name === "memory_store") {
|
|
155
|
+
if (!eng) {
|
|
156
|
+
return respondError(id, -32603, "Engine not available");
|
|
157
|
+
}
|
|
158
|
+
const content = args?.content;
|
|
159
|
+
if (!content) {
|
|
160
|
+
return respondError(id, -32602, "Missing required parameter: content");
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const opts = {};
|
|
164
|
+
if (args.namespace) opts.namespace = args.namespace;
|
|
165
|
+
if (args.tags) opts.tags = args.tags;
|
|
166
|
+
const result = await eng.store(content, opts);
|
|
167
|
+
return respond(id, {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: JSON.stringify(
|
|
172
|
+
{ status: result.status, id: result.id || result.existingId },
|
|
173
|
+
null,
|
|
174
|
+
2,
|
|
175
|
+
),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return respondError(id, -32603, err.message);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (name === "memory_search") {
|
|
185
|
+
if (!eng) {
|
|
186
|
+
return respondError(id, -32603, "Engine not available");
|
|
187
|
+
}
|
|
188
|
+
const query = args?.query;
|
|
189
|
+
if (!query) {
|
|
190
|
+
return respondError(id, -32602, "Missing required parameter: query");
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const limit = args.limit || 10;
|
|
194
|
+
const queryObj = { text: query, limit };
|
|
195
|
+
if (args.namespace) queryObj.namespace = args.namespace;
|
|
196
|
+
const results = await eng.executeQuery(queryObj);
|
|
197
|
+
const formatted = results.map((r) => ({
|
|
198
|
+
content: r.content,
|
|
199
|
+
score: r.score,
|
|
200
|
+
id: r.id,
|
|
201
|
+
namespace: r.namespace,
|
|
202
|
+
}));
|
|
203
|
+
return respond(id, {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: JSON.stringify({ query, results: formatted }, null, 2),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return respondError(id, -32603, err.message);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleResourcesRead(id, params) {
|
|
220
|
+
const { uri } = params;
|
|
221
|
+
const eng = initEngine();
|
|
222
|
+
|
|
223
|
+
if (uri === "shabti://status") {
|
|
224
|
+
if (!eng) {
|
|
225
|
+
return respond(id, {
|
|
226
|
+
contents: [
|
|
227
|
+
{
|
|
228
|
+
uri,
|
|
229
|
+
mimeType: "application/json",
|
|
230
|
+
text: JSON.stringify({ status: "unavailable", entry_count: 0 }),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const status = eng.status();
|
|
236
|
+
return respond(id, {
|
|
237
|
+
contents: [
|
|
238
|
+
{
|
|
239
|
+
uri,
|
|
240
|
+
mimeType: "application/json",
|
|
241
|
+
text: JSON.stringify({
|
|
242
|
+
status: "ok",
|
|
243
|
+
entry_count: status.entryCount,
|
|
244
|
+
tier: status.tier,
|
|
245
|
+
model_id: status.modelId,
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (uri === "shabti://config") {
|
|
253
|
+
const config = loadConfig();
|
|
254
|
+
return respond(id, {
|
|
255
|
+
contents: [
|
|
256
|
+
{
|
|
257
|
+
uri,
|
|
258
|
+
mimeType: "application/json",
|
|
259
|
+
text: JSON.stringify(config),
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
respondError(id, -32602, `Unknown resource: ${uri}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function handleRequest(line) {
|
|
269
|
+
let req;
|
|
270
|
+
try {
|
|
271
|
+
req = JSON.parse(line);
|
|
272
|
+
} catch {
|
|
273
|
+
return respondError(null, -32700, "Parse error");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const { id, method, params } = req;
|
|
277
|
+
|
|
278
|
+
switch (method) {
|
|
279
|
+
case "initialize":
|
|
280
|
+
return handleInitialize(id);
|
|
281
|
+
case "notifications/initialized":
|
|
282
|
+
return; // no response needed for notifications
|
|
283
|
+
case "tools/list":
|
|
284
|
+
return handleToolsList(id);
|
|
285
|
+
case "tools/call":
|
|
286
|
+
return handleToolsCall(id, params || {});
|
|
287
|
+
case "resources/list":
|
|
288
|
+
return handleResourcesList(id);
|
|
289
|
+
case "resources/read":
|
|
290
|
+
return handleResourcesRead(id, params || {});
|
|
291
|
+
default:
|
|
292
|
+
return respondError(id, -32601, "Method not found");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// stdio transport: read newline-delimited JSON from stdin
|
|
297
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
298
|
+
rl.on("line", (line) => {
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (trimmed) handleRequest(trimmed);
|
|
301
|
+
});
|
|
302
|
+
rl.on("close", () => process.exit(0));
|