shabti 2.5.0 → 2.7.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 +139 -41
- package/package.json +1 -1
- package/src/a2a/server.js +21 -2
- package/src/commands/a2a.js +9 -2
- package/src/commands/delete.js +20 -0
- package/src/commands/export.js +2 -1
- package/src/commands/import.js +7 -2
- package/src/commands/search.js +7 -6
- package/src/commands/store.js +2 -1
- package/src/core/engine.js +1 -1
- package/src/core/retry.js +49 -0
- package/src/index.js +2 -0
- package/src/mcp/server.js +33 -4
- package/src/repl/index.js +1 -1
- package/src/utils/logger.js +40 -0
- package/src/utils/validate.js +54 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Agent Memory OS — semantic memory for AI agents.
|
|
4
4
|
|
|
5
|
-
A Rust-powered memory engine with Node.js CLI that provides semantic search, deduplication, time-decay scoring, and graph-based memory linking for AI agents.
|
|
5
|
+
A Rust-powered memory engine with Node.js CLI that provides semantic search, deduplication, time-decay scoring, and graph-based memory linking for AI agents. Integrates with Claude Code, Cursor, and other tools via MCP and A2A protocols.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -30,11 +30,14 @@ shabti config setup --check
|
|
|
30
30
|
# Store a memory
|
|
31
31
|
shabti store "Rust is a systems programming language"
|
|
32
32
|
|
|
33
|
+
# Store with namespace and TTL (auto-expires after 1 hour)
|
|
34
|
+
shabti store "temp note" --namespace work --ttl 3600
|
|
35
|
+
|
|
33
36
|
# Search memories
|
|
34
37
|
shabti search "systems programming"
|
|
35
38
|
|
|
36
|
-
# Search with score explanation
|
|
37
|
-
shabti search "programming" --explain
|
|
39
|
+
# Search with score explanation and graph expansion
|
|
40
|
+
shabti search "programming" --explain --follow-links 2
|
|
38
41
|
|
|
39
42
|
# Check engine status
|
|
40
43
|
shabti status
|
|
@@ -42,14 +45,20 @@ shabti status
|
|
|
42
45
|
|
|
43
46
|
## Commands
|
|
44
47
|
|
|
45
|
-
| Command | Description
|
|
46
|
-
| ----------------- |
|
|
47
|
-
| `store <content>` | Store a memory entry
|
|
48
|
-
| `search <query>` | Search memory entries
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
48
|
+
| Command | Description |
|
|
49
|
+
| ----------------- | ----------------------------------- |
|
|
50
|
+
| `store <content>` | Store a memory entry |
|
|
51
|
+
| `search <query>` | Search memory entries |
|
|
52
|
+
| `delete <id>` | Delete a memory entry by ID |
|
|
53
|
+
| `status` | Show engine status |
|
|
54
|
+
| `export` | Export memories as JSONL |
|
|
55
|
+
| `import <file>` | Import memories from JSONL |
|
|
56
|
+
| `snapshot` | Manage storage snapshots |
|
|
57
|
+
| `config` | Manage configuration |
|
|
58
|
+
| `gc` | Garbage collect expired entries |
|
|
59
|
+
| `a2a` | Start A2A protocol server |
|
|
60
|
+
| `chat` | Interactive chat with OpenAI |
|
|
61
|
+
| `mcp-config` | Print MCP server configuration JSON |
|
|
53
62
|
|
|
54
63
|
### store
|
|
55
64
|
|
|
@@ -57,6 +66,7 @@ shabti status
|
|
|
57
66
|
shabti store "Tokyo is the capital of Japan"
|
|
58
67
|
shabti store "meeting at 3pm" --namespace work
|
|
59
68
|
shabti store "buy groceries" --tags "todo,personal"
|
|
69
|
+
shabti store "temporary note" --ttl 3600 # expires in 1 hour
|
|
60
70
|
```
|
|
61
71
|
|
|
62
72
|
### search
|
|
@@ -70,6 +80,28 @@ shabti search "recent events" --min-score 0.5
|
|
|
70
80
|
shabti search "query" --json # JSON output
|
|
71
81
|
```
|
|
72
82
|
|
|
83
|
+
### delete
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
shabti delete <uuid>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### export / import
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Export all entries to stdout
|
|
93
|
+
shabti export
|
|
94
|
+
|
|
95
|
+
# Export to file with namespace filter
|
|
96
|
+
shabti export --namespace work --output backup.jsonl
|
|
97
|
+
|
|
98
|
+
# Import from JSONL file
|
|
99
|
+
shabti import backup.jsonl
|
|
100
|
+
|
|
101
|
+
# Import with namespace override and dry-run
|
|
102
|
+
shabti import data.jsonl --namespace imported --dry-run
|
|
103
|
+
```
|
|
104
|
+
|
|
73
105
|
### snapshot
|
|
74
106
|
|
|
75
107
|
```bash
|
|
@@ -88,13 +120,18 @@ shabti config setup # show Qdrant setup instructions
|
|
|
88
120
|
shabti config setup --check # test Qdrant connection
|
|
89
121
|
```
|
|
90
122
|
|
|
123
|
+
### gc
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
shabti gc # remove entries past their TTL
|
|
127
|
+
```
|
|
128
|
+
|
|
91
129
|
## Interactive Mode (REPL)
|
|
92
130
|
|
|
93
131
|
Run `shabti` with no arguments in a terminal to enter interactive mode:
|
|
94
132
|
|
|
95
133
|
```
|
|
96
134
|
$ shabti
|
|
97
|
-
shabti v2.0.0
|
|
98
135
|
|
|
99
136
|
[info] Memory engine connected (/remember, /recall available)
|
|
100
137
|
[info] Interactive mode — model: gpt-4o-mini
|
|
@@ -119,14 +156,14 @@ Requires `OPENAI_API_KEY` in `.env` for chat functionality. Memory commands (`/r
|
|
|
119
156
|
|
|
120
157
|
## MCP Server
|
|
121
158
|
|
|
122
|
-
|
|
159
|
+
Shabti includes an MCP (Model Context Protocol) server for integration with Claude Code, Cursor, and other MCP-compatible tools.
|
|
123
160
|
|
|
124
161
|
```bash
|
|
125
162
|
# Start the MCP server (stdio transport)
|
|
126
163
|
npx shabti-mcp
|
|
127
164
|
```
|
|
128
165
|
|
|
129
|
-
### Claude Code configuration
|
|
166
|
+
### Claude Code / Cursor configuration
|
|
130
167
|
|
|
131
168
|
Generate the MCP settings JSON:
|
|
132
169
|
|
|
@@ -147,23 +184,57 @@ Or manually add to your MCP settings:
|
|
|
147
184
|
}
|
|
148
185
|
```
|
|
149
186
|
|
|
150
|
-
###
|
|
187
|
+
### MCP Tools
|
|
151
188
|
|
|
152
|
-
| Tool | Description
|
|
153
|
-
| --------------- |
|
|
154
|
-
| `memory_store` | Store a memory entry
|
|
155
|
-
| `memory_search` | Search memories by semantic similarity
|
|
156
|
-
| `memory_delete` | Delete a memory entry by ID
|
|
157
|
-
| `memory_list` | List recent memory entries
|
|
158
|
-
| `
|
|
189
|
+
| Tool | Description |
|
|
190
|
+
| --------------- | ----------------------------------------------------------- |
|
|
191
|
+
| `memory_store` | Store a memory entry with optional namespace, tags, and TTL |
|
|
192
|
+
| `memory_search` | Search memories by semantic similarity |
|
|
193
|
+
| `memory_delete` | Delete a memory entry by ID |
|
|
194
|
+
| `memory_list` | List recent memory entries |
|
|
195
|
+
| `memory_export` | Export entries as JSONL |
|
|
196
|
+
| `memory_gc` | Garbage collect expired entries |
|
|
197
|
+
| `memory_status` | Get engine status |
|
|
159
198
|
|
|
160
|
-
###
|
|
199
|
+
### MCP Resources
|
|
161
200
|
|
|
162
201
|
| URI | Description |
|
|
163
202
|
| ----------------- | ---------------------------- |
|
|
164
203
|
| `shabti://status` | Engine status and statistics |
|
|
165
204
|
| `shabti://config` | Current configuration |
|
|
166
205
|
|
|
206
|
+
## A2A Protocol Server
|
|
207
|
+
|
|
208
|
+
Shabti supports Google's [Agent-to-Agent (A2A) protocol](https://google.github.io/A2A/) v0.3.0 for inter-agent communication.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Start the A2A server (default port 3000)
|
|
212
|
+
shabti a2a
|
|
213
|
+
|
|
214
|
+
# Start on a custom port
|
|
215
|
+
shabti a2a --port 4000
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Agent Card
|
|
219
|
+
|
|
220
|
+
Discoverable at `GET /.well-known/agent-card.json`.
|
|
221
|
+
|
|
222
|
+
### Skills
|
|
223
|
+
|
|
224
|
+
| Skill | Description |
|
|
225
|
+
| --------------- | ------------------------------------ |
|
|
226
|
+
| `memory_store` | Store a memory entry via A2A message |
|
|
227
|
+
| `memory_search` | Semantic search via A2A message |
|
|
228
|
+
| `memory_status` | Engine status via A2A message |
|
|
229
|
+
|
|
230
|
+
### JSON-RPC Methods
|
|
231
|
+
|
|
232
|
+
| Method | Description |
|
|
233
|
+
| -------------- | -------------------------------- |
|
|
234
|
+
| `message/send` | Send a message to invoke a skill |
|
|
235
|
+
| `tasks/get` | Retrieve task status and results |
|
|
236
|
+
| `tasks/cancel` | Cancel a running task |
|
|
237
|
+
|
|
167
238
|
## Node.js API
|
|
168
239
|
|
|
169
240
|
```javascript
|
|
@@ -176,7 +247,11 @@ const engine = new ShabtiEngine({
|
|
|
176
247
|
});
|
|
177
248
|
|
|
178
249
|
// Store
|
|
179
|
-
await engine.store("Rust is a systems programming language", {
|
|
250
|
+
await engine.store("Rust is a systems programming language", {
|
|
251
|
+
namespace: "tech",
|
|
252
|
+
tags: ["rust", "programming"],
|
|
253
|
+
ttlSeconds: 86400, // expires in 24 hours
|
|
254
|
+
});
|
|
180
255
|
|
|
181
256
|
// Search
|
|
182
257
|
const results = await engine.executeQuery({
|
|
@@ -187,29 +262,52 @@ const results = await engine.executeQuery({
|
|
|
187
262
|
|
|
188
263
|
for (const r of results) {
|
|
189
264
|
console.log(`${r.score.toFixed(4)} ${r.content}`);
|
|
190
|
-
if (r.explanation) {
|
|
191
|
-
console.log(
|
|
192
|
-
` sim=${r.explanation.semanticSimilarity.toFixed(3)} ` +
|
|
193
|
-
`decay=${r.explanation.timeDecayFactor.toFixed(3)} ` +
|
|
194
|
-
`boost=${r.explanation.accessBoostFactor.toFixed(3)}`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
265
|
}
|
|
198
266
|
|
|
267
|
+
// Export
|
|
268
|
+
const entries = engine.listEntries({ namespace: "tech", limit: 100 });
|
|
269
|
+
|
|
270
|
+
// Garbage collect
|
|
271
|
+
const removed = await engine.gc();
|
|
272
|
+
|
|
199
273
|
await engine.shutdown();
|
|
200
274
|
```
|
|
201
275
|
|
|
276
|
+
## Configuration
|
|
277
|
+
|
|
278
|
+
### Environment Variables
|
|
279
|
+
|
|
280
|
+
| Variable | Description | Default |
|
|
281
|
+
| ------------------- | ----------------------------------------------------- | ----------------------- |
|
|
282
|
+
| `SHABTI_QDRANT_URL` | Override Qdrant URL | `http://localhost:6334` |
|
|
283
|
+
| `SHABTI_LOG_LEVEL` | Log level: `debug`, `info`, `warn`, `error`, `silent` | `info` |
|
|
284
|
+
| `SHABTI_LOG_JSON` | Set to `1` for JSON-formatted log output | — |
|
|
285
|
+
| `SHABTI_A2A_PORT` | A2A server port (standalone mode) | `3000` |
|
|
286
|
+
| `OPENAI_API_KEY` | Required for chat/REPL mode | — |
|
|
287
|
+
|
|
288
|
+
### Config File
|
|
289
|
+
|
|
290
|
+
Stored at `~/.shabti/config.json`. Manage via `shabti config` commands.
|
|
291
|
+
|
|
202
292
|
## Architecture
|
|
203
293
|
|
|
204
294
|
```
|
|
205
295
|
shabti (npm CLI + Node.js API)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
296
|
+
├── src/commands/ 12 CLI commands (Commander.js)
|
|
297
|
+
├── src/repl/ Interactive REPL with slash commands
|
|
298
|
+
├── src/mcp/ MCP server (stdio, 7 tools, 2 resources)
|
|
299
|
+
├── src/a2a/ A2A server (HTTP, JSON-RPC 2.0)
|
|
300
|
+
├── src/core/ Engine factory, session, retry logic
|
|
301
|
+
├── src/utils/ Style, validation, structured logger
|
|
302
|
+
└── native.cjs NAPI-RS bridge
|
|
303
|
+
└── crates/
|
|
304
|
+
├── shabti-engine Orchestration, store/search/gc
|
|
305
|
+
├── shabti-core Data models, scoring, dedup, query DSL
|
|
306
|
+
├── shabti-embedding fastembed-rs (MultilingualE5Small, 384-dim)
|
|
307
|
+
├── shabti-index Qdrant vector DB client
|
|
308
|
+
├── shabti-storage Append-only log, event store, snapshots
|
|
309
|
+
├── shabti-graph k-NN memory link graph
|
|
310
|
+
└── shabti-napi NAPI-RS bindings
|
|
213
311
|
```
|
|
214
312
|
|
|
215
313
|
## Benchmarks
|
|
@@ -227,10 +325,10 @@ See [BENCHMARKS.md](BENCHMARKS.md) for detailed results.
|
|
|
227
325
|
```bash
|
|
228
326
|
npm install # install JS dependencies
|
|
229
327
|
cargo build # build Rust workspace
|
|
230
|
-
npm test # run JS tests (vitest)
|
|
231
|
-
cargo test # run Rust tests
|
|
328
|
+
npm test # run JS tests (vitest) — 129 tests
|
|
329
|
+
cargo test # run Rust tests — 26 test modules
|
|
232
330
|
npm run lint # eslint
|
|
233
|
-
|
|
331
|
+
npm run ci # lint + format check + audit + coverage
|
|
234
332
|
```
|
|
235
333
|
|
|
236
334
|
## License
|
package/package.json
CHANGED
package/src/a2a/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createServer } from "http";
|
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { buildAgentCard } from "./agentCard.js";
|
|
4
4
|
import { createEngine } from "../core/engine.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
5
6
|
|
|
6
7
|
// ============================================================
|
|
7
8
|
// Task Store (in-memory)
|
|
@@ -289,7 +290,9 @@ export function startA2AServer(port = 3000) {
|
|
|
289
290
|
if (engineError) throw engineError;
|
|
290
291
|
try {
|
|
291
292
|
engine = createEngine();
|
|
293
|
+
logger.info("A2A engine initialized");
|
|
292
294
|
} catch (err) {
|
|
295
|
+
logger.error("A2A engine initialization failed", { error: err.message });
|
|
293
296
|
engineError = err;
|
|
294
297
|
throw err;
|
|
295
298
|
}
|
|
@@ -322,7 +325,7 @@ export function startA2AServer(port = 3000) {
|
|
|
322
325
|
let body;
|
|
323
326
|
try {
|
|
324
327
|
body = await readBody(req);
|
|
325
|
-
} catch {
|
|
328
|
+
} catch (_) {
|
|
326
329
|
res.writeHead(400);
|
|
327
330
|
return res.end();
|
|
328
331
|
}
|
|
@@ -330,7 +333,7 @@ export function startA2AServer(port = 3000) {
|
|
|
330
333
|
let rpc;
|
|
331
334
|
try {
|
|
332
335
|
rpc = JSON.parse(body);
|
|
333
|
-
} catch {
|
|
336
|
+
} catch (_) {
|
|
334
337
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
335
338
|
return res.end(
|
|
336
339
|
JSON.stringify({
|
|
@@ -367,5 +370,21 @@ export function startA2AServer(port = 3000) {
|
|
|
367
370
|
console.log(` Skills: ${agentCard.skills.map((s) => s.id).join(", ")}\n`);
|
|
368
371
|
});
|
|
369
372
|
|
|
373
|
+
async function shutdown() {
|
|
374
|
+
console.log("\n Shutting down A2A server...");
|
|
375
|
+
server.close();
|
|
376
|
+
if (engine && engine.shutdown) {
|
|
377
|
+
try {
|
|
378
|
+
await engine.shutdown();
|
|
379
|
+
} catch (_) {
|
|
380
|
+
// best-effort
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
process.exit(0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
process.on("SIGINT", shutdown);
|
|
387
|
+
process.on("SIGTERM", shutdown);
|
|
388
|
+
|
|
370
389
|
return server;
|
|
371
390
|
}
|
package/src/commands/a2a.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { startA2AServer } from "../a2a/server.js";
|
|
2
|
+
import { error } from "../utils/style.js";
|
|
3
|
+
import { parsePort } from "../utils/validate.js";
|
|
2
4
|
|
|
3
5
|
export function registerA2A(program) {
|
|
4
6
|
program
|
|
@@ -6,7 +8,12 @@ export function registerA2A(program) {
|
|
|
6
8
|
.description("Start the A2A (Agent-to-Agent) protocol server")
|
|
7
9
|
.option("-p, --port <port>", "Port to listen on", "3000")
|
|
8
10
|
.action((opts) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
try {
|
|
12
|
+
const port = parsePort(opts.port, "--port");
|
|
13
|
+
startA2AServer(port);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
error(err.message);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
11
18
|
});
|
|
12
19
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createEngine } from "../core/engine.js";
|
|
2
|
+
import { success, error } from "../utils/style.js";
|
|
3
|
+
|
|
4
|
+
export function registerDelete(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("delete")
|
|
7
|
+
.description("Delete a memory entry by ID")
|
|
8
|
+
.argument("<id>", "UUID of the memory entry to delete")
|
|
9
|
+
.action(async (id) => {
|
|
10
|
+
try {
|
|
11
|
+
const engine = createEngine();
|
|
12
|
+
await engine.delete(id);
|
|
13
|
+
success(`Deleted: ${id}`);
|
|
14
|
+
await engine.shutdown();
|
|
15
|
+
} catch (err) {
|
|
16
|
+
error(err.message);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
package/src/commands/export.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "fs";
|
|
2
2
|
import { createEngine } from "../core/engine.js";
|
|
3
3
|
import { success, error } from "../utils/style.js";
|
|
4
|
+
import { parsePositiveInt } from "../utils/validate.js";
|
|
4
5
|
|
|
5
6
|
export function registerExport(program) {
|
|
6
7
|
program
|
|
@@ -15,7 +16,7 @@ export function registerExport(program) {
|
|
|
15
16
|
const engine = createEngine();
|
|
16
17
|
const listOpts = {};
|
|
17
18
|
if (opts.namespace) listOpts.namespace = opts.namespace;
|
|
18
|
-
if (opts.limit) listOpts.limit =
|
|
19
|
+
if (opts.limit) listOpts.limit = parsePositiveInt(opts.limit, "--limit");
|
|
19
20
|
listOpts.includeEmbeddings = opts.embeddings !== false ? false : false;
|
|
20
21
|
// --no-embeddings sets opts.embeddings = false (commander negatable)
|
|
21
22
|
// default: exclude embeddings for smaller output
|
package/src/commands/import.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { createEngine } from "../core/engine.js";
|
|
3
3
|
import { success, error, info, warn } from "../utils/style.js";
|
|
4
4
|
|
|
@@ -11,6 +11,11 @@ export function registerImport(program) {
|
|
|
11
11
|
.option("--dry-run", "Parse and validate without storing")
|
|
12
12
|
.action(async (file, opts) => {
|
|
13
13
|
try {
|
|
14
|
+
if (!existsSync(file)) {
|
|
15
|
+
error(`File not found: ${file}`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
14
19
|
const raw = readFileSync(file, "utf8");
|
|
15
20
|
const lines = raw
|
|
16
21
|
.split("\n")
|
|
@@ -27,7 +32,7 @@ export function registerImport(program) {
|
|
|
27
32
|
for (let i = 0; i < lines.length; i++) {
|
|
28
33
|
try {
|
|
29
34
|
entries.push(JSON.parse(lines[i]));
|
|
30
|
-
} catch {
|
|
35
|
+
} catch (_) {
|
|
31
36
|
warn(`Skipping invalid JSON on line ${i + 1}`);
|
|
32
37
|
}
|
|
33
38
|
}
|
package/src/commands/search.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import Table from "cli-table3";
|
|
3
3
|
import { createEngine } from "../core/engine.js";
|
|
4
4
|
import { error, heading } from "../utils/style.js";
|
|
5
|
+
import { parsePositiveInt, parseScore } from "../utils/validate.js";
|
|
5
6
|
|
|
6
7
|
export function registerSearch(program) {
|
|
7
8
|
program
|
|
@@ -23,13 +24,13 @@ export function registerSearch(program) {
|
|
|
23
24
|
const engine = createEngine();
|
|
24
25
|
|
|
25
26
|
const query = { text: queryText };
|
|
26
|
-
query.limit =
|
|
27
|
+
query.limit = parsePositiveInt(opts.limit, "--limit");
|
|
27
28
|
if (opts.namespace) query.namespace = opts.namespace;
|
|
28
|
-
if (opts.timeStart) query.timeStart =
|
|
29
|
-
if (opts.timeEnd) query.timeEnd =
|
|
30
|
-
if (opts.cluster) query.clusterId =
|
|
31
|
-
if (opts.followLinks) query.maxHops =
|
|
32
|
-
if (opts.minScore) query.minScore =
|
|
29
|
+
if (opts.timeStart) query.timeStart = parsePositiveInt(opts.timeStart, "--time-start");
|
|
30
|
+
if (opts.timeEnd) query.timeEnd = parsePositiveInt(opts.timeEnd, "--time-end");
|
|
31
|
+
if (opts.cluster) query.clusterId = parsePositiveInt(opts.cluster, "--cluster");
|
|
32
|
+
if (opts.followLinks) query.maxHops = parsePositiveInt(opts.followLinks, "--follow-links");
|
|
33
|
+
if (opts.minScore) query.minScore = parseScore(opts.minScore, "--min-score");
|
|
33
34
|
if (opts.explain) query.withExplanation = true;
|
|
34
35
|
if (opts.excludeSuperseded) query.excludeSuperseded = true;
|
|
35
36
|
|
package/src/commands/store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createEngine, loadConfig, saveConfig } from "../core/engine.js";
|
|
2
2
|
import { success, error, info, warn } from "../utils/style.js";
|
|
3
|
+
import { parsePositiveInt } from "../utils/validate.js";
|
|
3
4
|
|
|
4
5
|
export function registerStore(program) {
|
|
5
6
|
program
|
|
@@ -17,7 +18,7 @@ export function registerStore(program) {
|
|
|
17
18
|
if (opts.namespace) options.namespace = opts.namespace;
|
|
18
19
|
if (opts.session) options.sessionId = opts.session;
|
|
19
20
|
if (opts.tags) options.tags = opts.tags.split(",").map((t) => t.trim());
|
|
20
|
-
if (opts.ttl) options.ttlSeconds =
|
|
21
|
+
if (opts.ttl) options.ttlSeconds = parsePositiveInt(opts.ttl, "--ttl");
|
|
21
22
|
|
|
22
23
|
// Model versioning check
|
|
23
24
|
const currentModelId = engine.modelId();
|
package/src/core/engine.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
4
|
+
const DEFAULT_BACKOFF_MS = [500, 1000, 2000];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create an engine with retry and exponential backoff.
|
|
8
|
+
* Intended for long-running servers (MCP, A2A) — not for one-shot CLI commands.
|
|
9
|
+
*
|
|
10
|
+
* @param {Function} factory - Function that creates the engine (e.g., createEngine)
|
|
11
|
+
* @param {object} [options]
|
|
12
|
+
* @param {number} [options.maxRetries=3]
|
|
13
|
+
* @param {number[]} [options.backoffMs=[500,1000,2000]]
|
|
14
|
+
* @returns {Promise<object>} The created engine
|
|
15
|
+
*/
|
|
16
|
+
export async function createEngineWithRetry(factory, options = {}) {
|
|
17
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
18
|
+
const backoffs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
19
|
+
|
|
20
|
+
let lastError;
|
|
21
|
+
|
|
22
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
const engine = factory();
|
|
25
|
+
if (attempt > 0) {
|
|
26
|
+
logger.info(`Engine connected after ${attempt + 1} attempts`);
|
|
27
|
+
}
|
|
28
|
+
return engine;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
lastError = err;
|
|
31
|
+
if (attempt < maxRetries) {
|
|
32
|
+
const delay = backoffs[Math.min(attempt, backoffs.length - 1)];
|
|
33
|
+
logger.warn(
|
|
34
|
+
`Engine connection failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms`,
|
|
35
|
+
{
|
|
36
|
+
error: err.message,
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logger.error(`Engine connection failed after ${maxRetries + 1} attempts`, {
|
|
45
|
+
error: lastError.message,
|
|
46
|
+
hint: "Is Qdrant running? Try: shabti config setup --check",
|
|
47
|
+
});
|
|
48
|
+
throw lastError;
|
|
49
|
+
}
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerStatus } from "./commands/status.js";
|
|
|
15
15
|
import { registerExport } from "./commands/export.js";
|
|
16
16
|
import { registerImport } from "./commands/import.js";
|
|
17
17
|
import { registerStore } from "./commands/store.js";
|
|
18
|
+
import { registerDelete } from "./commands/delete.js";
|
|
18
19
|
|
|
19
20
|
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
20
21
|
|
|
@@ -84,6 +85,7 @@ function buildProgram() {
|
|
|
84
85
|
registerExport(program);
|
|
85
86
|
registerImport(program);
|
|
86
87
|
registerStore(program);
|
|
88
|
+
registerDelete(program);
|
|
87
89
|
|
|
88
90
|
program
|
|
89
91
|
.command("gc")
|
package/src/mcp/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from "readline";
|
|
3
3
|
import { createEngine, loadConfig } from "../core/engine.js";
|
|
4
|
+
import { createEngineWithRetry } from "../core/retry.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
4
6
|
|
|
5
7
|
const SERVER_INFO = {
|
|
6
8
|
name: "shabti-memory",
|
|
@@ -123,11 +125,22 @@ let engineInitAttempted = false;
|
|
|
123
125
|
function initEngine() {
|
|
124
126
|
if (engine) return engine;
|
|
125
127
|
if (engineInitAttempted) return null;
|
|
128
|
+
// Synchronous fallback for initial attempt
|
|
126
129
|
engineInitAttempted = true;
|
|
127
130
|
try {
|
|
128
131
|
engine = createEngine();
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
logger.info("Engine initialized");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
logger.warn("Engine not available, starting background retry", { error: err.message });
|
|
135
|
+
// Start async retry in background
|
|
136
|
+
createEngineWithRetry(createEngine)
|
|
137
|
+
.then((eng) => {
|
|
138
|
+
engine = eng;
|
|
139
|
+
logger.info("Engine connected via retry");
|
|
140
|
+
})
|
|
141
|
+
.catch(() => {
|
|
142
|
+
logger.error("Engine retry exhausted — tools will return errors");
|
|
143
|
+
});
|
|
131
144
|
}
|
|
132
145
|
return engine;
|
|
133
146
|
}
|
|
@@ -415,7 +428,7 @@ async function handleRequest(line) {
|
|
|
415
428
|
let req;
|
|
416
429
|
try {
|
|
417
430
|
req = JSON.parse(line);
|
|
418
|
-
} catch {
|
|
431
|
+
} catch (_) {
|
|
419
432
|
return respondError(null, -32700, "Parse error");
|
|
420
433
|
}
|
|
421
434
|
|
|
@@ -445,4 +458,20 @@ rl.on("line", (line) => {
|
|
|
445
458
|
const trimmed = line.trim();
|
|
446
459
|
if (trimmed) handleRequest(trimmed);
|
|
447
460
|
});
|
|
448
|
-
|
|
461
|
+
|
|
462
|
+
async function shutdown() {
|
|
463
|
+
logger.info("MCP server shutting down");
|
|
464
|
+
rl.close();
|
|
465
|
+
if (engine && engine.shutdown) {
|
|
466
|
+
try {
|
|
467
|
+
await engine.shutdown();
|
|
468
|
+
} catch (_) {
|
|
469
|
+
// best-effort
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
rl.on("close", shutdown);
|
|
476
|
+
process.on("SIGINT", shutdown);
|
|
477
|
+
process.on("SIGTERM", shutdown);
|
package/src/repl/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function launchRepl() {
|
|
|
26
26
|
info(
|
|
27
27
|
`Memory engine connected (${chalk.cyan("/remember")}, ${chalk.cyan("/recall")} available)`,
|
|
28
28
|
);
|
|
29
|
-
} catch {
|
|
29
|
+
} catch (_) {
|
|
30
30
|
warn("Memory engine not available. Chat-only mode (start Qdrant to enable memory).");
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
2
|
+
|
|
3
|
+
function getLevel() {
|
|
4
|
+
const name = (process.env.SHABTI_LOG_LEVEL || "info").toLowerCase();
|
|
5
|
+
return LEVELS[name] ?? LEVELS.info;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isJsonMode() {
|
|
9
|
+
return process.env.SHABTI_LOG_JSON === "1";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldLog(level) {
|
|
13
|
+
return LEVELS[level] >= getLevel();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatTimestamp() {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function log(level, message, data) {
|
|
21
|
+
if (!shouldLog(level)) return;
|
|
22
|
+
|
|
23
|
+
if (isJsonMode()) {
|
|
24
|
+
const entry = { timestamp: formatTimestamp(), level, message };
|
|
25
|
+
if (data !== undefined) entry.data = data;
|
|
26
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
27
|
+
} else {
|
|
28
|
+
const prefix = `[${formatTimestamp()}] [${level.toUpperCase()}]`;
|
|
29
|
+
const msg =
|
|
30
|
+
data !== undefined ? `${prefix} ${message} ${JSON.stringify(data)}` : `${prefix} ${message}`;
|
|
31
|
+
process.stderr.write(msg + "\n");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const logger = {
|
|
36
|
+
debug: (message, data) => log("debug", message, data),
|
|
37
|
+
info: (message, data) => log("info", message, data),
|
|
38
|
+
warn: (message, data) => log("warn", message, data),
|
|
39
|
+
error: (message, data) => log("error", message, data),
|
|
40
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a string as a positive integer. Returns the number or throws.
|
|
3
|
+
*/
|
|
4
|
+
export function parsePositiveInt(value, name) {
|
|
5
|
+
const n = parseInt(value, 10);
|
|
6
|
+
if (isNaN(n) || n <= 0) {
|
|
7
|
+
throw new Error(`${name} must be a positive integer, got: ${value}`);
|
|
8
|
+
}
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a string as a non-negative integer (>= 0). Returns the number or throws.
|
|
14
|
+
*/
|
|
15
|
+
export function parseNonNegativeInt(value, name) {
|
|
16
|
+
const n = parseInt(value, 10);
|
|
17
|
+
if (isNaN(n) || n < 0) {
|
|
18
|
+
throw new Error(`${name} must be a non-negative integer, got: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a string as a positive float. Returns the number or throws.
|
|
25
|
+
*/
|
|
26
|
+
export function parsePositiveFloat(value, name) {
|
|
27
|
+
const n = parseFloat(value);
|
|
28
|
+
if (isNaN(n) || n <= 0) {
|
|
29
|
+
throw new Error(`${name} must be a positive number, got: ${value}`);
|
|
30
|
+
}
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a string as a float in [0, 1]. Returns the number or throws.
|
|
36
|
+
*/
|
|
37
|
+
export function parseScore(value, name) {
|
|
38
|
+
const n = parseFloat(value);
|
|
39
|
+
if (isNaN(n) || n < 0 || n > 1) {
|
|
40
|
+
throw new Error(`${name} must be a number between 0 and 1, got: ${value}`);
|
|
41
|
+
}
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a string as a valid TCP port (1–65535). Returns the number or throws.
|
|
47
|
+
*/
|
|
48
|
+
export function parsePort(value, name) {
|
|
49
|
+
const n = parseInt(value, 10);
|
|
50
|
+
if (isNaN(n) || n < 1 || n > 65535) {
|
|
51
|
+
throw new Error(`${name} must be a port number (1–65535), got: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
return n;
|
|
54
|
+
}
|