shabti 2.2.0 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shabti",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Agent Memory OS — semantic memory for AI agents",
5
5
  "type": "module",
6
6
  "main": "native.cjs",
@@ -0,0 +1,44 @@
1
+ import { writeFileSync } from "fs";
2
+ import { createEngine } from "../core/engine.js";
3
+ import { success, error } from "../utils/style.js";
4
+
5
+ export function registerExport(program) {
6
+ program
7
+ .command("export")
8
+ .description("Export memory entries as JSONL")
9
+ .option("-n, --namespace <ns>", "Filter by namespace")
10
+ .option("--no-embeddings", "Exclude embedding vectors from output")
11
+ .option("-o, --output <file>", "Write to file instead of stdout")
12
+ .option("--limit <n>", "Maximum number of entries to export")
13
+ .action(async (opts) => {
14
+ try {
15
+ const engine = createEngine();
16
+ const listOpts = {};
17
+ if (opts.namespace) listOpts.namespace = opts.namespace;
18
+ if (opts.limit) listOpts.limit = parseInt(opts.limit, 10);
19
+ listOpts.includeEmbeddings = opts.embeddings !== false ? false : false;
20
+ // --no-embeddings sets opts.embeddings = false (commander negatable)
21
+ // default: exclude embeddings for smaller output
22
+ listOpts.includeEmbeddings = opts.embeddings === true;
23
+
24
+ const entries = engine.listEntries(listOpts);
25
+
26
+ const lines = entries.map((e) => JSON.stringify(e));
27
+ const output = lines.join("\n");
28
+
29
+ if (opts.output) {
30
+ writeFileSync(opts.output, output + (lines.length ? "\n" : ""), "utf8");
31
+ success(`Exported ${entries.length} entries to ${opts.output}`);
32
+ } else {
33
+ if (output) console.log(output);
34
+ // Print summary to stderr so it doesn't pollute piped output
35
+ process.stderr.write(`\n${entries.length} entries exported\n`);
36
+ }
37
+
38
+ await engine.shutdown();
39
+ } catch (err) {
40
+ error(err.message);
41
+ process.exitCode = 1;
42
+ }
43
+ });
44
+ }
@@ -0,0 +1,66 @@
1
+ import { readFileSync } from "fs";
2
+ import { createEngine } from "../core/engine.js";
3
+ import { success, error, info, warn } from "../utils/style.js";
4
+
5
+ export function registerImport(program) {
6
+ program
7
+ .command("import")
8
+ .description("Import memory entries from a JSONL file")
9
+ .argument("<file>", "Path to JSONL file")
10
+ .option("-n, --namespace <ns>", "Override namespace for all imported entries")
11
+ .option("--dry-run", "Parse and validate without storing")
12
+ .action(async (file, opts) => {
13
+ try {
14
+ const raw = readFileSync(file, "utf8");
15
+ const lines = raw
16
+ .split("\n")
17
+ .map((l) => l.trim())
18
+ .filter((l) => l.length > 0);
19
+
20
+ if (lines.length === 0) {
21
+ info("No entries found in file.");
22
+ return;
23
+ }
24
+
25
+ // Parse all lines first to validate
26
+ const entries = [];
27
+ for (let i = 0; i < lines.length; i++) {
28
+ try {
29
+ entries.push(JSON.parse(lines[i]));
30
+ } catch {
31
+ warn(`Skipping invalid JSON on line ${i + 1}`);
32
+ }
33
+ }
34
+
35
+ if (opts.dryRun) {
36
+ info(`Dry run: ${entries.length} entries parsed, 0 stored.`);
37
+ return;
38
+ }
39
+
40
+ const engine = createEngine();
41
+ let stored = 0;
42
+ let skipped = 0;
43
+
44
+ for (const entry of entries) {
45
+ const storeOpts = {};
46
+ storeOpts.namespace = opts.namespace || entry.namespace || undefined;
47
+ if (entry.tags && entry.tags.length) storeOpts.tags = entry.tags;
48
+ if (entry.sessionId || entry.session_id)
49
+ storeOpts.sessionId = entry.sessionId || entry.session_id;
50
+
51
+ const result = await engine.store(entry.content, storeOpts);
52
+ if (result.status === "stored") {
53
+ stored++;
54
+ } else {
55
+ skipped++;
56
+ }
57
+ }
58
+
59
+ success(`Import complete: ${stored} stored, ${skipped} skipped (duplicates)`);
60
+ await engine.shutdown();
61
+ } catch (err) {
62
+ error(err.message);
63
+ process.exitCode = 1;
64
+ }
65
+ });
66
+ }
@@ -9,6 +9,7 @@ export function registerStore(program) {
9
9
  .option("-n, --namespace <ns>", "Namespace for the entry")
10
10
  .option("-s, --session <id>", "Session ID")
11
11
  .option("-t, --tags <tags>", "Comma-separated tags")
12
+ .option("--ttl <seconds>", "Time-to-live in seconds (auto-expires after this duration)")
12
13
  .action(async (content, opts) => {
13
14
  try {
14
15
  const engine = createEngine();
@@ -16,6 +17,7 @@ export function registerStore(program) {
16
17
  if (opts.namespace) options.namespace = opts.namespace;
17
18
  if (opts.session) options.sessionId = opts.session;
18
19
  if (opts.tags) options.tags = opts.tags.split(",").map((t) => t.trim());
20
+ if (opts.ttl) options.ttlSeconds = parseInt(opts.ttl, 10);
19
21
 
20
22
  // Model versioning check
21
23
  const currentModelId = engine.modelId();
package/src/index.js CHANGED
@@ -11,6 +11,8 @@ import { registerSearch } from "./commands/search.js";
11
11
  import { registerSnapshot } from "./commands/snapshot.js";
12
12
  import { registerSpin } from "./commands/spin.js";
13
13
  import { registerStatus } from "./commands/status.js";
14
+ import { registerExport } from "./commands/export.js";
15
+ import { registerImport } from "./commands/import.js";
14
16
  import { registerStore } from "./commands/store.js";
15
17
 
16
18
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -77,8 +79,27 @@ function buildProgram() {
77
79
  registerSnapshot(program);
78
80
  registerSpin(program);
79
81
  registerStatus(program);
82
+ registerExport(program);
83
+ registerImport(program);
80
84
  registerStore(program);
81
85
 
86
+ program
87
+ .command("gc")
88
+ .description("Garbage collect expired memory entries")
89
+ .action(async () => {
90
+ const { createEngine } = await import("./core/engine.js");
91
+ const { success, error: showError } = await import("./utils/style.js");
92
+ try {
93
+ const engine = createEngine();
94
+ const removed = await engine.gc();
95
+ success(`GC complete: ${removed} expired entries removed`);
96
+ await engine.shutdown();
97
+ } catch (err) {
98
+ showError(err.message);
99
+ process.exitCode = 1;
100
+ }
101
+ });
102
+
82
103
  program
83
104
  .command("mcp-config")
84
105
  .description("Print MCP server configuration JSON for Claude Code / Cursor")
package/src/mcp/server.js CHANGED
@@ -24,6 +24,10 @@ const TOOLS = [
24
24
  items: { type: "string" },
25
25
  description: "Tags to associate with the memory",
26
26
  },
27
+ ttl: {
28
+ type: "integer",
29
+ description: "Time-to-live in seconds (entry auto-expires after this duration)",
30
+ },
27
31
  },
28
32
  required: ["content"],
29
33
  },
@@ -69,6 +73,25 @@ const TOOLS = [
69
73
  },
70
74
  },
71
75
  },
76
+ {
77
+ name: "memory_export",
78
+ description: "Export all memory entries as a JSONL array",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ namespace: { type: "string", description: "Filter by namespace" },
83
+ limit: { type: "integer", description: "Maximum entries to export" },
84
+ },
85
+ },
86
+ },
87
+ {
88
+ name: "memory_gc",
89
+ description: "Garbage collect expired memory entries (removes entries past their TTL)",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {},
93
+ },
94
+ },
72
95
  {
73
96
  name: "memory_status",
74
97
  description: "Get the current status of the memory engine",
@@ -191,6 +214,7 @@ async function handleToolsCall(id, params) {
191
214
  const opts = {};
192
215
  if (args.namespace) opts.namespace = args.namespace;
193
216
  if (args.tags) opts.tags = args.tags;
217
+ if (args.ttl) opts.ttlSeconds = args.ttl;
194
218
  const result = await eng.store(content, opts);
195
219
  return respond(id, {
196
220
  content: [
@@ -293,6 +317,48 @@ async function handleToolsCall(id, params) {
293
317
  }
294
318
  }
295
319
 
320
+ if (name === "memory_export") {
321
+ if (!eng) {
322
+ return respondError(id, -32603, "Engine not available");
323
+ }
324
+ try {
325
+ const listOpts = {};
326
+ if (args?.namespace) listOpts.namespace = args.namespace;
327
+ if (args?.limit) listOpts.limit = args.limit;
328
+ const entries = eng.listEntries(listOpts);
329
+ const lines = entries.map((e) => JSON.stringify(e));
330
+ return respond(id, {
331
+ content: [
332
+ {
333
+ type: "text",
334
+ text: JSON.stringify({ entries: entries.length, data: lines.join("\n") }, null, 2),
335
+ },
336
+ ],
337
+ });
338
+ } catch (err) {
339
+ return respondError(id, -32603, err.message);
340
+ }
341
+ }
342
+
343
+ if (name === "memory_gc") {
344
+ if (!eng) {
345
+ return respondError(id, -32603, "Engine not available");
346
+ }
347
+ try {
348
+ const removed = await eng.gc();
349
+ return respond(id, {
350
+ content: [
351
+ {
352
+ type: "text",
353
+ text: JSON.stringify({ removed }, null, 2),
354
+ },
355
+ ],
356
+ });
357
+ } catch (err) {
358
+ return respondError(id, -32603, err.message);
359
+ }
360
+ }
361
+
296
362
  respondError(id, -32601, `Unknown tool: ${name}`);
297
363
  }
298
364
 
@@ -9,6 +9,7 @@ const COMMANDS = {
9
9
  "/history": "Show conversation history",
10
10
  "/remember": "Store a memory (e.g. /remember Tokyo is the capital of Japan)",
11
11
  "/recall": "Search memories (e.g. /recall capital of Japan)",
12
+ "/gc": "Garbage collect expired memory entries",
12
13
  };
13
14
 
14
15
  /**
@@ -75,6 +76,9 @@ export function handleSlashCommand(cmd, args, session, rl, engine = null) {
75
76
  case "/recall":
76
77
  return handleRecall(args, engine);
77
78
 
79
+ case "/gc":
80
+ return handleGc(engine);
81
+
78
82
  default:
79
83
  return false;
80
84
  }
@@ -136,3 +140,20 @@ async function handleRecall(query, engine) {
136
140
  console.log();
137
141
  return true;
138
142
  }
143
+
144
+ async function handleGc(engine) {
145
+ if (!engine) {
146
+ console.log();
147
+ warn("Memory engine not available. Start Qdrant to enable memory features.");
148
+ console.log();
149
+ return true;
150
+ }
151
+ try {
152
+ const removed = await engine.gc();
153
+ success(`GC complete: ${removed} expired entries removed`);
154
+ } catch (err) {
155
+ error(`Failed to run GC: ${err.message}`);
156
+ }
157
+ console.log();
158
+ return true;
159
+ }