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 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
- | `status` | Show engine status |
50
- | `snapshot` | Manage storage snapshots |
51
- | `config` | Manage configuration |
52
- | `chat` | Interactive chat with OpenAI |
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
- shabti includes an MCP (Model Context Protocol) server for integration with Claude Code, Cursor, and other MCP-compatible tools.
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
- ### Available tools
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
- | `memory_status` | Get engine status |
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
- ### Available resources
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
- └── shabti-napi (Rust Node.js FFI via NAPI-RS)
207
- └── shabti-engine (orchestration layer)
208
- ├── shabti-embedding (fastembed-rs, MultilingualE5Small 384-dim)
209
- ├── shabti-index (Qdrant vector DB client)
210
- ├── shabti-storage (append-only log, event store, snapshots)
211
- ├── shabti-graph (k-NN memory link graph)
212
- └── shabti-core (data models, scoring, dedup, query DSL)
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
- cargo clippy # Rust linter
331
+ npm run ci # lint + format check + audit + coverage
234
332
  ```
235
333
 
236
334
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shabti",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Agent Memory OS — semantic memory for AI agents",
5
5
  "type": "module",
6
6
  "main": "native.cjs",
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
  }
@@ -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
- const port = parseInt(opts.port, 10);
10
- startA2AServer(port);
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
+ }
@@ -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 = parseInt(opts.limit, 10);
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
@@ -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
  }
@@ -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 = parseInt(opts.limit, 10);
27
+ query.limit = parsePositiveInt(opts.limit, "--limit");
27
28
  if (opts.namespace) query.namespace = opts.namespace;
28
- if (opts.timeStart) query.timeStart = parseInt(opts.timeStart, 10);
29
- if (opts.timeEnd) query.timeEnd = parseInt(opts.timeEnd, 10);
30
- if (opts.cluster) query.clusterId = parseInt(opts.cluster, 10);
31
- if (opts.followLinks) query.maxHops = parseInt(opts.followLinks, 10);
32
- if (opts.minScore) query.minScore = parseFloat(opts.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
 
@@ -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 = parseInt(opts.ttl, 10);
21
+ if (opts.ttl) options.ttlSeconds = parsePositiveInt(opts.ttl, "--ttl");
21
22
 
22
23
  // Model versioning check
23
24
  const currentModelId = engine.modelId();
@@ -26,7 +26,7 @@ export function loadConfig() {
26
26
  if (existsSync(CONFIG_PATH)) {
27
27
  try {
28
28
  config = { ...config, ...JSON.parse(readFileSync(CONFIG_PATH, "utf8")) };
29
- } catch {
29
+ } catch (_) {
30
30
  // keep defaults
31
31
  }
32
32
  }
@@ -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
- } catch {
130
- // engine stays null — tools will return errors
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
- rl.on("close", () => process.exit(0));
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
+ }