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 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.14.0",
4
- "description": "Demo CLI toolshowcasing npm-publishable CLI structure",
3
+ "version": "2.1.0",
4
+ "description": "Agent Memory OSsemantic 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
- "cli",
38
- "demo",
39
- "tool"
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));