@a13xu/lucid 1.0.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/LICENSE +21 -0
- package/README.md +99 -0
- package/build/database.d.ts +34 -0
- package/build/database.js +114 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +154 -0
- package/build/tools/forget.d.ts +11 -0
- package/build/tools/forget.js +13 -0
- package/build/tools/recall-all.d.ts +3 -0
- package/build/tools/recall-all.js +45 -0
- package/build/tools/recall.d.ts +11 -0
- package/build/tools/recall.js +53 -0
- package/build/tools/relate.d.ts +17 -0
- package/build/tools/relate.js +21 -0
- package/build/tools/remember.d.ts +17 -0
- package/build/tools/remember.js +26 -0
- package/build/tools/stats.d.ts +3 -0
- package/build/tools/stats.js +30 -0
- package/build/types.d.ts +53 -0
- package/build/types.js +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 a13xu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @a13xu/lucid
|
|
2
|
+
|
|
3
|
+
Persistent memory for Claude Code agents, backed by **SQLite + FTS5**.
|
|
4
|
+
|
|
5
|
+
Stores a knowledge graph (entities, relations, observations) with full-text search — indexed queries under 1ms, no JSON files, no linear scans.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
**Requirements:** Node.js 18+
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Run directly (no install needed)
|
|
13
|
+
npx @a13xu/lucid
|
|
14
|
+
|
|
15
|
+
# Or install globally
|
|
16
|
+
npm install -g @a13xu/lucid
|
|
17
|
+
lucid
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Add to Claude Code
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
claude mcp add --transport stdio lucid -- npx -y @a13xu/lucid
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add manually to `.mcp.json` in your project root:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"lucid": {
|
|
32
|
+
"type": "stdio",
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "@a13xu/lucid"],
|
|
35
|
+
"env": {
|
|
36
|
+
"MEMORY_DB_PATH": "/your/project/.claude/memory.db"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Default DB path: `~/.claude/memory.db`
|
|
44
|
+
|
|
45
|
+
## Why SQLite + FTS5 instead of JSON?
|
|
46
|
+
|
|
47
|
+
| | JSON file | SQLite + FTS5 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Search | O(n) linear scan | O(log n) indexed |
|
|
50
|
+
| Write | Rewrite entire file | Atomic incremental |
|
|
51
|
+
| Concurrent reads | Lock entire file | WAL mode |
|
|
52
|
+
| Stemming / unicode | Manual | Built-in |
|
|
53
|
+
|
|
54
|
+
## Tools (6)
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `remember` | Store a fact about an entity (project, person, tool, decision…) |
|
|
59
|
+
| `relate` | Create a directed relationship between two entities |
|
|
60
|
+
| `recall` | Full-text search across all memory (FTS5 + LIKE fallback) |
|
|
61
|
+
| `recall_all` | Return the entire knowledge graph with stats |
|
|
62
|
+
| `forget` | Remove an entity and all its relations |
|
|
63
|
+
| `memory_stats` | DB size, WAL status, entity/relation counts |
|
|
64
|
+
|
|
65
|
+
### Entity types
|
|
66
|
+
`person` · `project` · `decision` · `pattern` · `tool` · `config` · `bug` · `convention`
|
|
67
|
+
|
|
68
|
+
### Relation types
|
|
69
|
+
`uses` · `depends_on` · `created_by` · `part_of` · `replaced_by` · `conflicts_with` · `tested_by`
|
|
70
|
+
|
|
71
|
+
## Usage examples
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
You: "Remember that this project uses PostgreSQL with Prisma ORM"
|
|
75
|
+
Claude: [calls remember] → Created "PostgreSQL" [tool]
|
|
76
|
+
|
|
77
|
+
You: "What do you know about the database?"
|
|
78
|
+
Claude: [calls recall("database PostgreSQL")] → returns entity + observations
|
|
79
|
+
|
|
80
|
+
You: "How are the services connected?"
|
|
81
|
+
Claude: [calls recall_all] → returns full knowledge graph
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Debugging
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"test","version":"1.0"},"protocolVersion":"2024-11-05"}}' \
|
|
88
|
+
| npx @a13xu/lucid
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
In Claude Code: run `/mcp` — you should see `lucid` listed with 6 tools.
|
|
92
|
+
|
|
93
|
+
## Tech stack
|
|
94
|
+
|
|
95
|
+
- **Runtime:** Node.js 18+, TypeScript, ES modules
|
|
96
|
+
- **MCP SDK:** `@modelcontextprotocol/sdk`
|
|
97
|
+
- **Database:** `better-sqlite3` (synchronous, no async overhead)
|
|
98
|
+
- **Validation:** `zod`
|
|
99
|
+
- **Transport:** stdio
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { EntityRow, RelationRow } from "./types.js";
|
|
3
|
+
export declare function initDatabase(): Database.Database;
|
|
4
|
+
type Stmt<P extends unknown[], R> = Database.Statement<P, R>;
|
|
5
|
+
type WriteStmt<P extends unknown[]> = Database.Statement<P, unknown>;
|
|
6
|
+
export interface Statements {
|
|
7
|
+
getEntityByName: Stmt<[string], EntityRow>;
|
|
8
|
+
insertEntity: WriteStmt<[string, string, string]>;
|
|
9
|
+
updateEntity: WriteStmt<[string, number]>;
|
|
10
|
+
deleteEntity: WriteStmt<[string]>;
|
|
11
|
+
getAllEntities: Stmt<[], EntityRow>;
|
|
12
|
+
getRelationsForEntity: Stmt<[number, number], RelationRow & {
|
|
13
|
+
from_name: string;
|
|
14
|
+
to_name: string;
|
|
15
|
+
}>;
|
|
16
|
+
insertRelation: WriteStmt<[number, number, string]>;
|
|
17
|
+
searchFTS: Stmt<[string], EntityRow>;
|
|
18
|
+
searchLike: Stmt<[string, string, string], EntityRow>;
|
|
19
|
+
countEntities: Stmt<[], {
|
|
20
|
+
count: number;
|
|
21
|
+
}>;
|
|
22
|
+
countRelations: Stmt<[], {
|
|
23
|
+
count: number;
|
|
24
|
+
}>;
|
|
25
|
+
getWalMode: Stmt<[], {
|
|
26
|
+
journal_mode: string;
|
|
27
|
+
}>;
|
|
28
|
+
getAllRelations: Stmt<[], RelationRow & {
|
|
29
|
+
from_name: string;
|
|
30
|
+
to_name: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export declare function prepareStatements(db: Database.Database): Statements;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// DB path
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function resolveDbPath() {
|
|
9
|
+
const envPath = process.env["MEMORY_DB_PATH"];
|
|
10
|
+
if (envPath)
|
|
11
|
+
return envPath;
|
|
12
|
+
return join(homedir(), ".claude", "memory.db");
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Init
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
export function initDatabase() {
|
|
18
|
+
const dbPath = resolveDbPath();
|
|
19
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
20
|
+
const db = new Database(dbPath);
|
|
21
|
+
// Pragmas obligatorii
|
|
22
|
+
db.pragma("journal_mode = WAL");
|
|
23
|
+
db.pragma("synchronous = NORMAL");
|
|
24
|
+
db.pragma("cache_size = -8000");
|
|
25
|
+
db.pragma("temp_store = MEMORY");
|
|
26
|
+
db.pragma("mmap_size = 67108864");
|
|
27
|
+
db.pragma("foreign_keys = ON");
|
|
28
|
+
createSchema(db);
|
|
29
|
+
console.error(`[lucid] DB: ${dbPath}`);
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
function createSchema(db) {
|
|
33
|
+
db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
35
|
+
id INTEGER PRIMARY KEY,
|
|
36
|
+
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
37
|
+
type TEXT NOT NULL,
|
|
38
|
+
observations TEXT NOT NULL DEFAULT '[]',
|
|
39
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
40
|
+
updated_at INTEGER DEFAULT (unixepoch())
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
44
|
+
id INTEGER PRIMARY KEY,
|
|
45
|
+
from_entity INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
46
|
+
to_entity INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
47
|
+
relation_type TEXT NOT NULL,
|
|
48
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
49
|
+
UNIQUE(from_entity, to_entity, relation_type)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
53
|
+
name,
|
|
54
|
+
type,
|
|
55
|
+
observations,
|
|
56
|
+
content='entities',
|
|
57
|
+
content_rowid='id',
|
|
58
|
+
tokenize='porter unicode61'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
|
|
62
|
+
INSERT INTO entities_fts(rowid, name, type, observations)
|
|
63
|
+
VALUES (new.id, new.name, new.type, new.observations);
|
|
64
|
+
END;
|
|
65
|
+
|
|
66
|
+
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
|
|
67
|
+
INSERT INTO entities_fts(entities_fts, rowid, name, type, observations)
|
|
68
|
+
VALUES('delete', old.id, old.name, old.type, old.observations);
|
|
69
|
+
INSERT INTO entities_fts(rowid, name, type, observations)
|
|
70
|
+
VALUES (new.id, new.name, new.type, new.observations);
|
|
71
|
+
END;
|
|
72
|
+
|
|
73
|
+
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
|
|
74
|
+
INSERT INTO entities_fts(entities_fts, rowid, name, type, observations)
|
|
75
|
+
VALUES('delete', old.id, old.name, old.type, old.observations);
|
|
76
|
+
END;
|
|
77
|
+
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
export function prepareStatements(db) {
|
|
84
|
+
return {
|
|
85
|
+
getEntityByName: db.prepare("SELECT * FROM entities WHERE name = ? COLLATE NOCASE"),
|
|
86
|
+
insertEntity: db.prepare("INSERT INTO entities (name, type, observations) VALUES (?, ?, ?)"),
|
|
87
|
+
// SQL: SET observations = ?, WHERE id = ? → 2 params
|
|
88
|
+
updateEntity: db.prepare("UPDATE entities SET observations = ?, updated_at = unixepoch() WHERE id = ?"),
|
|
89
|
+
deleteEntity: db.prepare("DELETE FROM entities WHERE name = ? COLLATE NOCASE"),
|
|
90
|
+
getAllEntities: db.prepare("SELECT * FROM entities ORDER BY updated_at DESC"),
|
|
91
|
+
getRelationsForEntity: db.prepare(`SELECT r.*, ef.name AS from_name, et.name AS to_name
|
|
92
|
+
FROM relations r
|
|
93
|
+
JOIN entities ef ON r.from_entity = ef.id
|
|
94
|
+
JOIN entities et ON r.to_entity = et.id
|
|
95
|
+
WHERE r.from_entity = ? OR r.to_entity = ?`),
|
|
96
|
+
insertRelation: db.prepare("INSERT OR IGNORE INTO relations (from_entity, to_entity, relation_type) VALUES (?, ?, ?)"),
|
|
97
|
+
searchFTS: db.prepare(`SELECT e.* FROM entities_fts
|
|
98
|
+
JOIN entities e ON entities_fts.rowid = e.id
|
|
99
|
+
WHERE entities_fts MATCH ?
|
|
100
|
+
ORDER BY rank
|
|
101
|
+
LIMIT 20`),
|
|
102
|
+
searchLike: db.prepare(`SELECT * FROM entities
|
|
103
|
+
WHERE name LIKE ? OR type LIKE ? OR observations LIKE ?
|
|
104
|
+
ORDER BY updated_at DESC
|
|
105
|
+
LIMIT 20`),
|
|
106
|
+
countEntities: db.prepare("SELECT COUNT(*) as count FROM entities"),
|
|
107
|
+
countRelations: db.prepare("SELECT COUNT(*) as count FROM relations"),
|
|
108
|
+
getWalMode: db.prepare("PRAGMA journal_mode"),
|
|
109
|
+
getAllRelations: db.prepare(`SELECT r.*, ef.name AS from_name, et.name AS to_name
|
|
110
|
+
FROM relations r
|
|
111
|
+
JOIN entities ef ON r.from_entity = ef.id
|
|
112
|
+
JOIN entities et ON r.to_entity = et.id`),
|
|
113
|
+
};
|
|
114
|
+
}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { initDatabase, prepareStatements } from "./database.js";
|
|
7
|
+
import { remember, RememberSchema } from "./tools/remember.js";
|
|
8
|
+
import { relate, RelateSchema } from "./tools/relate.js";
|
|
9
|
+
import { recall, RecallSchema } from "./tools/recall.js";
|
|
10
|
+
import { recallAll } from "./tools/recall-all.js";
|
|
11
|
+
import { forget, ForgetSchema } from "./tools/forget.js";
|
|
12
|
+
import { memoryStats } from "./tools/stats.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Init DB
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const db = initDatabase();
|
|
17
|
+
const stmts = prepareStatements(db);
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// MCP Server
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const server = new Server({ name: "lucid", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Tool definitions
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
26
|
+
tools: [
|
|
27
|
+
{
|
|
28
|
+
name: "remember",
|
|
29
|
+
description: "Store a fact, decision, or observation about an entity in the knowledge graph.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
entity: { type: "string", description: "Entity name (project, person, concept, tool)" },
|
|
34
|
+
entityType: {
|
|
35
|
+
type: "string",
|
|
36
|
+
enum: ["person", "project", "decision", "pattern", "tool", "config", "bug", "convention"],
|
|
37
|
+
},
|
|
38
|
+
observation: { type: "string", description: "The fact to remember" },
|
|
39
|
+
},
|
|
40
|
+
required: ["entity", "entityType", "observation"],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "relate",
|
|
45
|
+
description: "Create a directed relationship between two entities in the knowledge graph.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
from: { type: "string", description: "Source entity name" },
|
|
50
|
+
to: { type: "string", description: "Target entity name" },
|
|
51
|
+
relationType: {
|
|
52
|
+
type: "string",
|
|
53
|
+
enum: ["uses", "depends_on", "created_by", "part_of", "replaced_by", "conflicts_with", "tested_by"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["from", "to", "relationType"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "recall",
|
|
61
|
+
description: "Search memory using full-text search. Fast, indexed, supports partial matches and stemming.",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
query: { type: "string", description: "Search terms" },
|
|
66
|
+
},
|
|
67
|
+
required: ["query"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "recall_all",
|
|
72
|
+
description: "Get the entire knowledge graph with statistics.",
|
|
73
|
+
inputSchema: { type: "object", properties: {} },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "forget",
|
|
77
|
+
description: "Remove an entity and all its relations from memory.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
entity: { type: "string", description: "Entity name to remove" },
|
|
82
|
+
},
|
|
83
|
+
required: ["entity"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "memory_stats",
|
|
88
|
+
description: "Get memory usage statistics.",
|
|
89
|
+
inputSchema: { type: "object", properties: {} },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}));
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Tool handlers
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
97
|
+
const { name, arguments: args } = request.params;
|
|
98
|
+
try {
|
|
99
|
+
let text;
|
|
100
|
+
switch (name) {
|
|
101
|
+
case "remember": {
|
|
102
|
+
const input = RememberSchema.parse(args);
|
|
103
|
+
text = remember(stmts, input);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "relate": {
|
|
107
|
+
const input = RelateSchema.parse(args);
|
|
108
|
+
text = relate(stmts, input);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "recall": {
|
|
112
|
+
const input = RecallSchema.parse(args);
|
|
113
|
+
text = recall(stmts, input);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "recall_all": {
|
|
117
|
+
text = recallAll(db, stmts);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "forget": {
|
|
121
|
+
const input = ForgetSchema.parse(args);
|
|
122
|
+
text = forget(stmts, input);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "memory_stats": {
|
|
126
|
+
text = memoryStats(db, stmts);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { content: [{ type: "text", text }] };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const message = err instanceof z.ZodError
|
|
139
|
+
? `Validation error: ${err.errors.map((e) => e.message).join(", ")}`
|
|
140
|
+
: err instanceof Error
|
|
141
|
+
? err.message
|
|
142
|
+
: String(err);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
145
|
+
isError: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Start
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
const transport = new StdioServerTransport();
|
|
153
|
+
await server.connect(transport);
|
|
154
|
+
console.error("[lucid] Server started on stdio.");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const ForgetSchema: z.ZodObject<{
|
|
4
|
+
entity: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
entity: string;
|
|
7
|
+
}, {
|
|
8
|
+
entity: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ForgetInput = z.infer<typeof ForgetSchema>;
|
|
11
|
+
export declare function forget(stmts: Statements, input: ForgetInput): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const ForgetSchema = z.object({
|
|
3
|
+
entity: z.string().min(1),
|
|
4
|
+
});
|
|
5
|
+
export function forget(stmts, input) {
|
|
6
|
+
const existing = stmts.getEntityByName.get(input.entity);
|
|
7
|
+
if (!existing) {
|
|
8
|
+
return `Entity "${input.entity}" not found in memory.`;
|
|
9
|
+
}
|
|
10
|
+
// ON DELETE CASCADE șterge relațiile automat
|
|
11
|
+
stmts.deleteEntity.run(input.entity);
|
|
12
|
+
return `Removed "${input.entity}" and all its relations from memory.`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { statSync } from "fs";
|
|
2
|
+
export function recallAll(db, stmts) {
|
|
3
|
+
const entityCount = stmts.countEntities.get().count;
|
|
4
|
+
const relationCount = stmts.countRelations.get().count;
|
|
5
|
+
const allEntities = stmts.getAllEntities.all();
|
|
6
|
+
const allRelations = stmts.getAllRelations.all();
|
|
7
|
+
// Numără toate observațiile
|
|
8
|
+
let observationCount = 0;
|
|
9
|
+
const entities = allEntities.map((row) => {
|
|
10
|
+
const observations = JSON.parse(row.observations);
|
|
11
|
+
observationCount += observations.length;
|
|
12
|
+
const relations = allRelations
|
|
13
|
+
.filter((r) => r.from_entity === row.id || r.to_entity === row.id)
|
|
14
|
+
.map((r) => ({ from: r.from_name, to: r.to_name, type: r.relation_type }));
|
|
15
|
+
return {
|
|
16
|
+
id: row.id,
|
|
17
|
+
name: row.name,
|
|
18
|
+
type: row.type,
|
|
19
|
+
observations,
|
|
20
|
+
created_at: row.created_at,
|
|
21
|
+
updated_at: row.updated_at,
|
|
22
|
+
relations,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
let dbSizeBytes = 0;
|
|
26
|
+
try {
|
|
27
|
+
dbSizeBytes = statSync(db.name).size;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// fișierul poate fi in-memory sau inaccesibil
|
|
31
|
+
}
|
|
32
|
+
const graph = {
|
|
33
|
+
stats: {
|
|
34
|
+
entity_count: entityCount,
|
|
35
|
+
relation_count: relationCount,
|
|
36
|
+
observation_count: observationCount,
|
|
37
|
+
db_size_bytes: dbSizeBytes,
|
|
38
|
+
db_size_kb: Math.round(dbSizeBytes / 1024),
|
|
39
|
+
wal_mode: true,
|
|
40
|
+
fts5_enabled: true,
|
|
41
|
+
},
|
|
42
|
+
entities,
|
|
43
|
+
};
|
|
44
|
+
return JSON.stringify(graph, null, 2);
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const RecallSchema: z.ZodObject<{
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
query: string;
|
|
7
|
+
}, {
|
|
8
|
+
query: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type RecallInput = z.infer<typeof RecallSchema>;
|
|
11
|
+
export declare function recall(stmts: Statements, input: RecallInput): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RecallSchema = z.object({
|
|
3
|
+
query: z.string().min(1),
|
|
4
|
+
});
|
|
5
|
+
function sanitizeFTSQuery(query) {
|
|
6
|
+
return query
|
|
7
|
+
.replace(/[^\w\s\u00C0-\u024F]/g, "") // păstrează litere + diacritice
|
|
8
|
+
.trim()
|
|
9
|
+
.split(/\s+/)
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join(" OR ");
|
|
12
|
+
}
|
|
13
|
+
function buildEntityWithRelations(stmts, row) {
|
|
14
|
+
const observations = JSON.parse(row.observations);
|
|
15
|
+
const relRows = stmts.getRelationsForEntity.all(row.id, row.id);
|
|
16
|
+
const relations = relRows.map((r) => ({
|
|
17
|
+
from: r.from_name,
|
|
18
|
+
to: r.to_name,
|
|
19
|
+
type: r.relation_type,
|
|
20
|
+
}));
|
|
21
|
+
return {
|
|
22
|
+
id: row.id,
|
|
23
|
+
name: row.name,
|
|
24
|
+
type: row.type,
|
|
25
|
+
observations,
|
|
26
|
+
created_at: row.created_at,
|
|
27
|
+
updated_at: row.updated_at,
|
|
28
|
+
relations,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function recall(stmts, input) {
|
|
32
|
+
const sanitized = sanitizeFTSQuery(input.query);
|
|
33
|
+
let rows = [];
|
|
34
|
+
if (sanitized) {
|
|
35
|
+
try {
|
|
36
|
+
rows = stmts.searchFTS.all(sanitized);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// FTS query invalid — fallback la LIKE
|
|
40
|
+
console.error(`[lucid] FTS fallback for query: ${sanitized}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Fallback LIKE dacă FTS nu a returnat rezultate
|
|
44
|
+
if (rows.length === 0) {
|
|
45
|
+
const like = `%${input.query}%`;
|
|
46
|
+
rows = stmts.searchLike.all(like, like, like);
|
|
47
|
+
}
|
|
48
|
+
if (rows.length === 0) {
|
|
49
|
+
return `No results found for "${input.query}".`;
|
|
50
|
+
}
|
|
51
|
+
const entities = rows.map((row) => buildEntityWithRelations(stmts, row));
|
|
52
|
+
return JSON.stringify(entities, null, 2);
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const RelateSchema: z.ZodObject<{
|
|
4
|
+
from: z.ZodString;
|
|
5
|
+
to: z.ZodString;
|
|
6
|
+
relationType: z.ZodEnum<["uses", "depends_on", "created_by", "part_of", "replaced_by", "conflicts_with", "tested_by"]>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
relationType: "uses" | "depends_on" | "created_by" | "part_of" | "replaced_by" | "conflicts_with" | "tested_by";
|
|
11
|
+
}, {
|
|
12
|
+
from: string;
|
|
13
|
+
to: string;
|
|
14
|
+
relationType: "uses" | "depends_on" | "created_by" | "part_of" | "replaced_by" | "conflicts_with" | "tested_by";
|
|
15
|
+
}>;
|
|
16
|
+
export type RelateInput = z.infer<typeof RelateSchema>;
|
|
17
|
+
export declare function relate(stmts: Statements, input: RelateInput): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RelateSchema = z.object({
|
|
3
|
+
from: z.string().min(1),
|
|
4
|
+
to: z.string().min(1),
|
|
5
|
+
relationType: z.enum([
|
|
6
|
+
"uses", "depends_on", "created_by", "part_of",
|
|
7
|
+
"replaced_by", "conflicts_with", "tested_by",
|
|
8
|
+
]),
|
|
9
|
+
});
|
|
10
|
+
export function relate(stmts, input) {
|
|
11
|
+
const fromEntity = stmts.getEntityByName.get(input.from);
|
|
12
|
+
if (!fromEntity) {
|
|
13
|
+
return `Error: Entity "${input.from}" not found. Use remember() to create it first.`;
|
|
14
|
+
}
|
|
15
|
+
const toEntity = stmts.getEntityByName.get(input.to);
|
|
16
|
+
if (!toEntity) {
|
|
17
|
+
return `Error: Entity "${input.to}" not found. Use remember() to create it first.`;
|
|
18
|
+
}
|
|
19
|
+
stmts.insertRelation.run(fromEntity.id, toEntity.id, input.relationType);
|
|
20
|
+
return `${input.from} --[${input.relationType}]--> ${input.to}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const RememberSchema: z.ZodObject<{
|
|
4
|
+
entity: z.ZodString;
|
|
5
|
+
entityType: z.ZodEnum<["person", "project", "decision", "pattern", "tool", "config", "bug", "convention"]>;
|
|
6
|
+
observation: z.ZodString;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
entity: string;
|
|
9
|
+
entityType: "person" | "project" | "decision" | "pattern" | "tool" | "config" | "bug" | "convention";
|
|
10
|
+
observation: string;
|
|
11
|
+
}, {
|
|
12
|
+
entity: string;
|
|
13
|
+
entityType: "person" | "project" | "decision" | "pattern" | "tool" | "config" | "bug" | "convention";
|
|
14
|
+
observation: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type RememberInput = z.infer<typeof RememberSchema>;
|
|
17
|
+
export declare function remember(stmts: Statements, input: RememberInput): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RememberSchema = z.object({
|
|
3
|
+
entity: z.string().min(1),
|
|
4
|
+
entityType: z.enum([
|
|
5
|
+
"person", "project", "decision", "pattern",
|
|
6
|
+
"tool", "config", "bug", "convention",
|
|
7
|
+
]),
|
|
8
|
+
observation: z.string().min(1),
|
|
9
|
+
});
|
|
10
|
+
export function remember(stmts, input) {
|
|
11
|
+
const { entity, entityType, observation } = input;
|
|
12
|
+
const existing = stmts.getEntityByName.get(entity);
|
|
13
|
+
if (existing) {
|
|
14
|
+
const observations = JSON.parse(existing.observations);
|
|
15
|
+
// Nu adăuga duplicate
|
|
16
|
+
if (!observations.includes(observation)) {
|
|
17
|
+
observations.push(observation);
|
|
18
|
+
stmts.updateEntity.run(JSON.stringify(observations), existing.id);
|
|
19
|
+
}
|
|
20
|
+
return `Updated "${entity}" [${existing.type}] — ${observations.length} observation(s) total.`;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
stmts.insertEntity.run(entity, entityType, JSON.stringify([observation]));
|
|
24
|
+
return `Created "${entity}" [${entityType}].`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { statSync } from "fs";
|
|
2
|
+
export function memoryStats(db, stmts) {
|
|
3
|
+
const entityCount = stmts.countEntities.get().count;
|
|
4
|
+
const relationCount = stmts.countRelations.get().count;
|
|
5
|
+
const walMode = stmts.getWalMode.get().journal_mode === "wal";
|
|
6
|
+
// Numără observațiile
|
|
7
|
+
const allEntities = stmts.getAllEntities.all();
|
|
8
|
+
let observationCount = 0;
|
|
9
|
+
for (const row of allEntities) {
|
|
10
|
+
const obs = JSON.parse(row.observations);
|
|
11
|
+
observationCount += obs.length;
|
|
12
|
+
}
|
|
13
|
+
let dbSizeBytes = 0;
|
|
14
|
+
try {
|
|
15
|
+
dbSizeBytes = statSync(db.name).size;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// in-memory sau inaccesibil
|
|
19
|
+
}
|
|
20
|
+
const stats = {
|
|
21
|
+
entity_count: entityCount,
|
|
22
|
+
relation_count: relationCount,
|
|
23
|
+
observation_count: observationCount,
|
|
24
|
+
db_size_bytes: dbSizeBytes,
|
|
25
|
+
db_size_kb: Math.round(dbSizeBytes / 1024),
|
|
26
|
+
wal_mode: walMode,
|
|
27
|
+
fts5_enabled: true,
|
|
28
|
+
};
|
|
29
|
+
return JSON.stringify(stats, null, 2);
|
|
30
|
+
}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type EntityType = "person" | "project" | "decision" | "pattern" | "tool" | "config" | "bug" | "convention";
|
|
2
|
+
export type RelationType = "uses" | "depends_on" | "created_by" | "part_of" | "replaced_by" | "conflicts_with" | "tested_by";
|
|
3
|
+
export interface Entity {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
type: EntityType;
|
|
7
|
+
observations: string[];
|
|
8
|
+
created_at: number;
|
|
9
|
+
updated_at: number;
|
|
10
|
+
}
|
|
11
|
+
export interface EntityRow {
|
|
12
|
+
id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
observations: string;
|
|
16
|
+
created_at: number;
|
|
17
|
+
updated_at: number;
|
|
18
|
+
}
|
|
19
|
+
export interface Relation {
|
|
20
|
+
id: number;
|
|
21
|
+
from_entity: number;
|
|
22
|
+
to_entity: number;
|
|
23
|
+
relation_type: RelationType;
|
|
24
|
+
created_at: number;
|
|
25
|
+
}
|
|
26
|
+
export interface RelationRow {
|
|
27
|
+
id: number;
|
|
28
|
+
from_entity: number;
|
|
29
|
+
to_entity: number;
|
|
30
|
+
relation_type: string;
|
|
31
|
+
created_at: number;
|
|
32
|
+
}
|
|
33
|
+
export interface EntityWithRelations extends Entity {
|
|
34
|
+
relations: RelationDisplay[];
|
|
35
|
+
}
|
|
36
|
+
export interface RelationDisplay {
|
|
37
|
+
from: string;
|
|
38
|
+
to: string;
|
|
39
|
+
type: string;
|
|
40
|
+
}
|
|
41
|
+
export interface MemoryStats {
|
|
42
|
+
entity_count: number;
|
|
43
|
+
relation_count: number;
|
|
44
|
+
observation_count: number;
|
|
45
|
+
db_size_bytes: number;
|
|
46
|
+
db_size_kb: number;
|
|
47
|
+
wal_mode: boolean;
|
|
48
|
+
fts5_enabled: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface KnowledgeGraph {
|
|
51
|
+
stats: MemoryStats;
|
|
52
|
+
entities: EntityWithRelations[];
|
|
53
|
+
}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a13xu/lucid",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Persistent memory for Claude Code agents — SQLite + FTS5 knowledge graph via MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lucid": "./build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build/**/*.js",
|
|
11
|
+
"build/**/*.d.ts",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"claude",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"memory",
|
|
23
|
+
"sqlite",
|
|
24
|
+
"knowledge-graph",
|
|
25
|
+
"ai",
|
|
26
|
+
"anthropic"
|
|
27
|
+
],
|
|
28
|
+
"author": "a13xu",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/a13xu/lucid.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/a13xu/lucid#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
+
"better-sqlite3": "^11.0.0",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
45
|
+
"@types/node": "^20.0.0",
|
|
46
|
+
"typescript": "^5.4.0"
|
|
47
|
+
}
|
|
48
|
+
}
|