memory-crystal 0.2.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/.env.example +20 -0
- package/CHANGELOG.md +6 -0
- package/LETTERS.md +22 -0
- package/LICENSE +21 -0
- package/README-ENTERPRISE.md +162 -0
- package/README-old.md +275 -0
- package/README.md +91 -0
- package/RELAY.md +88 -0
- package/TECHNICAL.md +379 -0
- package/ai/dev-updates/2026-02-25--cc-air--phase2-architecture-pivot.md +70 -0
- package/ai/dev-updates/2026-02-25--cc-air--phase2-worker-build.md +72 -0
- package/ai/dev-updates/2026-02-26--10-25-16--cc-mini--phase2-implementation.md +49 -0
- package/ai/dev-updates/2026-02-27--20-30-00--cc-mini--readme-overhaul-and-public-deploy.md +69 -0
- package/ai/notes/2026-02-26--cc-air--notes.md +412 -0
- package/ai/notes/2026-02-27--cc-mini--grok-feedback.md +44 -0
- package/ai/notes/2026-02-27--cc-mini--lesa-feedback.md +45 -0
- package/ai/notes/RESEARCH.md +1185 -0
- package/ai/notes/salience-research/README.md +29 -0
- package/ai/notes/salience-research/eurosla-salience-review.md +64 -0
- package/ai/notes/salience-research/full-research-summary.md +269 -0
- package/ai/notes/salience-research/salience-levels-diagram.png +0 -0
- package/ai/plan/2026-02-27--cc-mini--qr-pairing-spec.md +203 -0
- package/ai/plan/_archive/PLAN.md +194 -0
- package/ai/plan/_archive/PRD.md +1014 -0
- package/ai/plan/cc-plans-duplicates-from-dot-claude/2026-02-26--cc-mini--phase2-implementation-plan.md +245 -0
- package/ai/plan/dev-conventions-note.md +70 -0
- package/ai/plan/ldm-os-install-and-boot-architecture.md +285 -0
- package/ai/plan/memory-crystal-phase2-plan.md +192 -0
- package/ai/plan/memory-system-lay-of-the-land.md +214 -0
- package/ai/plan/phase2-ephemeral-relay.md +238 -0
- package/ai/plan/readme-first.md +68 -0
- package/ai/plan/roadmap.md +159 -0
- package/ai/todos/PUNCHLIST.md +44 -0
- package/ai/todos/README.md +31 -0
- package/ai/todos/inboxes/cc-air/2026-02-26--cc-air--post-relay-todos.md +85 -0
- package/ai/todos/inboxes/cc-mini/2026-02-26--cc-mini--phase2-status.md +100 -0
- package/ai/todos/inboxes/cc-mini/_archive/TODO.md +25 -0
- package/ai/todos/inboxes/parker/2026-02-25--cc-air--setup-checklist.md +139 -0
- package/ai/todos/inboxes/parker/2026-02-26--cc-mini--phase2-your-moves.md +72 -0
- package/dist/cc-hook.d.ts +1 -0
- package/dist/cc-hook.js +349 -0
- package/dist/chunk-3VFIJYS4.js +818 -0
- package/dist/chunk-52QE3YI3.js +1169 -0
- package/dist/chunk-AA3OPP4Z.js +432 -0
- package/dist/chunk-D3I3ZSE2.js +411 -0
- package/dist/chunk-EKSACBTJ.js +1070 -0
- package/dist/chunk-F3Y7EL7K.js +83 -0
- package/dist/chunk-JWZXYVET.js +1068 -0
- package/dist/chunk-KYVWO6ZM.js +1069 -0
- package/dist/chunk-L3VHARQH.js +413 -0
- package/dist/chunk-LOVAHSQV.js +411 -0
- package/dist/chunk-LQOYCAGG.js +446 -0
- package/dist/chunk-MK42FMEG.js +147 -0
- package/dist/chunk-NIJCVN3O.js +147 -0
- package/dist/chunk-O2UITJGH.js +465 -0
- package/dist/chunk-PEK6JH65.js +432 -0
- package/dist/chunk-PJ6FFKEX.js +77 -0
- package/dist/chunk-PLUBBZYR.js +800 -0
- package/dist/chunk-SGL6ISBJ.js +1061 -0
- package/dist/chunk-UNHVZB5G.js +411 -0
- package/dist/chunk-VAFTWSTE.js +1061 -0
- package/dist/chunk-XZ3S56RQ.js +1061 -0
- package/dist/chunk-Y72C7F6O.js +148 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +325 -0
- package/dist/core.d.ts +188 -0
- package/dist/core.js +12 -0
- package/dist/crypto.d.ts +16 -0
- package/dist/crypto.js +18 -0
- package/dist/dev-update-SZ2Z4WCQ.js +6 -0
- package/dist/ldm.d.ts +17 -0
- package/dist/ldm.js +12 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +250 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +89 -0
- package/dist/mirror-sync.d.ts +1 -0
- package/dist/mirror-sync.js +130 -0
- package/dist/openclaw.d.ts +5 -0
- package/dist/openclaw.js +349 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +272 -0
- package/dist/summarize.d.ts +19 -0
- package/dist/summarize.js +10 -0
- package/dist/worker.js +137 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +40 -0
- package/scripts/migrate-lance-to-sqlite.mjs +217 -0
- package/skills/memory/SKILL.md +61 -0
- package/src/cc-hook.ts +447 -0
- package/src/cli.ts +356 -0
- package/src/core.ts +1472 -0
- package/src/crypto.ts +113 -0
- package/src/dev-update.ts +178 -0
- package/src/ldm.ts +117 -0
- package/src/mcp-server.ts +274 -0
- package/src/migrate.ts +104 -0
- package/src/mirror-sync.ts +175 -0
- package/src/openclaw.ts +250 -0
- package/src/poller.ts +345 -0
- package/src/summarize.ts +210 -0
- package/src/worker.ts +208 -0
- package/tsconfig.json +18 -0
- package/wrangler.toml +20 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/worker.ts
|
|
2
|
+
function authenticate(request, env) {
|
|
3
|
+
const auth = request.headers.get("Authorization");
|
|
4
|
+
if (!auth?.startsWith("Bearer ")) {
|
|
5
|
+
return json({ error: "Missing Authorization header" }, 401);
|
|
6
|
+
}
|
|
7
|
+
const token = auth.slice(7);
|
|
8
|
+
const tokenMap = {};
|
|
9
|
+
if (env.AUTH_TOKEN_CC_AIR) tokenMap[env.AUTH_TOKEN_CC_AIR] = "cc-air";
|
|
10
|
+
if (env.AUTH_TOKEN_CC_MINI) tokenMap[env.AUTH_TOKEN_CC_MINI] = "cc-mini";
|
|
11
|
+
if (env.AUTH_TOKEN_LESA) tokenMap[env.AUTH_TOKEN_LESA] = "lesa-mini";
|
|
12
|
+
const agentId = tokenMap[token];
|
|
13
|
+
if (!agentId) {
|
|
14
|
+
return json({ error: "Invalid token" }, 403);
|
|
15
|
+
}
|
|
16
|
+
return { agentId };
|
|
17
|
+
}
|
|
18
|
+
var VALID_CHANNELS = ["conversations", "mirror"];
|
|
19
|
+
function isValidChannel(channel) {
|
|
20
|
+
return VALID_CHANNELS.includes(channel);
|
|
21
|
+
}
|
|
22
|
+
async function handleDrop(request, env, agentId, channel) {
|
|
23
|
+
if (!isValidChannel(channel)) {
|
|
24
|
+
return json({ error: `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}` }, 400);
|
|
25
|
+
}
|
|
26
|
+
const body = await request.arrayBuffer();
|
|
27
|
+
if (body.byteLength === 0) {
|
|
28
|
+
return json({ error: "Empty payload" }, 400);
|
|
29
|
+
}
|
|
30
|
+
if (body.byteLength > 100 * 1024 * 1024) {
|
|
31
|
+
return json({ error: "Payload too large (max 100MB)" }, 413);
|
|
32
|
+
}
|
|
33
|
+
const id = crypto.randomUUID();
|
|
34
|
+
const key = `${channel}/${id}`;
|
|
35
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36
|
+
await env.RELAY.put(key, body, {
|
|
37
|
+
customMetadata: {
|
|
38
|
+
agent_id: agentId,
|
|
39
|
+
dropped_at: now,
|
|
40
|
+
size: String(body.byteLength)
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return json({ ok: true, id, channel, size: body.byteLength, dropped_at: now });
|
|
44
|
+
}
|
|
45
|
+
async function handlePickupList(env, channel) {
|
|
46
|
+
if (!isValidChannel(channel)) {
|
|
47
|
+
return json({ error: `Invalid channel: ${channel}` }, 400);
|
|
48
|
+
}
|
|
49
|
+
const listed = await env.RELAY.list({ prefix: `${channel}/` });
|
|
50
|
+
const blobs = listed.objects.map((obj) => ({
|
|
51
|
+
id: obj.key.split("/")[1],
|
|
52
|
+
size: obj.size,
|
|
53
|
+
dropped_at: obj.customMetadata?.dropped_at || obj.uploaded.toISOString(),
|
|
54
|
+
agent_id: obj.customMetadata?.agent_id || "unknown"
|
|
55
|
+
}));
|
|
56
|
+
return json({ channel, count: blobs.length, blobs });
|
|
57
|
+
}
|
|
58
|
+
async function handlePickup(env, channel, id) {
|
|
59
|
+
if (!isValidChannel(channel)) {
|
|
60
|
+
return json({ error: `Invalid channel: ${channel}` }, 400);
|
|
61
|
+
}
|
|
62
|
+
const key = `${channel}/${id}`;
|
|
63
|
+
const obj = await env.RELAY.get(key);
|
|
64
|
+
if (!obj) {
|
|
65
|
+
return json({ error: "Blob not found (already picked up or expired)" }, 404);
|
|
66
|
+
}
|
|
67
|
+
return new Response(obj.body, {
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/octet-stream",
|
|
70
|
+
"X-Agent-Id": obj.customMetadata?.agent_id || "unknown",
|
|
71
|
+
"X-Dropped-At": obj.customMetadata?.dropped_at || ""
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function handleConfirm(env, channel, id) {
|
|
76
|
+
if (!isValidChannel(channel)) {
|
|
77
|
+
return json({ error: `Invalid channel: ${channel}` }, 400);
|
|
78
|
+
}
|
|
79
|
+
const key = `${channel}/${id}`;
|
|
80
|
+
const obj = await env.RELAY.head(key);
|
|
81
|
+
if (!obj) {
|
|
82
|
+
return json({ error: "Blob not found (already confirmed or expired)" }, 404);
|
|
83
|
+
}
|
|
84
|
+
await env.RELAY.delete(key);
|
|
85
|
+
return json({ ok: true, deleted: key });
|
|
86
|
+
}
|
|
87
|
+
function json(data, status = 200) {
|
|
88
|
+
return new Response(JSON.stringify(data), {
|
|
89
|
+
status,
|
|
90
|
+
headers: { "Content-Type": "application/json" }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
var worker_default = {
|
|
94
|
+
async fetch(request, env) {
|
|
95
|
+
const url = new URL(request.url);
|
|
96
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
97
|
+
if (parts[0] === "health" && request.method === "GET") {
|
|
98
|
+
return json({ ok: true, service: "memory-crystal-relay", mode: "ephemeral" });
|
|
99
|
+
}
|
|
100
|
+
const authResult = authenticate(request, env);
|
|
101
|
+
if (authResult instanceof Response) return authResult;
|
|
102
|
+
const { agentId } = authResult;
|
|
103
|
+
try {
|
|
104
|
+
if (parts[0] === "drop" && parts[1] && request.method === "POST") {
|
|
105
|
+
return handleDrop(request, env, agentId, parts[1]);
|
|
106
|
+
}
|
|
107
|
+
if (parts[0] === "pickup" && parts[1] && request.method === "GET") {
|
|
108
|
+
if (parts[2]) {
|
|
109
|
+
return handlePickup(env, parts[1], parts[2]);
|
|
110
|
+
}
|
|
111
|
+
return handlePickupList(env, parts[1]);
|
|
112
|
+
}
|
|
113
|
+
if (parts[0] === "confirm" && parts[1] && parts[2] && request.method === "DELETE") {
|
|
114
|
+
return handleConfirm(env, parts[1], parts[2]);
|
|
115
|
+
}
|
|
116
|
+
return json({ error: "Not found" }, 404);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return json({ error: err.message || "Internal error" }, 500);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
// Scheduled cleanup: delete blobs older than 24h (TTL safety net)
|
|
122
|
+
async scheduled(event, env) {
|
|
123
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
124
|
+
for (const channel of VALID_CHANNELS) {
|
|
125
|
+
const listed = await env.RELAY.list({ prefix: `${channel}/` });
|
|
126
|
+
for (const obj of listed.objects) {
|
|
127
|
+
const droppedAt = obj.customMetadata?.dropped_at;
|
|
128
|
+
if (droppedAt && new Date(droppedAt).getTime() < cutoff) {
|
|
129
|
+
await env.RELAY.delete(obj.key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export {
|
|
136
|
+
worker_default as default
|
|
137
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "memory-crystal",
|
|
3
|
+
"name": "Memory Crystal",
|
|
4
|
+
"description": "Sovereign memory system — search, remember, forget across all agent conversations and files.",
|
|
5
|
+
"skills": ["./skills"],
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {}
|
|
10
|
+
}
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memory-crystal",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Sovereign memory system — local-first with ephemeral encrypted relay. Your memory, your machine, your rules.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/core.js",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./dist/openclaw.js"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"crystal": "./dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/core.ts src/cli.ts src/mcp-server.ts src/openclaw.ts src/migrate.ts src/cc-hook.ts src/crypto.ts src/poller.ts src/mirror-sync.ts src/ldm.ts src/summarize.ts --format esm --dts --outDir dist && tsup src/worker.ts --format esm --outDir dist --no-dts",
|
|
17
|
+
"build:local": "tsup src/core.ts src/cli.ts src/mcp-server.ts src/openclaw.ts src/migrate.ts src/cc-hook.ts src/crypto.ts src/poller.ts src/mirror-sync.ts src/ldm.ts src/summarize.ts --format esm --dts --outDir dist",
|
|
18
|
+
"build:worker": "tsup src/worker.ts --format esm --outDir dist --no-dts",
|
|
19
|
+
"dev": "tsup src/core.ts src/cli.ts src/mcp-server.ts src/openclaw.ts src/migrate.ts src/cc-hook.ts src/crypto.ts --format esm --watch --outDir dist",
|
|
20
|
+
"check": "node dist/cli.js status",
|
|
21
|
+
"search": "node dist/cli.js search",
|
|
22
|
+
"migrate": "node dist/migrate.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@lancedb/lancedb": "^0.15.0",
|
|
26
|
+
"better-sqlite3": "^11.8.1",
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
28
|
+
"apache-arrow": "^18.1.0",
|
|
29
|
+
"sqlite-vec": "^0.1.7-alpha.2"
|
|
30
|
+
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"sqlite-vec-darwin-arm64": "^0.1.7-alpha.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.7.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// migrate-lance-to-sqlite.mjs — Copy all chunks + vectors from LanceDB to sqlite-vec.
|
|
3
|
+
// Reads vectors directly from LanceDB (no re-embedding needed).
|
|
4
|
+
// Deduplicates by SHA-256 hash of text content.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node scripts/migrate-lance-to-sqlite.mjs [--dry-run] [--batch-size N]
|
|
8
|
+
//
|
|
9
|
+
// Data dir: ~/.openclaw/memory-crystal/
|
|
10
|
+
|
|
11
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import * as sqliteVec from 'sqlite-vec';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const BATCH_SIZE = 500;
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const dryRun = args.includes('--dry-run');
|
|
23
|
+
const batchSizeArg = args.find((_, i) => args[i - 1] === '--batch-size');
|
|
24
|
+
const batchSize = batchSizeArg ? parseInt(batchSizeArg) : BATCH_SIZE;
|
|
25
|
+
|
|
26
|
+
const openclawHome = process.env.OPENCLAW_HOME || join(process.env.HOME || '/Users/lesa', '.openclaw');
|
|
27
|
+
const dataDir = join(openclawHome, 'memory-crystal');
|
|
28
|
+
const lanceDir = join(dataDir, 'lance');
|
|
29
|
+
const sqlitePath = join(dataDir, 'crystal.db');
|
|
30
|
+
|
|
31
|
+
if (!existsSync(lanceDir)) {
|
|
32
|
+
console.error(`LanceDB directory not found: ${lanceDir}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Open LanceDB
|
|
37
|
+
const lanceDb = await lancedb.connect(lanceDir);
|
|
38
|
+
const tableNames = await lanceDb.tableNames();
|
|
39
|
+
if (!tableNames.includes('chunks')) {
|
|
40
|
+
console.error('No "chunks" table in LanceDB');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const lanceTable = await lanceDb.openTable('chunks');
|
|
44
|
+
const totalLance = await lanceTable.countRows();
|
|
45
|
+
console.log(`LanceDB chunks: ${totalLance.toLocaleString()}`);
|
|
46
|
+
|
|
47
|
+
// Open SQLite + load sqlite-vec
|
|
48
|
+
const db = new Database(sqlitePath);
|
|
49
|
+
db.pragma('journal_mode = WAL');
|
|
50
|
+
sqliteVec.load(db);
|
|
51
|
+
|
|
52
|
+
// Ensure tables exist
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
text TEXT NOT NULL,
|
|
57
|
+
text_hash TEXT NOT NULL,
|
|
58
|
+
role TEXT,
|
|
59
|
+
source_type TEXT,
|
|
60
|
+
source_id TEXT,
|
|
61
|
+
agent_id TEXT,
|
|
62
|
+
token_count INTEGER,
|
|
63
|
+
created_at TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_agent ON chunks(agent_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source_type);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(text_hash);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_created ON chunks(created_at);
|
|
69
|
+
|
|
70
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
71
|
+
text,
|
|
72
|
+
tokenize='porter unicode61'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TRIGGER IF NOT EXISTS chunks_fts_insert AFTER INSERT ON chunks
|
|
76
|
+
BEGIN
|
|
77
|
+
INSERT INTO chunks_fts(rowid, text) VALUES (NEW.id, NEW.text);
|
|
78
|
+
END;
|
|
79
|
+
`);
|
|
80
|
+
|
|
81
|
+
const existingSqlite = (db.prepare('SELECT COUNT(*) as count FROM chunks').get()).count;
|
|
82
|
+
console.log(`SQLite chunks (before): ${existingSqlite.toLocaleString()}`);
|
|
83
|
+
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
// Sample some rows
|
|
86
|
+
const sample = await lanceTable.query().limit(3).toArray();
|
|
87
|
+
console.log('\nSample (3 rows):');
|
|
88
|
+
for (const row of sample) {
|
|
89
|
+
console.log(` [${row.source_type}] [${row.agent_id}] ${row.text?.slice(0, 80)}...`);
|
|
90
|
+
console.log(` vector: ${row.vector?.length} dims, created: ${row.created_at}`);
|
|
91
|
+
}
|
|
92
|
+
console.log(`\nWould migrate ${totalLance.toLocaleString()} chunks.`);
|
|
93
|
+
console.log(`Estimated crystal.db growth: ~${Math.round(totalLance * 1536 * 4 / 1024 / 1024)}MB vectors + text`);
|
|
94
|
+
db.close();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Detect dimensions from first row
|
|
99
|
+
const [firstRow] = await lanceTable.query().limit(1).toArray();
|
|
100
|
+
const dimensions = firstRow.vector?.length;
|
|
101
|
+
if (!dimensions) {
|
|
102
|
+
console.error('Could not determine vector dimensions from LanceDB');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
console.log(`Vector dimensions: ${dimensions}`);
|
|
106
|
+
|
|
107
|
+
// Create vec table if needed
|
|
108
|
+
const vecExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks_vec'`).get();
|
|
109
|
+
if (!vecExists) {
|
|
110
|
+
db.exec(`CREATE VIRTUAL TABLE chunks_vec USING vec0(
|
|
111
|
+
chunk_id INTEGER PRIMARY KEY,
|
|
112
|
+
embedding float[${dimensions}] distance_metric=cosine
|
|
113
|
+
)`);
|
|
114
|
+
console.log(`Created chunks_vec table (${dimensions} dims)`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build hash set of existing chunks for dedup
|
|
118
|
+
console.log('Building dedup hash set...');
|
|
119
|
+
const existingHashes = new Set();
|
|
120
|
+
const hashRows = db.prepare('SELECT text_hash FROM chunks').all();
|
|
121
|
+
for (const row of hashRows) {
|
|
122
|
+
existingHashes.add(row.text_hash);
|
|
123
|
+
}
|
|
124
|
+
console.log(`Existing unique hashes: ${existingHashes.size.toLocaleString()}`);
|
|
125
|
+
|
|
126
|
+
// Prepare insert statements
|
|
127
|
+
const insertChunk = db.prepare(`
|
|
128
|
+
INSERT INTO chunks (text, text_hash, role, source_type, source_id, agent_id, token_count, created_at)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
130
|
+
`);
|
|
131
|
+
const insertVec = db.prepare(`
|
|
132
|
+
INSERT INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
// Read all rows from LanceDB in batches using offset/limit
|
|
136
|
+
let migrated = 0;
|
|
137
|
+
let skippedDedup = 0;
|
|
138
|
+
let offset = 0;
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
|
|
141
|
+
while (offset < totalLance) {
|
|
142
|
+
const rows = await lanceTable.query().limit(batchSize).offset(offset).toArray();
|
|
143
|
+
if (rows.length === 0) break;
|
|
144
|
+
|
|
145
|
+
const transaction = db.transaction(() => {
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
const text = row.text || '';
|
|
148
|
+
const hash = createHash('sha256').update(text).digest('hex');
|
|
149
|
+
|
|
150
|
+
if (existingHashes.has(hash)) {
|
|
151
|
+
skippedDedup++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
existingHashes.add(hash);
|
|
155
|
+
|
|
156
|
+
const result = insertChunk.run(
|
|
157
|
+
text,
|
|
158
|
+
hash,
|
|
159
|
+
row.role || null,
|
|
160
|
+
row.source_type || null,
|
|
161
|
+
row.source_id || null,
|
|
162
|
+
row.agent_id || null,
|
|
163
|
+
row.token_count || Math.ceil(text.length / 4),
|
|
164
|
+
row.created_at || new Date().toISOString()
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// sqlite-vec needs BigInt for integer primary keys
|
|
168
|
+
const chunkId = typeof result.lastInsertRowid === 'bigint'
|
|
169
|
+
? result.lastInsertRowid
|
|
170
|
+
: BigInt(result.lastInsertRowid);
|
|
171
|
+
|
|
172
|
+
// Convert vector to Float32Array
|
|
173
|
+
const vector = row.vector;
|
|
174
|
+
const f32 = vector instanceof Float32Array ? vector : new Float32Array(Array.from(vector));
|
|
175
|
+
insertVec.run(chunkId, f32);
|
|
176
|
+
|
|
177
|
+
migrated++;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
transaction();
|
|
181
|
+
|
|
182
|
+
offset += rows.length;
|
|
183
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
184
|
+
const rate = Math.round(offset / elapsed);
|
|
185
|
+
const eta = Math.round((totalLance - offset) / rate);
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
`\r ${offset.toLocaleString()}/${totalLance.toLocaleString()} (${Math.round(offset / totalLance * 100)}%) ` +
|
|
188
|
+
`| migrated: ${migrated.toLocaleString()} | dedup: ${skippedDedup.toLocaleString()} ` +
|
|
189
|
+
`| ${rate}/s | ETA: ${eta}s `
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
194
|
+
console.log(`\n\nMigration complete in ${elapsed}s:`);
|
|
195
|
+
console.log(` Migrated: ${migrated.toLocaleString()}`);
|
|
196
|
+
console.log(` Dedup skip: ${skippedDedup.toLocaleString()}`);
|
|
197
|
+
|
|
198
|
+
// Verify
|
|
199
|
+
const finalCount = (db.prepare('SELECT COUNT(*) as count FROM chunks').get()).count;
|
|
200
|
+
const ftsCount = (db.prepare('SELECT COUNT(*) as count FROM chunks_fts').get()).count;
|
|
201
|
+
console.log(` SQLite chunks: ${finalCount.toLocaleString()}`);
|
|
202
|
+
console.log(` FTS entries: ${ftsCount.toLocaleString()}`);
|
|
203
|
+
console.log(` LanceDB: ${totalLance.toLocaleString()}`);
|
|
204
|
+
|
|
205
|
+
if (finalCount === ftsCount) {
|
|
206
|
+
console.log(' FTS sync: OK');
|
|
207
|
+
} else {
|
|
208
|
+
console.warn(` WARNING: FTS count mismatch (${ftsCount} vs ${finalCount})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
db.close();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
main().catch(err => {
|
|
215
|
+
console.error(`Migration failed: ${err.message}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory
|
|
3
|
+
description: Search and manage the shared memory crystal. Use when user says "do you remember", "search memory", "remember this", "forget that", "memory status", "what do you know about", or needs to recall past discussions, store facts, or check what's in memory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory Crystal Skill
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
- Searching for past conversations, decisions, or context
|
|
10
|
+
- Storing important facts, preferences, or observations that should persist
|
|
11
|
+
- Checking what's in memory and how much is stored
|
|
12
|
+
|
|
13
|
+
## Tools
|
|
14
|
+
|
|
15
|
+
### crystal_search
|
|
16
|
+
Search across all stored memory — conversations, files, manually stored facts.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
crystal_search query="OpenAI API key configuration" limit=5
|
|
20
|
+
crystal_search query="Parker's preferences" agent_id="main"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### crystal_remember
|
|
24
|
+
Store a fact or observation that should persist across sessions.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
crystal_remember text="Parker prefers Opus for complex tasks, Sonnet for quick ones" category="preference"
|
|
28
|
+
crystal_remember text="Gateway auth token is required since v2026.2.2" category="fact"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Categories: fact, preference, event, opinion, skill
|
|
32
|
+
|
|
33
|
+
### crystal_forget
|
|
34
|
+
Deprecate a stored memory by ID (doesn't delete — marks as deprecated).
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
crystal_forget id=42
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### crystal_status
|
|
41
|
+
Check memory crystal health — chunk count, agents, provider.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
crystal_status
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CLI (via bash)
|
|
48
|
+
```bash
|
|
49
|
+
crystal search "query"
|
|
50
|
+
crystal search "query" -n 10 --agent main
|
|
51
|
+
crystal remember "fact to store" --category preference
|
|
52
|
+
crystal forget 42
|
|
53
|
+
crystal status
|
|
54
|
+
crystal status --provider ollama # check with different provider
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tips
|
|
58
|
+
- Search is semantic — "how do plugins work" will find conversations about plugin architecture even if those exact words weren't used
|
|
59
|
+
- Store preferences and decisions as memories — they survive compaction
|
|
60
|
+
- Use agent_id filter when you only want results from a specific agent
|
|
61
|
+
- Available providers: openai (default), ollama (local, free), google
|