@aeriondyseti/vector-memory-mcp 1.0.2-dev.0 → 1.1.0-dev.2
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/dist/package.json +4 -4
- package/dist/src/config/index.d.ts +10 -0
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +13 -0
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/db/conversation-history.repository.d.ts +24 -0
- package/dist/src/db/conversation-history.repository.d.ts.map +1 -0
- package/dist/src/db/conversation-history.repository.js +184 -0
- package/dist/src/db/conversation-history.repository.js.map +1 -0
- package/dist/src/db/conversation-history.schema.d.ts +10 -0
- package/dist/src/db/conversation-history.schema.d.ts.map +1 -0
- package/dist/src/db/conversation-history.schema.js +31 -0
- package/dist/src/db/conversation-history.schema.js.map +1 -0
- package/dist/src/db/lancedb-utils.d.ts +35 -0
- package/dist/src/db/lancedb-utils.d.ts.map +1 -0
- package/dist/src/db/lancedb-utils.js +77 -0
- package/dist/src/db/lancedb-utils.js.map +1 -0
- package/dist/src/db/memory.repository.d.ts +2 -12
- package/dist/src/db/memory.repository.d.ts.map +1 -1
- package/dist/src/db/memory.repository.js +13 -56
- package/dist/src/db/memory.repository.js.map +1 -1
- package/dist/src/db/schema.d.ts +4 -1
- package/dist/src/db/schema.d.ts.map +1 -1
- package/dist/src/db/schema.js +8 -4
- package/dist/src/db/schema.js.map +1 -1
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/handlers.d.ts +3 -0
- package/dist/src/mcp/handlers.d.ts.map +1 -1
- package/dist/src/mcp/handlers.js +149 -17
- package/dist/src/mcp/handlers.js.map +1 -1
- package/dist/src/mcp/tools.d.ts +3 -0
- package/dist/src/mcp/tools.d.ts.map +1 -1
- package/dist/src/mcp/tools.js +54 -3
- package/dist/src/mcp/tools.js.map +1 -1
- package/dist/src/services/conversation-history.service.d.ts +64 -0
- package/dist/src/services/conversation-history.service.d.ts.map +1 -0
- package/dist/src/services/conversation-history.service.js +244 -0
- package/dist/src/services/conversation-history.service.js.map +1 -0
- package/dist/src/services/memory.service.d.ts +24 -0
- package/dist/src/services/memory.service.d.ts.map +1 -1
- package/dist/src/services/memory.service.js +58 -0
- package/dist/src/services/memory.service.js.map +1 -1
- package/dist/src/services/session-parser.d.ts +59 -0
- package/dist/src/services/session-parser.d.ts.map +1 -0
- package/dist/src/services/session-parser.js +147 -0
- package/dist/src/services/session-parser.js.map +1 -0
- package/dist/src/types/conversation-history.d.ts +74 -0
- package/dist/src/types/conversation-history.d.ts.map +1 -0
- package/dist/src/types/conversation-history.js +2 -0
- package/dist/src/types/conversation-history.js.map +1 -0
- package/dist/src/types/memory.d.ts +4 -2
- package/dist/src/types/memory.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/config/index.ts +23 -0
- package/src/db/conversation-history.repository.ts +255 -0
- package/src/db/conversation-history.schema.ts +40 -0
- package/src/db/lancedb-utils.ts +97 -0
- package/src/db/memory.repository.ts +18 -67
- package/src/db/schema.ts +17 -21
- package/src/index.ts +16 -0
- package/src/mcp/handlers.ts +178 -22
- package/src/mcp/tools.ts +66 -3
- package/src/services/conversation-history.service.ts +320 -0
- package/src/services/memory.service.ts +74 -0
- package/src/services/session-parser.ts +232 -0
- package/src/types/conversation-history.ts +82 -0
- package/src/types/memory.ts +4 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
2
|
+
import { Index, rerankers, type Table } from "@lancedb/lancedb";
|
|
3
|
+
import type { Schema } from "apache-arrow";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escapes a string for use in LanceDB/DataFusion SQL WHERE clauses.
|
|
7
|
+
* Doubles single quotes to prevent SQL injection (standard SQL escaping).
|
|
8
|
+
*/
|
|
9
|
+
export function escapeLanceDbString(value: string): string {
|
|
10
|
+
return value.replace(/'/g, "''");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts LanceDB's Arrow Vector type to a plain number[].
|
|
15
|
+
* LanceDB returns an Arrow Vector object which is iterable but not an array.
|
|
16
|
+
*/
|
|
17
|
+
export function arrowVectorToArray(value: unknown): number[] {
|
|
18
|
+
return Array.isArray(value)
|
|
19
|
+
? value
|
|
20
|
+
: (Array.from(value as Iterable<number>) as number[]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Opens an existing table or creates it with the given schema.
|
|
25
|
+
* Does NOT cache — callers should cache the returned Table if desired.
|
|
26
|
+
*/
|
|
27
|
+
export async function getOrCreateTable(
|
|
28
|
+
db: lancedb.Connection,
|
|
29
|
+
name: string,
|
|
30
|
+
schema: Schema
|
|
31
|
+
): Promise<Table> {
|
|
32
|
+
const names = await db.tableNames();
|
|
33
|
+
if (names.includes(name)) {
|
|
34
|
+
return await db.openTable(name);
|
|
35
|
+
}
|
|
36
|
+
return await db.createTable(name, [], { schema });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a mutex-guarded function that ensures an FTS index exists on a table's content column.
|
|
41
|
+
*
|
|
42
|
+
* Once the FTS index is confirmed/created, the promise is retained for the lifetime of the
|
|
43
|
+
* caller — the index persists in LanceDB, so re-checking is unnecessary. On error, the
|
|
44
|
+
* mutex resets so the next call can retry.
|
|
45
|
+
*
|
|
46
|
+
* The key design constraint: the promise must be captured synchronously (before any await)
|
|
47
|
+
* to prevent concurrent callers from racing past the guard.
|
|
48
|
+
*/
|
|
49
|
+
export function createFtsMutex(
|
|
50
|
+
getTable: () => Promise<Table>
|
|
51
|
+
): () => Promise<void> {
|
|
52
|
+
let promise: Promise<void> | null = null;
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
if (promise) return promise;
|
|
56
|
+
|
|
57
|
+
promise = (async () => {
|
|
58
|
+
const table = await getTable();
|
|
59
|
+
const indices = await table.listIndices();
|
|
60
|
+
const hasFtsIndex = indices.some(
|
|
61
|
+
(idx) => idx.columns.includes("content") && idx.indexType === "FTS"
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!hasFtsIndex) {
|
|
65
|
+
await table.createIndex("content", {
|
|
66
|
+
config: Index.fts(),
|
|
67
|
+
});
|
|
68
|
+
await table.waitForIndex(["content_idx"], 30);
|
|
69
|
+
}
|
|
70
|
+
})().catch((error) => {
|
|
71
|
+
promise = null;
|
|
72
|
+
throw error;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return promise;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a promise-mutex for RRFReranker instantiation.
|
|
81
|
+
* Same pattern as createFtsMutex: create once, cache forever, reset on error.
|
|
82
|
+
*/
|
|
83
|
+
export function createRerankerMutex(
|
|
84
|
+
k: number = 60
|
|
85
|
+
): () => Promise<rerankers.RRFReranker> {
|
|
86
|
+
let promise: Promise<rerankers.RRFReranker> | null = null;
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
if (!promise) {
|
|
90
|
+
promise = rerankers.RRFReranker.create(k).catch((e) => {
|
|
91
|
+
promise = null;
|
|
92
|
+
throw e;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return promise;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as lancedb from "@lancedb/lancedb";
|
|
2
|
-
import {
|
|
2
|
+
import { type Table } from "@lancedb/lancedb";
|
|
3
3
|
import { TABLE_NAME, memorySchema } from "./schema.js";
|
|
4
|
+
import { arrowVectorToArray, createFtsMutex, createRerankerMutex, escapeLanceDbString } from "./lancedb-utils.js";
|
|
4
5
|
import {
|
|
5
6
|
type Memory,
|
|
6
7
|
type HybridRow,
|
|
@@ -8,14 +9,18 @@ import {
|
|
|
8
9
|
} from "../types/memory.js";
|
|
9
10
|
|
|
10
11
|
export class MemoryRepository {
|
|
11
|
-
// Mutex for FTS index creation - ensures only one index creation runs at a time
|
|
12
|
-
// Once set, this promise is never cleared (FTS index persists in the database)
|
|
13
|
-
private ftsIndexPromise: Promise<void> | null = null;
|
|
14
|
-
|
|
15
12
|
// Mutex for schema migration - runs once per instance to add missing columns
|
|
16
13
|
private migrationPromise: Promise<void> | null = null;
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
// FTS index mutex — once created, the promise is never cleared (index persists in LanceDB)
|
|
16
|
+
private ensureFtsIndex: () => Promise<void>;
|
|
17
|
+
|
|
18
|
+
// Cached reranker — k=60 is constant, no need to recreate per search
|
|
19
|
+
private getReranker = createRerankerMutex();
|
|
20
|
+
|
|
21
|
+
constructor(private db: lancedb.Connection) {
|
|
22
|
+
this.ensureFtsIndex = createFtsMutex(() => this.getTable());
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
private async getTable() {
|
|
21
26
|
const names = await this.db.tableNames();
|
|
@@ -70,63 +75,14 @@ export class MemoryRepository {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
/**
|
|
74
|
-
* Ensures the FTS index exists on the content column.
|
|
75
|
-
* Uses a mutex pattern to prevent concurrent index creation.
|
|
76
|
-
* The key insight: we must capture the promise BEFORE any await.
|
|
77
|
-
*/
|
|
78
|
-
private ensureFtsIndex(): Promise<void> {
|
|
79
|
-
// If there's already a pending or completed index creation, return that promise
|
|
80
|
-
if (this.ftsIndexPromise) {
|
|
81
|
-
return this.ftsIndexPromise;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Synchronously set the promise BEFORE any await
|
|
85
|
-
// This is critical for proper mutex behavior in JS async code
|
|
86
|
-
this.ftsIndexPromise = this.createFtsIndexIfNeeded().catch((error) => {
|
|
87
|
-
// Reset on error so the next call can retry
|
|
88
|
-
this.ftsIndexPromise = null;
|
|
89
|
-
throw error;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return this.ftsIndexPromise;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Creates the FTS index if it doesn't already exist.
|
|
97
|
-
* Gets its own table reference to ensure consistent index state.
|
|
98
|
-
*/
|
|
99
|
-
private async createFtsIndexIfNeeded(): Promise<void> {
|
|
100
|
-
const table = await this.getTable();
|
|
101
|
-
const indices = await table.listIndices();
|
|
102
|
-
const hasFtsIndex = indices.some(
|
|
103
|
-
(idx) => idx.columns.includes("content") && idx.indexType === "FTS"
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
if (!hasFtsIndex) {
|
|
107
|
-
await table.createIndex("content", {
|
|
108
|
-
config: Index.fts(),
|
|
109
|
-
});
|
|
110
|
-
// Wait for the index to be fully created and available
|
|
111
|
-
await table.waitForIndex(["content_idx"], 30);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
78
|
/**
|
|
116
79
|
* Converts a raw LanceDB row to a Memory object.
|
|
117
80
|
*/
|
|
118
81
|
private rowToMemory(row: Record<string, unknown>): Memory {
|
|
119
|
-
// Handle Arrow Vector type conversion
|
|
120
|
-
// LanceDB returns an Arrow Vector object which is iterable but not an array
|
|
121
|
-
const vectorData = row.vector as unknown;
|
|
122
|
-
const embedding = Array.isArray(vectorData)
|
|
123
|
-
? vectorData
|
|
124
|
-
: Array.from(vectorData as Iterable<number>) as number[];
|
|
125
|
-
|
|
126
82
|
return {
|
|
127
83
|
id: row.id as string,
|
|
128
84
|
content: row.content as string,
|
|
129
|
-
embedding,
|
|
85
|
+
embedding: arrowVectorToArray(row.vector),
|
|
130
86
|
metadata: JSON.parse(row.metadata as string),
|
|
131
87
|
createdAt: new Date(row.created_at as number),
|
|
132
88
|
updatedAt: new Date(row.updated_at as number),
|
|
@@ -159,14 +115,14 @@ export class MemoryRepository {
|
|
|
159
115
|
|
|
160
116
|
async upsert(memory: Memory): Promise<void> {
|
|
161
117
|
const table = await this.getTable();
|
|
162
|
-
const existing = await table.query().where(`id = '${memory.id}'`).limit(1).toArray();
|
|
118
|
+
const existing = await table.query().where(`id = '${escapeLanceDbString(memory.id)}'`).limit(1).toArray();
|
|
163
119
|
|
|
164
120
|
if (existing.length === 0) {
|
|
165
121
|
return await this.insert(memory);
|
|
166
122
|
}
|
|
167
123
|
|
|
168
124
|
await table.update({
|
|
169
|
-
where: `id = '${memory.id}'`,
|
|
125
|
+
where: `id = '${escapeLanceDbString(memory.id)}'`,
|
|
170
126
|
values: {
|
|
171
127
|
vector: memory.embedding,
|
|
172
128
|
content: memory.content,
|
|
@@ -183,7 +139,7 @@ export class MemoryRepository {
|
|
|
183
139
|
|
|
184
140
|
async findById(id: string): Promise<Memory | null> {
|
|
185
141
|
const table = await this.getTable();
|
|
186
|
-
const results = await table.query().where(`id = '${id}'`).limit(1).toArray();
|
|
142
|
+
const results = await table.query().where(`id = '${escapeLanceDbString(id)}'`).limit(1).toArray();
|
|
187
143
|
|
|
188
144
|
if (results.length === 0) {
|
|
189
145
|
return null;
|
|
@@ -196,14 +152,14 @@ export class MemoryRepository {
|
|
|
196
152
|
const table = await this.getTable();
|
|
197
153
|
|
|
198
154
|
// Verify existence first to match previous behavior (return false if not found)
|
|
199
|
-
const existing = await table.query().where(`id = '${id}'`).limit(1).toArray();
|
|
155
|
+
const existing = await table.query().where(`id = '${escapeLanceDbString(id)}'`).limit(1).toArray();
|
|
200
156
|
if (existing.length === 0) {
|
|
201
157
|
return false;
|
|
202
158
|
}
|
|
203
159
|
|
|
204
160
|
const now = Date.now();
|
|
205
161
|
await table.update({
|
|
206
|
-
where: `id = '${id}'`,
|
|
162
|
+
where: `id = '${escapeLanceDbString(id)}'`,
|
|
207
163
|
values: {
|
|
208
164
|
superseded_by: DELETED_TOMBSTONE,
|
|
209
165
|
updated_at: now,
|
|
@@ -223,16 +179,11 @@ export class MemoryRepository {
|
|
|
223
179
|
* @returns Array of HybridRow containing full Memory data plus RRF score
|
|
224
180
|
*/
|
|
225
181
|
async findHybrid(embedding: number[], query: string, limit: number): Promise<HybridRow[]> {
|
|
226
|
-
// Ensure FTS index exists (with mutex to prevent concurrent creation)
|
|
227
|
-
// This must happen BEFORE getTable to ensure proper mutex behavior
|
|
228
182
|
await this.ensureFtsIndex();
|
|
229
183
|
|
|
230
184
|
const table = await this.getTable();
|
|
185
|
+
const reranker = await this.getReranker();
|
|
231
186
|
|
|
232
|
-
// Create RRF reranker with k=60 (standard parameter)
|
|
233
|
-
const reranker = await rerankers.RRFReranker.create(60);
|
|
234
|
-
|
|
235
|
-
// Perform hybrid search: combine vector search and full-text search
|
|
236
187
|
const results = await table
|
|
237
188
|
.query()
|
|
238
189
|
.nearestTo(embedding)
|
package/src/db/schema.ts
CHANGED
|
@@ -9,34 +9,30 @@ import {
|
|
|
9
9
|
Int32,
|
|
10
10
|
} from "apache-arrow";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Shared field helpers — used by both memory and conversation history schemas
|
|
13
|
+
export const EMBEDDING_DIMENSION = 384;
|
|
13
14
|
|
|
14
|
-
export const
|
|
15
|
-
new Field("id", new Utf8(), false),
|
|
15
|
+
export const vectorField = () =>
|
|
16
16
|
new Field(
|
|
17
17
|
"vector",
|
|
18
|
-
new FixedSizeList(
|
|
18
|
+
new FixedSizeList(EMBEDDING_DIMENSION, new Field("item", new Float32())),
|
|
19
19
|
false
|
|
20
|
-
)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const timestampField = (name: string, nullable = false) =>
|
|
23
|
+
new Field(name, new Timestamp(TimeUnit.MILLISECOND), nullable);
|
|
24
|
+
|
|
25
|
+
export const TABLE_NAME = "memories";
|
|
26
|
+
|
|
27
|
+
export const memorySchema = new Schema([
|
|
28
|
+
new Field("id", new Utf8(), false),
|
|
29
|
+
vectorField(),
|
|
21
30
|
new Field("content", new Utf8(), false),
|
|
22
31
|
new Field("metadata", new Utf8(), false), // JSON string
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
new Timestamp(TimeUnit.MILLISECOND),
|
|
26
|
-
false
|
|
27
|
-
),
|
|
28
|
-
new Field(
|
|
29
|
-
"updated_at",
|
|
30
|
-
new Timestamp(TimeUnit.MILLISECOND),
|
|
31
|
-
false
|
|
32
|
-
),
|
|
32
|
+
timestampField("created_at"),
|
|
33
|
+
timestampField("updated_at"),
|
|
33
34
|
new Field("superseded_by", new Utf8(), true), // Nullable
|
|
34
35
|
new Field("usefulness", new Float32(), false),
|
|
35
36
|
new Field("access_count", new Int32(), false),
|
|
36
|
-
|
|
37
|
-
"last_accessed",
|
|
38
|
-
new Timestamp(TimeUnit.MILLISECOND),
|
|
39
|
-
true
|
|
40
|
-
),
|
|
37
|
+
timestampField("last_accessed", true),
|
|
41
38
|
]);
|
|
42
|
-
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { connectToDatabase } from "./db/connection.js";
|
|
|
5
5
|
import { MemoryRepository } from "./db/memory.repository.js";
|
|
6
6
|
import { EmbeddingsService } from "./services/embeddings.service.js";
|
|
7
7
|
import { MemoryService } from "./services/memory.service.js";
|
|
8
|
+
import { ConversationHistoryRepository } from "./db/conversation-history.repository.js";
|
|
9
|
+
import { ConversationHistoryService } from "./services/conversation-history.service.js";
|
|
8
10
|
import { startServer } from "./mcp/server.js";
|
|
9
11
|
import { startHttpServer } from "./http/server.js";
|
|
10
12
|
|
|
@@ -30,6 +32,20 @@ async function main(): Promise<void> {
|
|
|
30
32
|
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
31
33
|
const memoryService = new MemoryService(repository, embeddings);
|
|
32
34
|
|
|
35
|
+
// Wire conversation history if enabled
|
|
36
|
+
if (config.conversationHistory.enabled) {
|
|
37
|
+
const historyRepo = new ConversationHistoryRepository(db);
|
|
38
|
+
const historyService = new ConversationHistoryService(
|
|
39
|
+
historyRepo,
|
|
40
|
+
embeddings,
|
|
41
|
+
config.conversationHistory.sessionPath,
|
|
42
|
+
);
|
|
43
|
+
memoryService.setConversationHistory(
|
|
44
|
+
historyService,
|
|
45
|
+
config.conversationHistory.historyWeight,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
// Track cleanup functions
|
|
34
50
|
let httpStop: (() => void) | null = null;
|
|
35
51
|
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type { MemoryService } from "../services/memory.service.js";
|
|
3
|
+
import type { ConversationHistoryService } from "../services/conversation-history.service.js";
|
|
3
4
|
import type { SearchIntent } from "../types/memory.js";
|
|
5
|
+
import type { SearchResult } from "../types/conversation-history.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Guard: returns the ConversationHistoryService or an error CallToolResult.
|
|
9
|
+
*/
|
|
10
|
+
function requireHistoryService(
|
|
11
|
+
service: MemoryService,
|
|
12
|
+
): ConversationHistoryService | CallToolResult {
|
|
13
|
+
const historyService = service.getConversationHistory();
|
|
14
|
+
if (!historyService) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: "Conversation history indexing is not enabled. Set conversationHistory.enabled = true in config.",
|
|
19
|
+
}],
|
|
20
|
+
isError: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return historyService;
|
|
24
|
+
}
|
|
4
25
|
|
|
5
26
|
export async function handleStoreMemories(
|
|
6
27
|
args: Record<string, unknown> | undefined,
|
|
@@ -103,9 +124,50 @@ export async function handleSearchMemories(
|
|
|
103
124
|
): Promise<CallToolResult> {
|
|
104
125
|
const query = args?.query as string;
|
|
105
126
|
const intent = (args?.intent as SearchIntent) ?? "fact_check";
|
|
106
|
-
const _reasonForSearch = args?.reason_for_search as string; // Logged but not used in logic
|
|
107
127
|
const limit = (args?.limit as number) ?? 10;
|
|
108
128
|
const includeDeleted = (args?.include_deleted as boolean) ?? false;
|
|
129
|
+
const includeHistory = (args?.include_history as boolean) ?? false;
|
|
130
|
+
const historyOnly = (args?.history_only as boolean) ?? false;
|
|
131
|
+
|
|
132
|
+
if (includeHistory && historyOnly) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: "Cannot set both include_history and history_only to true. Use history_only for conversation history only, or include_history to merge with memories.",
|
|
137
|
+
}],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// History-only: search only conversation history via the history service
|
|
143
|
+
if (historyOnly) {
|
|
144
|
+
const result = requireHistoryService(service);
|
|
145
|
+
if ("content" in result) return result;
|
|
146
|
+
const historyResults = await result.search(query, limit);
|
|
147
|
+
if (historyResults.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: "No conversation history found matching your query." }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: historyResults.map((r) => formatSearchResult(r)).join("\n\n---\n\n") }],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Unified search: merge memories + history
|
|
158
|
+
if (includeHistory) {
|
|
159
|
+
const results = await service.searchUnified(query, intent, limit, includeDeleted);
|
|
160
|
+
if (results.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: "No results found matching your query." }],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: results.map((r) => formatSearchResult(r, includeDeleted)).join("\n\n---\n\n") }],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Default: memory-only search (original behavior)
|
|
109
171
|
const memories = await service.search(query, intent, limit, includeDeleted);
|
|
110
172
|
|
|
111
173
|
if (memories.length === 0) {
|
|
@@ -130,36 +192,61 @@ export async function handleSearchMemories(
|
|
|
130
192
|
};
|
|
131
193
|
}
|
|
132
194
|
|
|
195
|
+
function formatMemoryDetail(
|
|
196
|
+
memoryId: string,
|
|
197
|
+
memory: Awaited<ReturnType<MemoryService["get"]>>
|
|
198
|
+
): string {
|
|
199
|
+
if (!memory) {
|
|
200
|
+
return `Memory ${memoryId} not found`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let result = `ID: ${memory.id}\nContent: ${memory.content}`;
|
|
204
|
+
if (Object.keys(memory.metadata).length > 0) {
|
|
205
|
+
result += `\nMetadata: ${JSON.stringify(memory.metadata)}`;
|
|
206
|
+
}
|
|
207
|
+
result += `\nCreated: ${memory.createdAt.toISOString()}`;
|
|
208
|
+
result += `\nUpdated: ${memory.updatedAt.toISOString()}`;
|
|
209
|
+
if (memory.supersededBy) {
|
|
210
|
+
result += `\nSuperseded by: ${memory.supersededBy}`;
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Format a unified SearchResult (memory or history) for display.
|
|
217
|
+
* TODO: The default memory-only search path in handleSearchMemories formats results inline
|
|
218
|
+
* with similar logic but without the "Source:" label. Consolidating would add "Source: memory"
|
|
219
|
+
* to existing output, which may break consumers that parse it. (#5)
|
|
220
|
+
*/
|
|
221
|
+
function formatSearchResult(result: SearchResult, includeDeleted: boolean = false): string {
|
|
222
|
+
if (result.source === "memory") {
|
|
223
|
+
let text = `ID: ${result.id}\nSource: memory\nContent: ${result.content}`;
|
|
224
|
+
if (Object.keys(result.metadata).length > 0) {
|
|
225
|
+
text += `\nMetadata: ${JSON.stringify(result.metadata)}`;
|
|
226
|
+
}
|
|
227
|
+
if (includeDeleted && result.supersededBy) {
|
|
228
|
+
text += `\n[DELETED]`;
|
|
229
|
+
}
|
|
230
|
+
return text;
|
|
231
|
+
}
|
|
232
|
+
// conversation_history
|
|
233
|
+
let text = `ID: ${result.id}\nSource: conversation_history\nSession: ${result.sessionId}\nRole: ${result.role}\nTimestamp: ${result.timestamp.toISOString()}\nContent: ${result.content}`;
|
|
234
|
+
if (Object.keys(result.metadata).length > 0) {
|
|
235
|
+
text += `\nMetadata: ${JSON.stringify(result.metadata)}`;
|
|
236
|
+
}
|
|
237
|
+
return text;
|
|
238
|
+
}
|
|
239
|
+
|
|
133
240
|
export async function handleGetMemories(
|
|
134
241
|
args: Record<string, unknown> | undefined,
|
|
135
242
|
service: MemoryService
|
|
136
243
|
): Promise<CallToolResult> {
|
|
137
244
|
const ids = args?.ids as string[];
|
|
138
245
|
|
|
139
|
-
const format = (
|
|
140
|
-
memoryId: string,
|
|
141
|
-
memory: Awaited<ReturnType<MemoryService["get"]>>
|
|
142
|
-
) => {
|
|
143
|
-
if (!memory) {
|
|
144
|
-
return `Memory ${memoryId} not found`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let result = `ID: ${memory.id}\nContent: ${memory.content}`;
|
|
148
|
-
if (Object.keys(memory.metadata).length > 0) {
|
|
149
|
-
result += `\nMetadata: ${JSON.stringify(memory.metadata)}`;
|
|
150
|
-
}
|
|
151
|
-
result += `\nCreated: ${memory.createdAt.toISOString()}`;
|
|
152
|
-
result += `\nUpdated: ${memory.updatedAt.toISOString()}`;
|
|
153
|
-
if (memory.supersededBy) {
|
|
154
|
-
result += `\nSuperseded by: ${memory.supersededBy}`;
|
|
155
|
-
}
|
|
156
|
-
return result;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
246
|
const blocks: string[] = [];
|
|
160
247
|
for (const id of ids) {
|
|
161
248
|
const memory = await service.get(id);
|
|
162
|
-
blocks.push(
|
|
249
|
+
blocks.push(formatMemoryDetail(id, memory));
|
|
163
250
|
}
|
|
164
251
|
|
|
165
252
|
return {
|
|
@@ -248,6 +335,69 @@ export async function handleGetCheckpoint(
|
|
|
248
335
|
};
|
|
249
336
|
}
|
|
250
337
|
|
|
338
|
+
export async function handleIndexConversations(
|
|
339
|
+
args: Record<string, unknown> | undefined,
|
|
340
|
+
service: MemoryService
|
|
341
|
+
): Promise<CallToolResult> {
|
|
342
|
+
const result = requireHistoryService(service);
|
|
343
|
+
if ("content" in result) return result;
|
|
344
|
+
|
|
345
|
+
const path = args?.path as string | undefined;
|
|
346
|
+
const summary = await result.indexConversations(path);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
content: [{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: `Indexing complete.\n- Sessions discovered: ${summary.sessionsDiscovered}\n- Sessions indexed: ${summary.sessionsIndexed}\n- Sessions skipped (unchanged): ${summary.sessionsSkipped}\n- Messages indexed: ${summary.messagesIndexed}`,
|
|
352
|
+
}],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function handleListIndexedSessions(
|
|
357
|
+
_args: Record<string, unknown> | undefined,
|
|
358
|
+
service: MemoryService
|
|
359
|
+
): Promise<CallToolResult> {
|
|
360
|
+
const result = requireHistoryService(service);
|
|
361
|
+
if ("content" in result) return result;
|
|
362
|
+
|
|
363
|
+
const sessions = await result.listIndexedSessions();
|
|
364
|
+
|
|
365
|
+
if (sessions.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
content: [{ type: "text", text: "No indexed sessions found. Run index_conversations first." }],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lines = sessions.map((s) => {
|
|
372
|
+
let line = `Session: ${s.sessionId}\n Messages: ${s.messageCount}\n First: ${s.firstMessageAt.toISOString()}\n Last: ${s.lastMessageAt.toISOString()}\n Indexed: ${s.indexedAt.toISOString()}`;
|
|
373
|
+
if (s.project) line += `\n Project: ${s.project}`;
|
|
374
|
+
if (s.gitBranch) line += `\n Branch: ${s.gitBranch}`;
|
|
375
|
+
return line;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: `${sessions.length} indexed session(s):\n\n${lines.join("\n\n")}` }],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function handleReindexSession(
|
|
384
|
+
args: Record<string, unknown> | undefined,
|
|
385
|
+
service: MemoryService
|
|
386
|
+
): Promise<CallToolResult> {
|
|
387
|
+
const result = requireHistoryService(service);
|
|
388
|
+
if ("content" in result) return result;
|
|
389
|
+
|
|
390
|
+
const sessionId = args?.session_id as string;
|
|
391
|
+
const summary = await result.reindexSession(sessionId);
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
content: [{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: `Reindex complete for session ${sessionId}.\n- Messages indexed: ${summary.messagesIndexed}`,
|
|
397
|
+
}],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
251
401
|
export async function handleToolCall(
|
|
252
402
|
name: string,
|
|
253
403
|
args: Record<string, unknown> | undefined,
|
|
@@ -270,6 +420,12 @@ export async function handleToolCall(
|
|
|
270
420
|
return handleStoreCheckpoint(args, service);
|
|
271
421
|
case "get_checkpoint":
|
|
272
422
|
return handleGetCheckpoint(args, service);
|
|
423
|
+
case "index_conversations":
|
|
424
|
+
return handleIndexConversations(args, service);
|
|
425
|
+
case "list_indexed_sessions":
|
|
426
|
+
return handleListIndexedSessions(args, service);
|
|
427
|
+
case "reindex_session":
|
|
428
|
+
return handleReindexSession(args, service);
|
|
273
429
|
default:
|
|
274
430
|
return {
|
|
275
431
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
package/src/mcp/tools.ts
CHANGED
|
@@ -54,7 +54,7 @@ For long content (>1000 chars), provide embedding_text with a searchable summary
|
|
|
54
54
|
},
|
|
55
55
|
required: ["memories"],
|
|
56
56
|
},
|
|
57
|
-
}
|
|
57
|
+
};
|
|
58
58
|
|
|
59
59
|
export const deleteMemoriesTool: Tool = {
|
|
60
60
|
name: "delete_memories",
|
|
@@ -74,7 +74,7 @@ export const deleteMemoriesTool: Tool = {
|
|
|
74
74
|
},
|
|
75
75
|
required: ["ids"],
|
|
76
76
|
},
|
|
77
|
-
}
|
|
77
|
+
};
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
const updateMemoriesTool: Tool = {
|
|
@@ -167,6 +167,20 @@ When in doubt, search. Missing context is costlier than an extra query.`,
|
|
|
167
167
|
description: "Include soft-deleted memories in results (default: false). Useful for recovering prior information.",
|
|
168
168
|
default: false,
|
|
169
169
|
},
|
|
170
|
+
include_history: {
|
|
171
|
+
type: "boolean",
|
|
172
|
+
description:
|
|
173
|
+
"Include conversation history results alongside memories (default: false). " +
|
|
174
|
+
"Requires conversation history indexing to be enabled. History results are weighted lower than explicit memories.",
|
|
175
|
+
default: false,
|
|
176
|
+
},
|
|
177
|
+
history_only: {
|
|
178
|
+
type: "boolean",
|
|
179
|
+
description:
|
|
180
|
+
"Search only conversation history, excluding explicit memories (default: false). " +
|
|
181
|
+
"Requires conversation history indexing to be enabled.",
|
|
182
|
+
default: false,
|
|
183
|
+
},
|
|
170
184
|
},
|
|
171
185
|
required: ["query", "intent", "reason_for_search"],
|
|
172
186
|
},
|
|
@@ -187,7 +201,7 @@ export const getMemoriesTool: Tool = {
|
|
|
187
201
|
},
|
|
188
202
|
required: ["ids"],
|
|
189
203
|
},
|
|
190
|
-
}
|
|
204
|
+
};
|
|
191
205
|
|
|
192
206
|
export const reportMemoryUsefulnessTool: Tool = {
|
|
193
207
|
name: "report_memory_usefulness",
|
|
@@ -272,6 +286,52 @@ export const getCheckpointTool: Tool = {
|
|
|
272
286
|
},
|
|
273
287
|
};
|
|
274
288
|
|
|
289
|
+
export const indexConversationsTool: Tool = {
|
|
290
|
+
name: "index_conversations",
|
|
291
|
+
description:
|
|
292
|
+
"Index Claude Code session logs for searchable conversation history. " +
|
|
293
|
+
"Scans for JSONL session files, detects new/changed sessions, and indexes text messages. " +
|
|
294
|
+
"Skips unchanged files. Run periodically or after significant work sessions.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
path: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description:
|
|
301
|
+
"Directory containing session JSONL files. If omitted, auto-detects the Claude Code sessions directory.",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export const listIndexedSessionsTool: Tool = {
|
|
308
|
+
name: "list_indexed_sessions",
|
|
309
|
+
description:
|
|
310
|
+
"List all conversation sessions that have been indexed. " +
|
|
311
|
+
"Shows session ID, message count, time range, and associated project/branch.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {},
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export const reindexSessionTool: Tool = {
|
|
319
|
+
name: "reindex_session",
|
|
320
|
+
description:
|
|
321
|
+
"Force a full re-index of a specific session. Deletes all existing entries for the session and re-parses from scratch. " +
|
|
322
|
+
"Use when session data appears corrupted or after parser improvements.",
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
session_id: {
|
|
327
|
+
type: "string",
|
|
328
|
+
description: "The session ID to reindex.",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
required: ["session_id"],
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
275
335
|
export const tools: Tool[] = [
|
|
276
336
|
storeMemoriesTool,
|
|
277
337
|
updateMemoriesTool,
|
|
@@ -281,4 +341,7 @@ export const tools: Tool[] = [
|
|
|
281
341
|
reportMemoryUsefulnessTool,
|
|
282
342
|
storeCheckpointTool,
|
|
283
343
|
getCheckpointTool,
|
|
344
|
+
indexConversationsTool,
|
|
345
|
+
listIndexedSessionsTool,
|
|
346
|
+
reindexSessionTool,
|
|
284
347
|
];
|