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 +1 -1
- package/src/commands/export.js +44 -0
- package/src/commands/import.js +66 -0
- package/src/commands/store.js +2 -0
- package/src/index.js +21 -0
- package/src/mcp/server.js +66 -0
- package/src/repl/slashCommands.js +21 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/store.js
CHANGED
|
@@ -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
|
+
}
|