coolmem 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/README.md +175 -0
- package/dist/db.d.ts +17 -0
- package/dist/db.js +26 -0
- package/dist/db.js.map +1 -0
- package/dist/embeddings.d.ts +19 -0
- package/dist/embeddings.js +46 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/logic.d.ts +56 -0
- package/dist/logic.js +180 -0
- package/dist/logic.js.map +1 -0
- package/dist/similarity.d.ts +24 -0
- package/dist/similarity.js +31 -0
- package/dist/similarity.js.map +1 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +38 -0
- package/dist/tools.js.map +1 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +23 -0
- package/dist/utils.js.map +1 -0
- package/package.json +29 -0
- package/prisma/schema.prisma +21 -0
- package/seed.mjs +69 -0
- package/test.mjs +185 -0
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# π§ coolmem β Cross-Agent Memory Fabric
|
|
2
|
+
|
|
3
|
+
A **privacy-first MCP server** that lets different AI agents (Cursor, Claude Desktop, etc.)
|
|
4
|
+
share a persistent **Knowledge Ledger** β powered by local vector embeddings and SQLite.
|
|
5
|
+
|
|
6
|
+
No API keys. No data leaves your machine.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Architecture at a Glance
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Agent A (Cursor) Agent B (Claude)
|
|
14
|
+
β β
|
|
15
|
+
β store_memory(content, cat) β search_memories(query)
|
|
16
|
+
βΌ βΌ
|
|
17
|
+
βββββββββββββββ MCP stdio ββββββββββββββββββ
|
|
18
|
+
β coolmem server β
|
|
19
|
+
β ββββββββββββββββββββββββββββββββββββββββββ β
|
|
20
|
+
β β @xenova/transformers β β
|
|
21
|
+
β β all-MiniLM-L6-v2 (384-dim, local CPU) β β
|
|
22
|
+
β ββββββββββββββββββ¬ββββββββββββββββββββββββ β
|
|
23
|
+
β β embed() β
|
|
24
|
+
β ββββββββββββββββββΌββββββββββββββββββββββββ β
|
|
25
|
+
β β SQLite (coolmem.db) β β
|
|
26
|
+
β β Memory { id, content, category, β β
|
|
27
|
+
β β embedding JSON, createdAt } β β
|
|
28
|
+
β ββββββββββββββββββββββββββββββββββββββββββ β
|
|
29
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 1. Install dependencies
|
|
38
|
+
npm install
|
|
39
|
+
|
|
40
|
+
# 2. Generate Prisma client + create the SQLite DB
|
|
41
|
+
npx prisma generate
|
|
42
|
+
npx prisma db push
|
|
43
|
+
|
|
44
|
+
# 3. Build
|
|
45
|
+
npm run build
|
|
46
|
+
|
|
47
|
+
# 4. Run (for manual testing)
|
|
48
|
+
node dist/index.js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The server communicates over **stdio** β you don't call it directly.
|
|
52
|
+
Hook it into Claude Desktop or Cursor via the config below.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Connecting to Claude Desktop
|
|
57
|
+
|
|
58
|
+
Open (or create) your `claude_desktop_config.json`:
|
|
59
|
+
|
|
60
|
+
- **macOS / Linux**: `~/.config/claude/claude_desktop_config.json`
|
|
61
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
62
|
+
|
|
63
|
+
Add the following block (adjust the path to match where you cloned this repo):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"coolmem": {
|
|
69
|
+
"command": "node",
|
|
70
|
+
"args": ["C:/Users/Dell/Desktop/YCU/coolmem/dist/index.js"],
|
|
71
|
+
"env": {
|
|
72
|
+
"DATABASE_URL": "file:C:/Users/Dell/Desktop/YCU/coolmem/coolmem.db"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> **Tip:** After editing the config, restart Claude Desktop. You should see
|
|
80
|
+
> `coolmem` appear in the Connected Servers list.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Connecting to Cursor
|
|
85
|
+
|
|
86
|
+
In Cursor β Settings β MCP β Add New MCP Server:
|
|
87
|
+
|
|
88
|
+
| Field | Value |
|
|
89
|
+
|---------|------------------------------------------------------------|
|
|
90
|
+
| Name | `coolmem` |
|
|
91
|
+
| Command | `node` |
|
|
92
|
+
| Args | `C:/Users/Dell/Desktop/YCU/coolmem/dist/index.js` |
|
|
93
|
+
| Env | `DATABASE_URL=file:C:/Users/Dell/Desktop/YCU/coolmem/coolmem.db` |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Available MCP Primitives
|
|
98
|
+
|
|
99
|
+
### π§ Tool: `store_memory`
|
|
100
|
+
|
|
101
|
+
Store a lesson learned, architectural decision, or any project knowledge.
|
|
102
|
+
|
|
103
|
+
| Parameter | Type | Description |
|
|
104
|
+
|------------|--------|------------------------------------------------------|
|
|
105
|
+
| `content` | string | The knowledge text to persist |
|
|
106
|
+
| `category` | string | Label: `architecture`, `bug-fix`, `decision`, etc. |
|
|
107
|
+
|
|
108
|
+
**Example agent prompt:**
|
|
109
|
+
> Use the `store_memory` tool. content: "We switched from REST to tRPC because it eliminates
|
|
110
|
+
> manual type duplication between client and server." category: "architecture"
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### π§ Tool: `search_memories`
|
|
115
|
+
|
|
116
|
+
Semantically search the ledger with a natural-language query.
|
|
117
|
+
|
|
118
|
+
| Parameter | Type | Description |
|
|
119
|
+
|-----------|--------|-------------------------------|
|
|
120
|
+
| `query` | string | What you want to know about |
|
|
121
|
+
|
|
122
|
+
Returns the **top 3 most relevant memories** with cosine-similarity scores.
|
|
123
|
+
|
|
124
|
+
**Example agent prompt:**
|
|
125
|
+
> Use `search_memories`. query: "Why did we change our API layer?"
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### π Resource: `project_timeline`
|
|
130
|
+
|
|
131
|
+
**URI:** `memory://coolmem/timeline`
|
|
132
|
+
|
|
133
|
+
A chronological list of the **last 10 things** the agents stored β useful for
|
|
134
|
+
onboarding a fresh agent to the current state of a project.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Project Structure
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
coolmem/
|
|
142
|
+
βββ src/
|
|
143
|
+
β βββ index.ts # MCP server β tools & resource definitions
|
|
144
|
+
β βββ embeddings.ts # @xenova/transformers wrapper (lazy singleton)
|
|
145
|
+
β βββ similarity.ts # Cosine similarity function
|
|
146
|
+
β βββ db.ts # Prisma client singleton
|
|
147
|
+
βββ prisma/
|
|
148
|
+
β βββ schema.prisma # Memory model β SQLite
|
|
149
|
+
βββ dist/ # Compiled output (git-ignored)
|
|
150
|
+
βββ models/ # Cached model weights (auto-downloaded, git-ignored)
|
|
151
|
+
βββ coolmem.db # SQLite database (git-ignored)
|
|
152
|
+
βββ .env # DATABASE_URL
|
|
153
|
+
βββ package.json
|
|
154
|
+
βββ tsconfig.json
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## First-Run Note on Model Download
|
|
160
|
+
|
|
161
|
+
On the **very first call** to either tool, `@xenova/transformers` will download
|
|
162
|
+
`all-MiniLM-L6-v2` (~22 MB) into the `./models/` folder. Subsequent calls use
|
|
163
|
+
the cached weights instantly. This is a one-time cost.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Extending This Server
|
|
168
|
+
|
|
169
|
+
| Want to add⦠| How |
|
|
170
|
+
|-------------------------------|------------------------------------------------------------------|
|
|
171
|
+
| Delete a memory | Add a `delete_memory(id)` tool via `server.tool()` |
|
|
172
|
+
| List memories by category | Add a `list_memories(category)` tool |
|
|
173
|
+
| More search results | Change `.slice(0, 3)` to `.slice(0, N)` in `index.ts` |
|
|
174
|
+
| Faster vector search at scale | Swap the JS cosine loop for `sqlite-vss` or `usearch` |
|
|
175
|
+
| Different model | Change `MODEL_NAME` in `embeddings.ts` |
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts β Prisma Client singleton with automatic directory creation
|
|
3
|
+
*
|
|
4
|
+
* This version ensures the database and models are stored in a persistent
|
|
5
|
+
* folder in the user's home directory (~/.coolmem), making it portable for NPX.
|
|
6
|
+
*/
|
|
7
|
+
import { PrismaClient } from "@prisma/client";
|
|
8
|
+
declare const COOLMEM_DIR: string;
|
|
9
|
+
declare const prisma: PrismaClient<{
|
|
10
|
+
datasources: {
|
|
11
|
+
db: {
|
|
12
|
+
url: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
}, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
16
|
+
export default prisma;
|
|
17
|
+
export { COOLMEM_DIR };
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts β Prisma Client singleton with automatic directory creation
|
|
3
|
+
*
|
|
4
|
+
* This version ensures the database and models are stored in a persistent
|
|
5
|
+
* folder in the user's home directory (~/.coolmem), making it portable for NPX.
|
|
6
|
+
*/
|
|
7
|
+
import { PrismaClient } from "@prisma/client";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { mkdirSync } from "fs";
|
|
11
|
+
// 1. Ensure the persistent data folder exists
|
|
12
|
+
const COOLMEM_DIR = join(homedir(), ".coolmem");
|
|
13
|
+
mkdirSync(COOLMEM_DIR, { recursive: true });
|
|
14
|
+
// 2. Derive the database file path
|
|
15
|
+
const dbPath = join(COOLMEM_DIR, "memories.db");
|
|
16
|
+
// 3. Initialize Prisma with the explicit URL to bypass .env requirements
|
|
17
|
+
const prisma = new PrismaClient({
|
|
18
|
+
datasources: {
|
|
19
|
+
db: {
|
|
20
|
+
url: `file:${dbPath}?connection_limit=1`,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
export default prisma;
|
|
25
|
+
export { COOLMEM_DIR };
|
|
26
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,8CAA8C;AAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AAChD,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE5C,mCAAmC;AACnC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AAEhD,yEAAyE;AACzE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;IAC9B,WAAW,EAAE;QACX,EAAE,EAAE;YACF,GAAG,EAAE,QAAQ,MAAM,qBAAqB;SACzC;KACF;CACF,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embeddings.ts β Local embedding generation via @xenova/transformers
|
|
3
|
+
*
|
|
4
|
+
* We use 'Xenova/all-MiniLM-L6-v2', a small (22 MB) sentence-transformer
|
|
5
|
+
* model that runs entirely on your CPU β no API keys, no data leaves your
|
|
6
|
+
* machine. It produces 384-dimensional float32 embeddings.
|
|
7
|
+
*
|
|
8
|
+
* The pipeline is initialised lazily (first call) and then cached, so the
|
|
9
|
+
* model is only loaded once per server process.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Converts a text string into a 384-dimensional embedding vector.
|
|
13
|
+
* Mean-pooling is applied over the token dimension so the output is
|
|
14
|
+
* always a flat `number[]` regardless of input length.
|
|
15
|
+
*
|
|
16
|
+
* @param text - The raw string to embed
|
|
17
|
+
* @returns A flat number[] of length 384
|
|
18
|
+
*/
|
|
19
|
+
export declare function embed(text: string): Promise<number[]>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embeddings.ts β Local embedding generation via @xenova/transformers
|
|
3
|
+
*
|
|
4
|
+
* We use 'Xenova/all-MiniLM-L6-v2', a small (22 MB) sentence-transformer
|
|
5
|
+
* model that runs entirely on your CPU β no API keys, no data leaves your
|
|
6
|
+
* machine. It produces 384-dimensional float32 embeddings.
|
|
7
|
+
*
|
|
8
|
+
* The pipeline is initialised lazily (first call) and then cached, so the
|
|
9
|
+
* model is only loaded once per server process.
|
|
10
|
+
*/
|
|
11
|
+
// @xenova/transformers is ESM-only; the dynamic import below is intentional.
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
let pipelineInstance = null;
|
|
14
|
+
const MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { COOLMEM_DIR } from "./db.js";
|
|
17
|
+
/**
|
|
18
|
+
* Returns the cached feature-extraction pipeline, loading it on first call.
|
|
19
|
+
* On first use it will download the model weights (~22 MB) into a local cache.
|
|
20
|
+
*/
|
|
21
|
+
async function getPipeline() {
|
|
22
|
+
if (!pipelineInstance) {
|
|
23
|
+
// Dynamic import keeps the ESM-only package compatible with our build
|
|
24
|
+
const { pipeline, env } = await import("@xenova/transformers");
|
|
25
|
+
// Store model files in the persistent folder (~/.coolmem/models)
|
|
26
|
+
env.cacheDir = join(COOLMEM_DIR, "models");
|
|
27
|
+
pipelineInstance = await pipeline("feature-extraction", MODEL_NAME);
|
|
28
|
+
}
|
|
29
|
+
return pipelineInstance;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Converts a text string into a 384-dimensional embedding vector.
|
|
33
|
+
* Mean-pooling is applied over the token dimension so the output is
|
|
34
|
+
* always a flat `number[]` regardless of input length.
|
|
35
|
+
*
|
|
36
|
+
* @param text - The raw string to embed
|
|
37
|
+
* @returns A flat number[] of length 384
|
|
38
|
+
*/
|
|
39
|
+
export async function embed(text) {
|
|
40
|
+
const extractor = await getPipeline();
|
|
41
|
+
// { pooling: "mean", normalize: true } β sentence embedding best practice
|
|
42
|
+
const output = await extractor(text, { pooling: "mean", normalize: true });
|
|
43
|
+
// output.data is a Float32Array; convert to a plain JS array
|
|
44
|
+
return Array.from(output.data);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=embeddings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeddings.js","sourceRoot":"","sources":["../src/embeddings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,6EAA6E;AAC7E,8DAA8D;AAC9D,IAAI,gBAAgB,GAAe,IAAI,CAAC;AAExC,MAAM,UAAU,GAAG,yBAAyB,CAAC;AAE7C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC;;;GAGG;AACH,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,sEAAsE;QACtE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAE/D,iEAAiE;QACjE,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAE3C,gBAAgB,GAAG,MAAM,QAAQ,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAY;IACtC,MAAM,SAAS,GAAG,MAAM,WAAW,EAAE,CAAC;IAEtC,0EAA0E;IAC1E,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3E,6DAA6D;IAC7D,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAoB,CAAC,CAAC;AACjD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* index.ts β Cross-Agent Memory Fabric: MCP Server (v2 β Optimized)
|
|
4
|
+
*
|
|
5
|
+
* This version separates the tool definitions and logic into separate files:
|
|
6
|
+
* - tools.ts: MCP tool and resource registrations
|
|
7
|
+
* - logic.ts: Business logic (Prisma, embeddings, etc.)
|
|
8
|
+
* - utils.ts: Constants and encoders
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* index.ts β Cross-Agent Memory Fabric: MCP Server (v2 β Optimized)
|
|
4
|
+
*
|
|
5
|
+
* This version separates the tool definitions and logic into separate files:
|
|
6
|
+
* - tools.ts: MCP tool and resource registrations
|
|
7
|
+
* - logic.ts: Business logic (Prisma, embeddings, etc.)
|
|
8
|
+
* - utils.ts: Constants and encoders
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import { registerTools } from "./tools.js";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { existsSync, writeFileSync } from "fs";
|
|
16
|
+
import { COOLMEM_DIR } from "./db.js";
|
|
17
|
+
// ββ Database Auto-Initialization ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
+
/**
|
|
19
|
+
* Ensures the database schema is applied. Runs 'prisma db push' on first launch.
|
|
20
|
+
* This makes the server "Plug and Play" for new users via npx.
|
|
21
|
+
*/
|
|
22
|
+
function ensureDatabaseIsReady() {
|
|
23
|
+
const flagFile = join(COOLMEM_DIR, ".pristine");
|
|
24
|
+
if (!existsSync(flagFile)) {
|
|
25
|
+
process.stderr.write("[coolmem] First run detected. Initializing database schemaβ¦\n");
|
|
26
|
+
try {
|
|
27
|
+
// We use the prisma CLI directly. Since it's a devDependency,
|
|
28
|
+
// it will be available in the node_modules when installed via npm/npx.
|
|
29
|
+
execSync("npx prisma db push --accept-data-loss", {
|
|
30
|
+
stdio: "inherit",
|
|
31
|
+
env: { ...process.env, DATABASE_URL: `file:${join(COOLMEM_DIR, "memories.db")}` }
|
|
32
|
+
});
|
|
33
|
+
writeFileSync(flagFile, "Initialised at " + new Date().toISOString());
|
|
34
|
+
process.stderr.write("[coolmem] Database initialization complete.\n");
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
process.stderr.write(`[coolmem] Warning: Auto-init failed. You may need to run 'npx prisma db push' manually. Error: ${err}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ββ Server bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
42
|
+
const server = new McpServer({ name: "coolmem", version: "2.0.0" });
|
|
43
|
+
// Register all tools and resources
|
|
44
|
+
registerTools(server);
|
|
45
|
+
// ββ Start βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
46
|
+
async function main() {
|
|
47
|
+
// Ensure DB is ready before connecting transport
|
|
48
|
+
ensureDatabaseIsReady();
|
|
49
|
+
const transport = new StdioServerTransport();
|
|
50
|
+
process.stderr.write("[coolmem v2] Cross-Agent Memory Fabric startingβ¦\n");
|
|
51
|
+
await server.connect(transport);
|
|
52
|
+
process.stderr.write("[coolmem v2] Connected via stdio. Ready.\n");
|
|
53
|
+
}
|
|
54
|
+
main().catch((err) => {
|
|
55
|
+
process.stderr.write(`[coolmem v2] Fatal: ${err}\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,iFAAiF;AAEjF;;;GAGG;AACH,SAAS,qBAAqB;IAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAChD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+DAA+D,CAAC,CAAC;QACtF,IAAI,CAAC;YACH,+DAA+D;YAC/D,uEAAuE;YACvE,QAAQ,CAAC,uCAAuC,EAAE;gBAChD,KAAK,EAAE,SAAS;gBAChB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,QAAQ,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EAAE,EAAE;aAClF,CAAC,CAAC;YACH,aAAa,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACxE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kGAAkG,GAAG,IAAI,CAAC,CAAC;QAClI,CAAC;IACH,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAEpE,mCAAmC;AACnC,aAAa,CAAC,MAAM,CAAC,CAAC;AAEtB,iFAAiF;AAEjF,KAAK,UAAU,IAAI;IACjB,iDAAiD;IACjD,qBAAqB,EAAE,CAAC;IAExB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;IAC3E,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;AACrE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/logic.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logic.ts β Core business logic for the Cross-Agent Memory Fabric
|
|
3
|
+
*/
|
|
4
|
+
interface StoreMemoryParams {
|
|
5
|
+
content: string;
|
|
6
|
+
category: string;
|
|
7
|
+
project: string;
|
|
8
|
+
}
|
|
9
|
+
interface SearchMemoriesParams {
|
|
10
|
+
query: string;
|
|
11
|
+
project?: string;
|
|
12
|
+
limit: number;
|
|
13
|
+
}
|
|
14
|
+
interface UpdateMemoryParams {
|
|
15
|
+
id: number;
|
|
16
|
+
content?: string;
|
|
17
|
+
category?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function storeMemory({ content, category, project }: StoreMemoryParams): Promise<{
|
|
20
|
+
content: {
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
}>;
|
|
25
|
+
export declare function searchMemories({ query, project, limit }: SearchMemoriesParams): Promise<{
|
|
26
|
+
content: {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
}[];
|
|
30
|
+
}>;
|
|
31
|
+
export declare function deleteMemory(id: number): Promise<{
|
|
32
|
+
content: {
|
|
33
|
+
type: "text";
|
|
34
|
+
text: string;
|
|
35
|
+
}[];
|
|
36
|
+
}>;
|
|
37
|
+
export declare function updateMemory({ id, content, category }: UpdateMemoryParams): Promise<{
|
|
38
|
+
content: {
|
|
39
|
+
type: "text";
|
|
40
|
+
text: string;
|
|
41
|
+
}[];
|
|
42
|
+
}>;
|
|
43
|
+
export declare function listProjects(): Promise<{
|
|
44
|
+
content: {
|
|
45
|
+
type: "text";
|
|
46
|
+
text: string;
|
|
47
|
+
}[];
|
|
48
|
+
}>;
|
|
49
|
+
export declare function getProjectTimeline(): Promise<{
|
|
50
|
+
contents: {
|
|
51
|
+
uri: string;
|
|
52
|
+
text: string;
|
|
53
|
+
mimeType: "text/plain";
|
|
54
|
+
}[];
|
|
55
|
+
}>;
|
|
56
|
+
export {};
|
package/dist/logic.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logic.ts β Core business logic for the Cross-Agent Memory Fabric
|
|
3
|
+
*/
|
|
4
|
+
import prisma from "./db.js";
|
|
5
|
+
import { embed } from "./embeddings.js";
|
|
6
|
+
import { cosineSimilarity } from "./similarity.js";
|
|
7
|
+
import { DEDUP_THRESHOLD, MIN_SCORE_THRESHOLD, encodeEmbedding, decodeEmbedding } from "./utils.js";
|
|
8
|
+
export async function storeMemory({ content, category, project }) {
|
|
9
|
+
// β Generate local embedding
|
|
10
|
+
const vector = await embed(content);
|
|
11
|
+
// β‘ Deduplication: scan existing memories in this project for near-duplicates
|
|
12
|
+
const existing = await prisma.memory.findMany({
|
|
13
|
+
where: { project },
|
|
14
|
+
select: { id: true, embedding: true },
|
|
15
|
+
});
|
|
16
|
+
for (const m of existing) {
|
|
17
|
+
const score = cosineSimilarity(vector, decodeEmbedding(m.embedding));
|
|
18
|
+
if (score >= DEDUP_THRESHOLD) {
|
|
19
|
+
// β’ Near-duplicate found β update in place
|
|
20
|
+
const updated = await prisma.memory.update({
|
|
21
|
+
where: { id: m.id },
|
|
22
|
+
data: { content, category, embedding: encodeEmbedding(vector) },
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: `β»οΈ Near-duplicate detected (score: ${score.toFixed(4)}). Updated Memory #${updated.id} in [${project}] instead of creating a duplicate.`,
|
|
28
|
+
}],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// β£ No duplicate β insert new memory
|
|
33
|
+
const memory = await prisma.memory.create({
|
|
34
|
+
data: { content, category, project, embedding: encodeEmbedding(vector) },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `β
Memory #${memory.id} stored in project [${project}].\nCategory: ${category}\nPreview: "${content.slice(0, 120)}${content.length > 120 ? "β¦" : ""}"`,
|
|
40
|
+
}],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function searchMemories({ query, project, limit }) {
|
|
44
|
+
// β Embed the query
|
|
45
|
+
const queryVector = await embed(query);
|
|
46
|
+
// β‘ Fetch relevant memories
|
|
47
|
+
const allMemories = await prisma.memory.findMany({
|
|
48
|
+
where: project ? { project } : undefined,
|
|
49
|
+
select: { id: true, content: true, category: true, project: true, embedding: true, createdAt: true },
|
|
50
|
+
});
|
|
51
|
+
if (allMemories.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: project
|
|
56
|
+
? `π No memories found for project "${project}". Use store_memory to add the first entry.`
|
|
57
|
+
: "π The memory ledger is empty. Use store_memory to add the first entry.",
|
|
58
|
+
}],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// β’ Score + decode in one pass
|
|
62
|
+
const scored = allMemories
|
|
63
|
+
.map((m) => ({ ...m, score: cosineSimilarity(queryVector, decodeEmbedding(m.embedding)) }))
|
|
64
|
+
.filter((m) => m.score >= MIN_SCORE_THRESHOLD)
|
|
65
|
+
.sort((a, b) => b.score - a.score)
|
|
66
|
+
.slice(0, limit);
|
|
67
|
+
if (scored.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: `π No memories scored above the relevance threshold (${MIN_SCORE_THRESHOLD}) for: "${query}"\n\nTry rephrasing or broadening your query.`,
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const formatted = scored
|
|
76
|
+
.map((m, i) => `#${i + 1} [score: ${m.score.toFixed(4)}, project: "${m.project}", category: "${m.category}", id: ${m.id}]\n${m.content}`)
|
|
77
|
+
.join("\n\n---\n\n");
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `π ${scored.length} result${scored.length > 1 ? "s" : ""} for: "${query}"\n\n${formatted}`,
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function deleteMemory(id) {
|
|
86
|
+
const existing = await prisma.memory.findUnique({ where: { id }, select: { id: true, content: true } });
|
|
87
|
+
if (!existing) {
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: `β Memory #${id} not found. It may have already been deleted.` }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
await prisma.memory.delete({ where: { id } });
|
|
93
|
+
return {
|
|
94
|
+
content: [{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: `ποΈ Memory #${id} deleted.\nDeleted content: "${existing.content.slice(0, 100)}${existing.content.length > 100 ? "β¦" : ""}"`,
|
|
97
|
+
}],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function updateMemory({ id, content, category }) {
|
|
101
|
+
const existing = await prisma.memory.findUnique({ where: { id } });
|
|
102
|
+
if (!existing) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: `β Memory #${id} not found.` }],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const newEmbedding = content ? encodeEmbedding(await embed(content)) : undefined;
|
|
108
|
+
const updated = await prisma.memory.update({
|
|
109
|
+
where: { id },
|
|
110
|
+
data: {
|
|
111
|
+
...(content && { content }),
|
|
112
|
+
...(category && { category }),
|
|
113
|
+
...(newEmbedding && { embedding: newEmbedding }),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
content: [{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: [
|
|
120
|
+
`βοΈ Memory #${updated.id} updated.`,
|
|
121
|
+
content ? ` Content : "${updated.content.slice(0, 100)}β¦"` : null,
|
|
122
|
+
category ? ` Category: ${updated.category}` : null,
|
|
123
|
+
content ? " Embedding regenerated." : null,
|
|
124
|
+
].filter(Boolean).join("\n"),
|
|
125
|
+
}],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export async function listProjects() {
|
|
129
|
+
const groups = await prisma.memory.groupBy({
|
|
130
|
+
by: ["project"],
|
|
131
|
+
_count: { _all: true },
|
|
132
|
+
});
|
|
133
|
+
if (groups.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: "π No projects found in the ledger." }],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const list = groups
|
|
139
|
+
.map((g) => `- **${g.project}**: ${g._count._all} memories`)
|
|
140
|
+
.sort()
|
|
141
|
+
.join("\n");
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: `π **Available Projects:**\n\n${list}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function getProjectTimeline() {
|
|
152
|
+
const recent = await prisma.memory.findMany({
|
|
153
|
+
orderBy: { createdAt: "desc" },
|
|
154
|
+
take: 10,
|
|
155
|
+
select: { id: true, content: true, category: true, project: true, createdAt: true },
|
|
156
|
+
});
|
|
157
|
+
if (recent.length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
contents: [{
|
|
160
|
+
uri: "memory://coolmem/timeline",
|
|
161
|
+
text: "No memories have been stored yet.",
|
|
162
|
+
mimeType: "text/plain",
|
|
163
|
+
}],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const lines = recent.map((m) => {
|
|
167
|
+
const ts = m.createdAt.toISOString().replace("T", " ").slice(0, 19);
|
|
168
|
+
return `[${ts}] #${m.id} | project: ${m.project} | ${m.category}\n ${m.content.slice(0, 200)}${m.content.length > 200 ? "β¦" : ""}`;
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
contents: [{
|
|
172
|
+
uri: "memory://coolmem/timeline",
|
|
173
|
+
mimeType: "text/plain",
|
|
174
|
+
text: `π Project Timeline β Last ${recent.length} memories (newest first)\n` +
|
|
175
|
+
`${"β".repeat(60)}\n\n` +
|
|
176
|
+
lines.join("\n\n"),
|
|
177
|
+
}],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logic.js","sourceRoot":"","sources":["../src/logic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* similarity.ts β Optimized dot-product similarity for normalized vectors
|
|
3
|
+
*
|
|
4
|
+
* WHY NOT FULL COSINE:
|
|
5
|
+
* Full cosine = dot(a,b) / (|a| Γ |b|)
|
|
6
|
+
* But embeddings.ts always uses { normalize: true }, so every vector from
|
|
7
|
+
* all-MiniLM-L6-v2 is a unit vector (|v| = 1). The denominator is always
|
|
8
|
+
* 1Γ1 = 1, reducing to a pure dot product β saving 2 magnitude accumulators
|
|
9
|
+
* + 2 Math.sqrt calls per comparison (~60% fewer ops across 384 dimensions).
|
|
10
|
+
*
|
|
11
|
+
* RESULT RANGE: still [-1, 1] β identical semantics to full cosine similarity.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: If you ever change normalize to false in embeddings.ts, restore the
|
|
14
|
+
* full cosine formula or pre-normalize vectors at write-time.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Dot-product similarity for L2-normalized (unit) vectors.
|
|
18
|
+
* Semantically equivalent to cosine similarity when inputs are unit vectors.
|
|
19
|
+
*
|
|
20
|
+
* @param a - Unit vector (|a| = 1), length 384
|
|
21
|
+
* @param b - Unit vector (|b| = 1), length 384
|
|
22
|
+
* @returns Score in [-1, 1]; higher = more similar
|
|
23
|
+
*/
|
|
24
|
+
export declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* similarity.ts β Optimized dot-product similarity for normalized vectors
|
|
3
|
+
*
|
|
4
|
+
* WHY NOT FULL COSINE:
|
|
5
|
+
* Full cosine = dot(a,b) / (|a| Γ |b|)
|
|
6
|
+
* But embeddings.ts always uses { normalize: true }, so every vector from
|
|
7
|
+
* all-MiniLM-L6-v2 is a unit vector (|v| = 1). The denominator is always
|
|
8
|
+
* 1Γ1 = 1, reducing to a pure dot product β saving 2 magnitude accumulators
|
|
9
|
+
* + 2 Math.sqrt calls per comparison (~60% fewer ops across 384 dimensions).
|
|
10
|
+
*
|
|
11
|
+
* RESULT RANGE: still [-1, 1] β identical semantics to full cosine similarity.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: If you ever change normalize to false in embeddings.ts, restore the
|
|
14
|
+
* full cosine formula or pre-normalize vectors at write-time.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Dot-product similarity for L2-normalized (unit) vectors.
|
|
18
|
+
* Semantically equivalent to cosine similarity when inputs are unit vectors.
|
|
19
|
+
*
|
|
20
|
+
* @param a - Unit vector (|a| = 1), length 384
|
|
21
|
+
* @param b - Unit vector (|b| = 1), length 384
|
|
22
|
+
* @returns Score in [-1, 1]; higher = more similar
|
|
23
|
+
*/
|
|
24
|
+
export function cosineSimilarity(a, b) {
|
|
25
|
+
let dot = 0;
|
|
26
|
+
for (let i = 0; i < a.length; i++) {
|
|
27
|
+
dot += a[i] * b[i];
|
|
28
|
+
}
|
|
29
|
+
return dot;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=similarity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similarity.js","sourceRoot":"","sources":["../src/similarity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAW,EAAE,CAAW;IACvD,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.ts β MCP Tool and Resource definitions
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
/**
|
|
6
|
+
* Registers all tools and resources to the given MCP server instance.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerTools(server: McpServer): void;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.ts β MCP Tool and Resource definitions
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import * as logic from "./logic.js";
|
|
6
|
+
import { MAX_LIMIT } from "./utils.js";
|
|
7
|
+
/**
|
|
8
|
+
* Registers all tools and resources to the given MCP server instance.
|
|
9
|
+
*/
|
|
10
|
+
export function registerTools(server) {
|
|
11
|
+
// ββ TOOL 1: store_memory βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
12
|
+
server.tool("store_memory", "Store a lesson learned, architectural decision, or project knowledge into the shared memory ledger. Automatically deduplicates β if a near-identical memory already exists, it is updated rather than duplicated.", {
|
|
13
|
+
content: z.string().min(1).describe("The knowledge to persist. Be specific for better recall."),
|
|
14
|
+
category: z.string().min(1).describe('Label: "architecture", "bug-fix", "decision", "api-contract", "deployment", etc.'),
|
|
15
|
+
project: z.string().default("default").describe('Project namespace, e.g. "coolmem", "fastgistics". Keeps memories scoped. Defaults to "default".'),
|
|
16
|
+
}, async (params) => logic.storeMemory(params));
|
|
17
|
+
// ββ TOOL 2: search_memories ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
+
server.tool("search_memories", "Semantically search the shared knowledge ledger. Returns the top N most relevant memories above a minimum relevance threshold. Results below 0.30 cosine similarity are automatically excluded as irrelevant.", {
|
|
19
|
+
query: z.string().min(1).describe("Natural-language question or topic to search for."),
|
|
20
|
+
project: z.string().optional().describe('Restrict search to a specific project namespace. Omit to search all projects.'),
|
|
21
|
+
limit: z.number().int().min(1).max(MAX_LIMIT).default(3).describe(`Max results to return (1β${MAX_LIMIT}, default 3).`),
|
|
22
|
+
}, async (params) => logic.searchMemories(params));
|
|
23
|
+
// ββ TOOL 3: delete_memory ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
24
|
+
server.tool("delete_memory", "Permanently delete a memory from the ledger by its ID. Use this when a decision has changed or information is outdated. The ID is shown in search results.", {
|
|
25
|
+
id: z.number().int().positive().describe("The numeric ID of the memory to delete (visible in search results)."),
|
|
26
|
+
}, async ({ id }) => logic.deleteMemory(id));
|
|
27
|
+
// ββ TOOL 4: update_memory ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
28
|
+
server.tool("update_memory", "Update an existing memory's content and/or category. If content is updated, the embedding is automatically regenerated. Use this when a decision evolves rather than creating a duplicate.", {
|
|
29
|
+
id: z.number().int().positive().describe("The numeric ID of the memory to update."),
|
|
30
|
+
content: z.string().min(1).optional().describe("New content text. If provided, the embedding is regenerated automatically."),
|
|
31
|
+
category: z.string().min(1).optional().describe("New category label."),
|
|
32
|
+
}, async (params) => logic.updateMemory(params));
|
|
33
|
+
// ββ TOOL 5: list_projects ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
34
|
+
server.tool("list_projects", "List all project namespaces currently stored in the memory ledger, along with the count of memories in each. Use this to discover what knowledge buckets are available.", {}, async () => logic.listProjects());
|
|
35
|
+
// ββ RESOURCE: project_timeline βββββββββββββββββββββββββββββββββββββββββββββ
|
|
36
|
+
server.resource("project_timeline", "memory://coolmem/timeline", async () => logic.getProjectTimeline());
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,MAAiB;IAC7C,8EAA8E;IAC9E,MAAM,CAAC,IAAI,CACT,cAAc,EACd,mNAAmN,EACnN;QACE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,0DAA0D,CAAC;QAC/F,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,kFAAkF,CAAC;QACxH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,iGAAiG,CAAC;KACnJ,EACD,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAC5C,CAAC;IAEF,8EAA8E;IAC9E,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,+MAA+M,EAC/M;QACE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,mDAAmD,CAAC;QACtF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+EAA+E,CAAC;QACxH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,4BAA4B,SAAS,eAAe,CAAC;KACxH,EACD,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAC/C,CAAC;IAEF,8EAA8E;IAC9E,MAAM,CAAC,IAAI,CACT,eAAe,EACf,4JAA4J,EAC5J;QACE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qEAAqE,CAAC;KAChH,EACD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CACzC,CAAC;IAEF,8EAA8E;IAC9E,MAAM,CAAC,IAAI,CACT,eAAe,EACf,4LAA4L,EAC5L;QACE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QACnF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4EAA4E,CAAC;QAC5H,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qBAAqB,CAAC;KACvE,EACD,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAC7C,CAAC;IAEF,8EAA8E;IAC9E,MAAM,CAAC,IAAI,CACT,eAAe,EACf,yKAAyK,EACzK,EAAE,EACF,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CACjC,CAAC;IAEF,8EAA8E;IAC9E,MAAM,CAAC,QAAQ,CACb,kBAAkB,EAClB,2BAA2B,EAC3B,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,kBAAkB,EAAE,CACvC,CAAC;AACJ,CAAC"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils.ts β Embedding codecs and constants for the Memory Fabric
|
|
3
|
+
*/
|
|
4
|
+
/** Cosine score above which a new memory is considered a duplicate of an existing one */
|
|
5
|
+
export declare const DEDUP_THRESHOLD = 0.88;
|
|
6
|
+
/** Cosine score below which a search result is considered irrelevant and dropped */
|
|
7
|
+
export declare const MIN_SCORE_THRESHOLD = 0.3;
|
|
8
|
+
/** Maximum results a caller can request */
|
|
9
|
+
export declare const MAX_LIMIT = 20;
|
|
10
|
+
export declare function encodeEmbedding(vector: number[]): Buffer;
|
|
11
|
+
export declare function decodeEmbedding(buf: Buffer): number[];
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils.ts β Embedding codecs and constants for the Memory Fabric
|
|
3
|
+
*/
|
|
4
|
+
// ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
5
|
+
/** Cosine score above which a new memory is considered a duplicate of an existing one */
|
|
6
|
+
export const DEDUP_THRESHOLD = 0.88;
|
|
7
|
+
/** Cosine score below which a search result is considered irrelevant and dropped */
|
|
8
|
+
export const MIN_SCORE_THRESHOLD = 0.30;
|
|
9
|
+
/** Maximum results a caller can request */
|
|
10
|
+
export const MAX_LIMIT = 20;
|
|
11
|
+
// ββ Binary embedding codec ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
12
|
+
// Stores Float32 vectors as raw bytes: 384 Γ 4 B = 1,536 B per memory
|
|
13
|
+
// vs ~4 KB as a JSON string β plus no JSON.parse() on every search.
|
|
14
|
+
export function encodeEmbedding(vector) {
|
|
15
|
+
const f32 = new Float32Array(vector);
|
|
16
|
+
return Buffer.from(f32.buffer);
|
|
17
|
+
}
|
|
18
|
+
export function decodeEmbedding(buf) {
|
|
19
|
+
// Buffer is a Node.js subclass of Uint8Array; respect byteOffset for safety
|
|
20
|
+
const f32 = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
21
|
+
return Array.from(f32);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,iFAAiF;AAEjF,yFAAyF;AACzF,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAEpC,oFAAoF;AACpF,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAExC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAE5B,iFAAiF;AACjF,sEAAsE;AACtE,oEAAoE;AAEpE,MAAM,UAAU,eAAe,CAAC,MAAgB;IAC9C,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,4EAA4E;IAC5E,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IAC7E,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coolmem",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cross-Agent Memory Fabric β a privacy-first MCP server for shared AI memory with local vector embeddings.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"coolmem": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "node --loader ts-node/esm src/index.ts",
|
|
14
|
+
"db:push": "prisma db push",
|
|
15
|
+
"setup": "npm install && npx prisma db push && npm run build"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
19
|
+
"@prisma/client": "^5.22.0",
|
|
20
|
+
"@xenova/transformers": "^2.17.2",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"prisma": "^5.22.0",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
27
|
+
"typescript": "^5.7.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Prisma schema for the coolmem Cross-Agent Memory Fabric
|
|
2
|
+
// The "embedding" field stores a JSON-serialized float32 array
|
|
3
|
+
// produced by the all-MiniLM-L6-v2 model (384 dimensions).
|
|
4
|
+
|
|
5
|
+
generator client {
|
|
6
|
+
provider = "prisma-client-js"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
datasource db {
|
|
10
|
+
provider = "sqlite"
|
|
11
|
+
url = env("DATABASE_URL")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
model Memory {
|
|
15
|
+
id Int @id @default(autoincrement())
|
|
16
|
+
content String // The raw text that was "learned"
|
|
17
|
+
category String // e.g. "architecture", "bug-fix", "decision", "api-contract"
|
|
18
|
+
project String @default("default") // Scopes memories per project β prevents cross-project pollution
|
|
19
|
+
embedding Bytes // Float32Array as binary BLOB: 384 Γ 4 bytes = 1,536 bytes (vs ~4 KB as JSON)
|
|
20
|
+
createdAt DateTime @default(now())
|
|
21
|
+
}
|
package/seed.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seed.mjs β stores architectural decisions about coolmem into coolmem itself.
|
|
3
|
+
* Run once: node seed.mjs
|
|
4
|
+
*/
|
|
5
|
+
import { PrismaClient } from "@prisma/client";
|
|
6
|
+
import { pipeline, env } from "@xenova/transformers";
|
|
7
|
+
|
|
8
|
+
const prisma = new PrismaClient();
|
|
9
|
+
env.cacheDir = "./models";
|
|
10
|
+
|
|
11
|
+
function encodeEmbedding(vector) {
|
|
12
|
+
return Buffer.from(new Float32Array(vector).buffer);
|
|
13
|
+
}
|
|
14
|
+
function decodeEmbedding(buf) {
|
|
15
|
+
return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4));
|
|
16
|
+
}
|
|
17
|
+
function dotProduct(a, b) {
|
|
18
|
+
let dot = 0;
|
|
19
|
+
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
20
|
+
return dot;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEDUP = 0.88;
|
|
24
|
+
|
|
25
|
+
process.stdout.write("β³ Loading modelβ¦\n");
|
|
26
|
+
const extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
27
|
+
process.stdout.write("β
Model ready.\n\n");
|
|
28
|
+
|
|
29
|
+
async function embed(text) {
|
|
30
|
+
const out = await extractor(text, { pooling: "mean", normalize: true });
|
|
31
|
+
return Array.from(out.data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function store(content, category, project = "coolmem") {
|
|
35
|
+
const vec = await embed(content);
|
|
36
|
+
const existing = await prisma.memory.findMany({ where: { project }, select: { id: true, embedding: true } });
|
|
37
|
+
for (const m of existing) {
|
|
38
|
+
if (dotProduct(vec, decodeEmbedding(m.embedding)) >= DEDUP) {
|
|
39
|
+
await prisma.memory.update({ where: { id: m.id }, data: { content, category, embedding: encodeEmbedding(vec) } });
|
|
40
|
+
process.stdout.write(`β»οΈ Updated #${m.id} (duplicate)\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const r = await prisma.memory.create({ data: { content, category, project, embedding: encodeEmbedding(vec) } });
|
|
45
|
+
process.stdout.write(`β
Stored #${r.id} [${category}]\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await store(
|
|
49
|
+
"cosineSimilarity in similarity.ts is implemented as a pure dot product, not full cosine. This is correct and optimal because embeddings.ts always uses { normalize: true } with all-MiniLM-L6-v2, making every vector a unit vector (magnitude = 1). For unit vectors, cosine similarity = dot product. This saves ~60% of operations per comparison: removes 2 magnitude accumulators and 2 Math.sqrt calls across all 384 dimensions. If normalize is ever changed to false, full cosine must be restored.",
|
|
50
|
+
"architecture"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await store(
|
|
54
|
+
"Similarity search is brute-force O(n): all embeddings are loaded into RAM and scored per query. This is fine up to ~1,000 memories. Beyond that, switch to sqlite-vss (ANN index inside SQLite) or ChromaDB. Each embedding costs 1,536 bytes as Float32 BLOB.",
|
|
55
|
+
"architecture"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await store(
|
|
59
|
+
"DEDUP_THRESHOLD = 0.88 for store_memory deduplication. 0.95 was too strict (missed paraphrased rewrites of the same fact). 0.88 catches semantic duplicates without false-positives on genuinely different memories. MIN_SCORE_THRESHOLD = 0.30 for search (below this, results are irrelevant noise).",
|
|
60
|
+
"decision"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await store(
|
|
64
|
+
"all-MiniLM-L6-v2 (Xenova) is loaded lazily on first tool call and cached in ./models/. First-run downloads ~22 MB. It produces 384-dimensional L2-normalized float32 sentence embeddings. The pipeline instance is a module-level singleton β only one load per server process regardless of how many tool calls arrive.",
|
|
65
|
+
"architecture"
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await prisma.$disconnect();
|
|
69
|
+
process.stdout.write("\nπ coolmem seeded with its own architecture decisions.\n");
|
package/test.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test.mjs β Integration test for coolmem v2 (all optimizations)
|
|
3
|
+
* Run with: node test.mjs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PrismaClient } from "@prisma/client";
|
|
7
|
+
import { pipeline, env } from "@xenova/transformers";
|
|
8
|
+
|
|
9
|
+
// ββ color helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
10
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
11
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
12
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
13
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
14
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
15
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
16
|
+
const hr = (c = "β", n = 60) => c.repeat(n);
|
|
17
|
+
|
|
18
|
+
// ββ binary codec (mirrors src/index.ts) βββββββββββββββββββββββββββββββββββββββ
|
|
19
|
+
function encodeEmbedding(vector) {
|
|
20
|
+
return Buffer.from(new Float32Array(vector).buffer);
|
|
21
|
+
}
|
|
22
|
+
function decodeEmbedding(buf) {
|
|
23
|
+
return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4));
|
|
24
|
+
}
|
|
25
|
+
function cosineSimilarity(a, b) {
|
|
26
|
+
let dot = 0, magA = 0, magB = 0;
|
|
27
|
+
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; magA += a[i] * a[i]; magB += b[i] * b[i]; }
|
|
28
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ββ setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
32
|
+
const prisma = new PrismaClient();
|
|
33
|
+
env.cacheDir = "./models";
|
|
34
|
+
let extractor;
|
|
35
|
+
|
|
36
|
+
async function loadModel() {
|
|
37
|
+
process.stdout.write(yellow("β³ Loading modelβ¦\n"));
|
|
38
|
+
extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
39
|
+
process.stdout.write(green("β
Model ready.\n\n"));
|
|
40
|
+
}
|
|
41
|
+
async function embed(text) {
|
|
42
|
+
const out = await extractor(text, { pooling: "mean", normalize: true });
|
|
43
|
+
return Array.from(out.data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ββ tool simulations ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
47
|
+
const DEDUP_THRESHOLD = 0.95;
|
|
48
|
+
const MIN_SCORE = 0.30;
|
|
49
|
+
|
|
50
|
+
async function storeMemory(content, category, project = "default") {
|
|
51
|
+
const vector = await embed(content);
|
|
52
|
+
const existing = await prisma.memory.findMany({ where: { project }, select: { id: true, embedding: true } });
|
|
53
|
+
for (const m of existing) {
|
|
54
|
+
const score = cosineSimilarity(vector, decodeEmbedding(m.embedding));
|
|
55
|
+
if (score >= DEDUP_THRESHOLD) {
|
|
56
|
+
await prisma.memory.update({ where: { id: m.id }, data: { content, category, embedding: encodeEmbedding(vector) } });
|
|
57
|
+
return { id: m.id, duplicate: true, score };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const r = await prisma.memory.create({ data: { content, category, project, embedding: encodeEmbedding(vector) } });
|
|
61
|
+
return { id: r.id, duplicate: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function searchMemories(query, project, limit = 3) {
|
|
65
|
+
const qVec = await embed(query);
|
|
66
|
+
const all = await prisma.memory.findMany({
|
|
67
|
+
where: project ? { project } : undefined,
|
|
68
|
+
select: { id: true, content: true, category: true, project: true, embedding: true }
|
|
69
|
+
});
|
|
70
|
+
return all
|
|
71
|
+
.map(m => ({ ...m, score: cosineSimilarity(qVec, decodeEmbedding(m.embedding)) }))
|
|
72
|
+
.filter(m => m.score >= MIN_SCORE)
|
|
73
|
+
.sort((a, b) => b.score - a.score)
|
|
74
|
+
.slice(0, limit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function deleteMemory(id) {
|
|
78
|
+
return prisma.memory.delete({ where: { id } });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function updateMemory(id, content, category) {
|
|
82
|
+
const newEmbed = content ? encodeEmbedding(await embed(content)) : undefined;
|
|
83
|
+
return prisma.memory.update({
|
|
84
|
+
where: { id }, data: {
|
|
85
|
+
...(content && { content }), ...(category && { category }), ...(newEmbed && { embedding: newEmbed }),
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ββ test runner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
91
|
+
async function main() {
|
|
92
|
+
console.log("\n" + bold("π§ coolmem v2 β Integration Test"));
|
|
93
|
+
console.log(hr("β") + "\n");
|
|
94
|
+
await loadModel();
|
|
95
|
+
|
|
96
|
+
// PHASE 1 β Store (two projects)
|
|
97
|
+
console.log(bold("PHASE 1 β store_memory (with project scoping)"));
|
|
98
|
+
console.log(hr() + "\n");
|
|
99
|
+
const toStore = [
|
|
100
|
+
{ content: "We use SQLite with Prisma ORM. Embeddings stored as binary BLOBs (Float32Array).", category: "architecture", project: "coolmem" },
|
|
101
|
+
{ content: "all-MiniLM-L6-v2 runs locally on CPU: 22 MB, 384-dim, zero API cost.", category: "decision", project: "coolmem" },
|
|
102
|
+
{ content: "MCP server uses stdio transport β never write to stdout or you corrupt the protocol.", category: "architecture", project: "coolmem" },
|
|
103
|
+
{ content: "The admin dashboard uses MongoDB with Next.js API routes for the revenue KPI endpoint.", category: "architecture", project: "admin-dashboard" },
|
|
104
|
+
];
|
|
105
|
+
for (const m of toStore) {
|
|
106
|
+
const r = await storeMemory(m.content, m.category, m.project);
|
|
107
|
+
const tag = r.duplicate ? yellow(`β»οΈ Deduplicated β updated #${r.id} (score ${r.score?.toFixed(4)})`) : green(`β
Stored as #${r.id}`);
|
|
108
|
+
console.log(` [${m.project}] ${tag}`);
|
|
109
|
+
console.log(dim(` "${m.content.slice(0, 80)}β¦"\n`));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// PHASE 2 β Deduplication
|
|
113
|
+
console.log(bold("PHASE 2 β Deduplication (store near-identical content)"));
|
|
114
|
+
console.log(hr() + "\n");
|
|
115
|
+
const dupResult = await storeMemory(
|
|
116
|
+
"all-MiniLM-L6-v2 runs on CPU locally: 22 MB model, produces 384-dimensional embeddings, no API key needed.",
|
|
117
|
+
"decision", "coolmem"
|
|
118
|
+
);
|
|
119
|
+
if (dupResult.duplicate) {
|
|
120
|
+
console.log(yellow(` β»οΈ Correctly detected duplicate! Updated #${dupResult.id} (score: ${dupResult.score.toFixed(4)})\n`));
|
|
121
|
+
} else {
|
|
122
|
+
console.log(red(` β Duplicate NOT detected β threshold may need tuning.\n`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// PHASE 3 β Search (project-scoped + score threshold)
|
|
126
|
+
console.log(bold("PHASE 3 β search_memories (scoped + score threshold 0.30)"));
|
|
127
|
+
console.log(hr() + "\n");
|
|
128
|
+
const queries = [
|
|
129
|
+
{ q: "how are embeddings stored?", project: "coolmem" },
|
|
130
|
+
{ q: "what AI model do we use?", project: "coolmem" },
|
|
131
|
+
{ q: "what database does admin dashboard use?", project: "admin-dashboard" },
|
|
132
|
+
{ q: "quantum physics lecture notes", project: undefined }, // should return nothing
|
|
133
|
+
];
|
|
134
|
+
for (const { q, project } of queries) {
|
|
135
|
+
console.log(cyan(` π "${q}"${project ? ` [project: ${project}]` : " [all projects]"}`));
|
|
136
|
+
const results = await searchMemories(q, project);
|
|
137
|
+
if (results.length === 0) {
|
|
138
|
+
console.log(dim(` β No results above threshold (correct for irrelevant query)\n`));
|
|
139
|
+
} else {
|
|
140
|
+
results.forEach((r, i) =>
|
|
141
|
+
console.log(` #${i + 1} score=${r.score.toFixed(4)} [${r.project}/${r.category}]\n ${dim(r.content.slice(0, 90) + "β¦")}`)
|
|
142
|
+
);
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// PHASE 4 β Update
|
|
148
|
+
console.log(bold("PHASE 4 β update_memory"));
|
|
149
|
+
console.log(hr() + "\n");
|
|
150
|
+
const all = await prisma.memory.findMany({ take: 1, orderBy: { id: "asc" }, select: { id: true, content: true } });
|
|
151
|
+
if (all.length > 0) {
|
|
152
|
+
const { id } = all[0];
|
|
153
|
+
await updateMemory(id, undefined, "architecture-v2"); // only category changed
|
|
154
|
+
console.log(green(` β
Updated category of Memory #${id} to "architecture-v2" (no re-embed needed)\n`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// PHASE 5 β Delete
|
|
158
|
+
console.log(bold("PHASE 5 β delete_memory"));
|
|
159
|
+
console.log(hr() + "\n");
|
|
160
|
+
const last = await prisma.memory.findFirst({ orderBy: { id: "desc" }, select: { id: true } });
|
|
161
|
+
if (last) {
|
|
162
|
+
await deleteMemory(last.id);
|
|
163
|
+
console.log(green(` β
Deleted Memory #${last.id}\n`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// PHASE 6 β Timeline
|
|
167
|
+
console.log(bold("PHASE 6 β project_timeline resource"));
|
|
168
|
+
console.log(hr() + "\n");
|
|
169
|
+
const timeline = await prisma.memory.findMany({
|
|
170
|
+
orderBy: { createdAt: "desc" }, take: 10,
|
|
171
|
+
select: { id: true, content: true, category: true, project: true, createdAt: true },
|
|
172
|
+
});
|
|
173
|
+
for (const m of timeline) {
|
|
174
|
+
const ts = m.createdAt.toISOString().replace("T", " ").slice(0, 19);
|
|
175
|
+
console.log(` [${ts}] #${m.id} | ${m.project} | ${m.category}`);
|
|
176
|
+
console.log(dim(` ${m.content.slice(0, 90)}β¦\n`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(hr("β"));
|
|
180
|
+
console.log(green(bold("π All phases passed!")));
|
|
181
|
+
|
|
182
|
+
await prisma.$disconnect();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
main().catch(e => { console.error(red("FATAL:"), e); prisma.$disconnect(); process.exit(1); });
|