@hanhnd/agent-kit 1.0.27 → 1.0.29

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.
@@ -0,0 +1,268 @@
1
+ import Database from 'better-sqlite3';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ export class StoreError extends Error {
5
+ constructor(message, cause) {
6
+ super(message);
7
+ this.name = 'StoreError';
8
+ if (cause instanceof Error)
9
+ this.cause = cause;
10
+ }
11
+ }
12
+ export class MemoryStore {
13
+ config;
14
+ db;
15
+ _vecAvailable = false;
16
+ constructor(dbPath, config) {
17
+ this.config = config;
18
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
19
+ this.db = this.openDatabase(dbPath);
20
+ }
21
+ openDatabase(dbPath) {
22
+ try {
23
+ const db = new Database(dbPath);
24
+ db.pragma('journal_mode = WAL');
25
+ db.pragma('busy_timeout = 5000');
26
+ this.loadVecExtension(db);
27
+ this.createSchema(db);
28
+ return db;
29
+ }
30
+ catch (err) {
31
+ console.error('[memory-store] DB open failed, recreating:', err);
32
+ try {
33
+ fs.unlinkSync(dbPath);
34
+ }
35
+ catch {
36
+ // ignore
37
+ }
38
+ const db = new Database(dbPath);
39
+ db.pragma('journal_mode = WAL');
40
+ db.pragma('busy_timeout = 5000');
41
+ this.loadVecExtension(db);
42
+ this.createSchema(db);
43
+ return db;
44
+ }
45
+ }
46
+ loadVecExtension(db) {
47
+ try {
48
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
49
+ const sqliteVec = require('sqlite-vec');
50
+ sqliteVec.load(db);
51
+ this._vecAvailable = true;
52
+ }
53
+ catch (err) {
54
+ console.warn('[memory-store] sqlite-vec unavailable, degrading to FTS5-only:', err);
55
+ this._vecAvailable = false;
56
+ }
57
+ }
58
+ createSchema(db) {
59
+ db.exec(`
60
+ CREATE TABLE IF NOT EXISTS memory_chunks (
61
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ id TEXT NOT NULL UNIQUE,
63
+ source TEXT NOT NULL,
64
+ heading TEXT NOT NULL DEFAULT '',
65
+ heading_level INTEGER NOT NULL DEFAULT 0,
66
+ content TEXT NOT NULL,
67
+ line_start INTEGER NOT NULL,
68
+ line_end INTEGER NOT NULL,
69
+ indexed_at INTEGER NOT NULL
70
+ );
71
+
72
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
73
+ content,
74
+ content='memory_chunks',
75
+ content_rowid='rowid'
76
+ );
77
+
78
+ CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
79
+ AFTER INSERT ON memory_chunks BEGIN
80
+ INSERT INTO memory_fts(rowid, content) VALUES (new.rowid, new.content);
81
+ END;
82
+
83
+ CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
84
+ AFTER DELETE ON memory_chunks BEGIN
85
+ INSERT INTO memory_fts(memory_fts, rowid, content)
86
+ VALUES ('delete', old.rowid, old.content);
87
+ END;
88
+ `);
89
+ if (this._vecAvailable) {
90
+ try {
91
+ db.exec(`
92
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
93
+ embedding FLOAT[${this.config.vectorDimension}]
94
+ );
95
+ `);
96
+ }
97
+ catch (err) {
98
+ console.warn('[memory-store] Failed to create memory_vec, disabling vector search:', err);
99
+ this._vecAvailable = false;
100
+ }
101
+ }
102
+ }
103
+ upsert(chunks, embeddings) {
104
+ const insertChunk = this.db.prepare(`
105
+ INSERT OR REPLACE INTO memory_chunks
106
+ (id, source, heading, heading_level, content, line_start, line_end, indexed_at)
107
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
108
+ `);
109
+ let insertVec = null;
110
+ if (this._vecAvailable) {
111
+ insertVec = this.db.prepare(`INSERT OR REPLACE INTO memory_vec (rowid, embedding) VALUES (?, ?)`);
112
+ }
113
+ const upsertAll = this.db.transaction(() => {
114
+ for (let i = 0; i < chunks.length; i++) {
115
+ const chunk = chunks[i];
116
+ try {
117
+ // Delete existing row first to get correct rowid
118
+ this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`).run(chunk.id);
119
+ const info = insertChunk.run(chunk.id, chunk.source, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now());
120
+ if (insertVec && embeddings[i] && info.lastInsertRowid) {
121
+ const rowid = Number(info.lastInsertRowid);
122
+ const emb = embeddings[i];
123
+ const buf = Buffer.from(emb.buffer, emb.byteOffset, emb.byteLength);
124
+ insertVec.run(rowid, buf);
125
+ }
126
+ }
127
+ catch (err) {
128
+ console.warn(`[memory-store] Failed to upsert chunk ${chunk.id}:`, err);
129
+ }
130
+ }
131
+ });
132
+ try {
133
+ upsertAll();
134
+ }
135
+ catch (err) {
136
+ throw new StoreError('Full batch upsert failed', err);
137
+ }
138
+ }
139
+ hashesBySource(source) {
140
+ const rows = this.db
141
+ .prepare(`SELECT id FROM memory_chunks WHERE source = ?`)
142
+ .all(source);
143
+ return new Set(rows.map((r) => r.id));
144
+ }
145
+ indexedSources() {
146
+ const rows = this.db
147
+ .prepare(`SELECT DISTINCT source FROM memory_chunks`)
148
+ .all();
149
+ return rows.map((r) => r.source);
150
+ }
151
+ deleteBySource(source) {
152
+ const rows = this.db
153
+ .prepare(`SELECT rowid FROM memory_chunks WHERE source = ?`)
154
+ .all(source);
155
+ const deleteAll = this.db.transaction(() => {
156
+ if (this._vecAvailable && rows.length > 0) {
157
+ const placeholders = rows.map(() => '?').join(', ');
158
+ this.db
159
+ .prepare(`DELETE FROM memory_vec WHERE rowid IN (${placeholders})`)
160
+ .run(...rows.map((r) => r.rowid));
161
+ }
162
+ this.db.prepare(`DELETE FROM memory_chunks WHERE source = ?`).run(source);
163
+ });
164
+ deleteAll();
165
+ }
166
+ deleteByIds(ids) {
167
+ if (ids.length === 0)
168
+ return;
169
+ const placeholders = ids.map(() => '?').join(', ');
170
+ const rows = this.db
171
+ .prepare(`SELECT rowid FROM memory_chunks WHERE id IN (${placeholders})`)
172
+ .all(...ids);
173
+ const deleteAll = this.db.transaction(() => {
174
+ if (this._vecAvailable && rows.length > 0) {
175
+ const rPlaceholders = rows.map(() => '?').join(', ');
176
+ this.db
177
+ .prepare(`DELETE FROM memory_vec WHERE rowid IN (${rPlaceholders})`)
178
+ .run(...rows.map((r) => r.rowid));
179
+ }
180
+ this.db.prepare(`DELETE FROM memory_chunks WHERE id IN (${placeholders})`).run(...ids);
181
+ });
182
+ deleteAll();
183
+ }
184
+ searchDense(embedding, limit) {
185
+ if (!this._vecAvailable)
186
+ return [];
187
+ try {
188
+ const buf = Buffer.from(embedding.buffer);
189
+ const rows = this.db
190
+ .prepare(`SELECT mc.id, mv.distance
191
+ FROM memory_vec mv
192
+ JOIN memory_chunks mc ON mc.rowid = mv.rowid
193
+ WHERE mv.embedding MATCH ? AND k = ?
194
+ ORDER BY mv.distance`)
195
+ .all(buf, limit);
196
+ return rows.map((r) => ({
197
+ id: r.id,
198
+ score: Math.max(0, 1 - r.distance),
199
+ }));
200
+ }
201
+ catch (err) {
202
+ console.warn('[memory-store] Dense search failed:', err);
203
+ return [];
204
+ }
205
+ }
206
+ searchBm25(query, limit) {
207
+ if (!query.trim())
208
+ return [];
209
+ // FTS5 defaults to AND for multi-word queries; convert to OR so any term is a hit
210
+ const ftsQuery = query
211
+ .trim()
212
+ .split(/\s+/)
213
+ .map((t) => t.replace(/['"*^():\-]/g, ' ').trim())
214
+ .filter(Boolean)
215
+ .join(' OR ');
216
+ if (!ftsQuery)
217
+ return [];
218
+ try {
219
+ const rows = this.db
220
+ .prepare(`SELECT mc.id, rank
221
+ FROM memory_fts
222
+ JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
223
+ WHERE memory_fts MATCH ?
224
+ ORDER BY rank
225
+ LIMIT ?`)
226
+ .all(ftsQuery, limit);
227
+ if (rows.length === 0)
228
+ return [];
229
+ // BM25 rank is negative in SQLite FTS5 (lower = more relevant)
230
+ const ranks = rows.map((r) => r.rank);
231
+ const minRank = Math.min(...ranks);
232
+ const maxRank = Math.max(...ranks);
233
+ const range = maxRank - minRank;
234
+ return rows.map((r) => ({
235
+ id: r.id,
236
+ score: range > 0 ? (maxRank - r.rank) / range : 1,
237
+ }));
238
+ }
239
+ catch (err) {
240
+ console.warn('[memory-store] BM25 search failed:', err);
241
+ return [];
242
+ }
243
+ }
244
+ getChunksByIds(ids) {
245
+ if (ids.length === 0)
246
+ return [];
247
+ const placeholders = ids.map(() => '?').join(', ');
248
+ const rows = this.db
249
+ .prepare(`SELECT id, source, heading, heading_level, content, line_start, line_end
250
+ FROM memory_chunks WHERE id IN (${placeholders})`)
251
+ .all(...ids);
252
+ return rows.map((r) => ({
253
+ id: r.id,
254
+ source: r.source,
255
+ heading: r.heading,
256
+ headingLevel: r.heading_level,
257
+ content: r.content,
258
+ lineStart: r.line_start,
259
+ lineEnd: r.line_end,
260
+ }));
261
+ }
262
+ get vecAvailable() {
263
+ return this._vecAvailable;
264
+ }
265
+ close() {
266
+ this.db.close();
267
+ }
268
+ }
@@ -0,0 +1,30 @@
1
+ export interface MemoryChunk {
2
+ id: string;
3
+ source: string;
4
+ heading: string;
5
+ headingLevel: number;
6
+ content: string;
7
+ lineStart: number;
8
+ lineEnd: number;
9
+ }
10
+ export interface SearchResult {
11
+ chunk: MemoryChunk;
12
+ score: number;
13
+ retriever: 'dense' | 'bm25' | 'both';
14
+ contentSource: 'file' | 'fallback';
15
+ }
16
+ export interface MemoryConfig {
17
+ enabled: boolean;
18
+ wikiDir: string;
19
+ topK: number;
20
+ chunkSize: number;
21
+ overlapLines: number;
22
+ embeddingModel: string;
23
+ vectorDimension: number;
24
+ }
25
+ export interface IndexStats {
26
+ indexed: number;
27
+ deleted: number;
28
+ skipped: number;
29
+ }
30
+ export declare const DEFAULT_MEMORY_CONFIG: MemoryConfig;
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_MEMORY_CONFIG = {
2
+ enabled: false,
3
+ wikiDir: '',
4
+ topK: 5,
5
+ chunkSize: 1500,
6
+ overlapLines: 2,
7
+ embeddingModel: 'Xenova/bge-small-en-v1.5',
8
+ vectorDimension: 384,
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,185 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { describe, test } from 'node:test';
3
+ import { resolveMemoryConfig } from '../config.js';
4
+ import { registerMemoryToolHandlers } from '../memory.js';
5
+ // Minimal McpServer stub that captures tool registrations
6
+ function makeMockServer() {
7
+ const tools = new Map();
8
+ const server = {
9
+ tool(name, _desc, _schema, handler) {
10
+ tools.set(name, handler);
11
+ },
12
+ };
13
+ return { server, tools };
14
+ }
15
+ function makeChunk(overrides = {}) {
16
+ return {
17
+ id: 'test-id-abc123',
18
+ source: '2024-01-01.md',
19
+ heading: 'My Section',
20
+ headingLevel: 2,
21
+ content: 'This is the chunk content.',
22
+ lineStart: 1,
23
+ lineEnd: 5,
24
+ ...overrides,
25
+ };
26
+ }
27
+ const BASE_CONFIG = {
28
+ enabled: true,
29
+ wikiDir: '/tmp/wiki',
30
+ topK: 5,
31
+ chunkSize: 1500,
32
+ overlapLines: 2,
33
+ embeddingModel: 'Xenova/bge-small-en-v1.5',
34
+ vectorDimension: 384,
35
+ };
36
+ function makeIndexerStub(overrides = {}) {
37
+ return {
38
+ search: overrides.search ?? (async () => []),
39
+ save: overrides.save ?? (async () => ({ indexed: 1, deleted: 0, skipped: 0 })),
40
+ };
41
+ }
42
+ function makeStoreStub(vecAvailable) {
43
+ return { vecAvailable };
44
+ }
45
+ function extractText(result) {
46
+ const r = result;
47
+ return r.content[0]?.text ?? '';
48
+ }
49
+ describe('kit_memory_search', () => {
50
+ test('returns formatted full content and display source when results exist', async () => {
51
+ const chunk = makeChunk({ content: 'Important memory content', source: 'compiled/entities/foo.md' });
52
+ const searchResult = { chunk, score: 0.85, retriever: 'bm25', contentSource: 'file' };
53
+ const indexer = makeIndexerStub({ search: async () => [searchResult] });
54
+ const store = makeStoreStub(true);
55
+ const { server, tools } = makeMockServer();
56
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
57
+ const result = await tools.get('kit_memory_search')({ query: 'test query' });
58
+ const text = extractText(result);
59
+ assert.ok(text.includes('Important memory content'), 'Result must include chunk content');
60
+ assert.ok(text.includes('### entities/foo.md (score: 0.850)'), 'Result must include display source and score');
61
+ assert.ok(!text.includes('compiled/entities/foo.md'), 'Result must strip compiled/ prefix');
62
+ assert.ok(!text.includes('My Section'), 'Result must not include chunk heading as the block title');
63
+ });
64
+ test('returns "no memories" message when results are empty', async () => {
65
+ const indexer = makeIndexerStub({ search: async () => [] });
66
+ const store = makeStoreStub(true);
67
+ const { server, tools } = makeMockServer();
68
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
69
+ const result = await tools.get('kit_memory_search')({ query: 'nothing here' });
70
+ const text = extractText(result);
71
+ assert.ok(text.toLowerCase().includes('no memories') || text.includes('No memories'), `Expected "no memories" message, got: ${text.slice(0, 100)}`);
72
+ });
73
+ test('prepends degraded warning when vecAvailable is false and no results', async () => {
74
+ const indexer = makeIndexerStub({ search: async () => [] });
75
+ const store = makeStoreStub(false);
76
+ const { server, tools } = makeMockServer();
77
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
78
+ const result = await tools.get('kit_memory_search')({ query: 'test' });
79
+ const text = extractText(result);
80
+ assert.ok(text.includes('Vector search unavailable') || text.includes('keyword-only'), `Expected degraded warning in: ${text.slice(0, 200)}`);
81
+ });
82
+ test('prepends degraded warning when vecAvailable is false with results', async () => {
83
+ const chunk = makeChunk({ content: 'Found content', source: 'test.md' });
84
+ const searchResult = { chunk, score: 0.5, retriever: 'bm25', contentSource: 'file' };
85
+ const indexer = makeIndexerStub({ search: async () => [searchResult] });
86
+ const store = makeStoreStub(false);
87
+ const { server, tools } = makeMockServer();
88
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
89
+ const result = await tools.get('kit_memory_search')({ query: 'test' });
90
+ const text = extractText(result);
91
+ assert.ok(text.includes('Vector search unavailable') || text.includes('keyword-only'), `Expected degraded warning with results in: ${text.slice(0, 200)}`);
92
+ assert.ok(text.includes('Found content'), 'Result must still contain chunk content');
93
+ });
94
+ test('prepends source-unavailable warning for fallback results', async () => {
95
+ const chunk = makeChunk({ content: 'Stored chunk only', source: 'compiled/entities/foo.md' });
96
+ const searchResult = { chunk, score: 0.5, retriever: 'bm25', contentSource: 'fallback' };
97
+ const indexer = makeIndexerStub({ search: async () => [searchResult] });
98
+ const store = makeStoreStub(true);
99
+ const { server, tools } = makeMockServer();
100
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
101
+ const result = await tools.get('kit_memory_search')({ query: 'test' });
102
+ const text = extractText(result);
103
+ assert.ok(text.includes('Source file unavailable'), `Expected fallback warning in: ${text}`);
104
+ assert.ok(text.includes('Stored chunk only'), 'Result must include fallback chunk content');
105
+ });
106
+ test('uses top_k parameter when provided', async () => {
107
+ let capturedTopK;
108
+ const indexer = makeIndexerStub({
109
+ search: async (_q, topK) => {
110
+ capturedTopK = topK;
111
+ return [];
112
+ },
113
+ });
114
+ const store = makeStoreStub(true);
115
+ const { server, tools } = makeMockServer();
116
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
117
+ await tools.get('kit_memory_search')({ query: 'test', top_k: 3 });
118
+ assert.equal(capturedTopK, 3);
119
+ });
120
+ test('falls back to config.topK when top_k is not provided', async () => {
121
+ let capturedTopK;
122
+ const indexer = makeIndexerStub({
123
+ search: async (_q, topK) => {
124
+ capturedTopK = topK;
125
+ return [];
126
+ },
127
+ });
128
+ const store = makeStoreStub(true);
129
+ const { server, tools } = makeMockServer();
130
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
131
+ await tools.get('kit_memory_search')({ query: 'test' });
132
+ assert.equal(capturedTopK, BASE_CONFIG.topK);
133
+ });
134
+ test('returns error message when indexer.search throws', async () => {
135
+ const indexer = makeIndexerStub({
136
+ search: async () => { throw new Error('search exploded'); },
137
+ });
138
+ const store = makeStoreStub(true);
139
+ const { server, tools } = makeMockServer();
140
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
141
+ const result = await tools.get('kit_memory_search')({ query: 'test' });
142
+ const text = extractText(result);
143
+ assert.ok(text.includes('kit_memory_search failed'), `Expected error prefix, got: ${text}`);
144
+ assert.ok(text.includes('search exploded'), `Expected error message, got: ${text}`);
145
+ });
146
+ });
147
+ describe('kit_memory_save', () => {
148
+ test('returns saved:true and queued-for-compile status on success', async () => {
149
+ const indexer = makeIndexerStub({
150
+ save: async () => ({ indexed: 0, deleted: 0, skipped: 0 }),
151
+ });
152
+ const store = makeStoreStub(true);
153
+ const { server, tools } = makeMockServer();
154
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
155
+ const result = await tools.get('kit_memory_save')({ content: 'Some important note' });
156
+ const text = extractText(result);
157
+ const parsed = JSON.parse(text);
158
+ assert.equal(parsed.saved, true);
159
+ assert.equal(parsed.queued_for_compile, true);
160
+ assert.equal(parsed.message, 'Saved to wiki/raw — will be indexed after next /wiki compile');
161
+ });
162
+ test('returns saved:false and error message when indexer.save throws', async () => {
163
+ const indexer = makeIndexerStub({
164
+ save: async () => { throw new Error('disk full'); },
165
+ });
166
+ const store = makeStoreStub(true);
167
+ const { server, tools } = makeMockServer();
168
+ registerMemoryToolHandlers(server, indexer, store, BASE_CONFIG);
169
+ const result = await tools.get('kit_memory_save')({ content: 'note' });
170
+ const text = extractText(result);
171
+ const parsed = JSON.parse(text);
172
+ assert.equal(parsed.saved, false);
173
+ assert.ok(parsed.error.includes('disk full'), `Expected "disk full" in error, got: ${parsed.error}`);
174
+ });
175
+ });
176
+ describe('resolveMemoryConfig', () => {
177
+ test('defaults wikiDir under .agent-kit', () => {
178
+ const config = resolveMemoryConfig({ memory: {} }, '/repo');
179
+ assert.equal(config.wikiDir, '/repo/.agent-kit/wiki');
180
+ });
181
+ test('uses explicit wikiDir override', () => {
182
+ const config = resolveMemoryConfig({ memory: { wikiDir: '/custom/wiki' } }, '/repo');
183
+ assert.equal(config.wikiDir, '/custom/wiki');
184
+ });
185
+ });
@@ -2,6 +2,7 @@
2
2
  * Configuration utilities
3
3
  * Shared configuration constants and loaders
4
4
  */
5
+ import { type MemoryConfig } from '../memory/types.js';
5
6
  /**
6
7
  * Default file extensions - expanded to support more languages
7
8
  * Used by kit_get_project_context and other tools
@@ -12,8 +13,14 @@ export declare const DEFAULT_EXTENSIONS: string[];
12
13
  */
13
14
  export interface ProjectSettings {
14
15
  fileExtensions?: string[];
16
+ memory?: Partial<MemoryConfig>;
15
17
  [key: string]: unknown;
16
18
  }
19
+ export { MemoryConfig };
20
+ /**
21
+ * Merge DEFAULT_MEMORY_CONFIG with settings.memory; resolve wikiDir when not explicitly set.
22
+ */
23
+ export declare function resolveMemoryConfig(settings: ProjectSettings, workspaceRoot: string): MemoryConfig;
17
24
  /**
18
25
  * Get file extensions - from settings.json or defaults
19
26
  * Allows project-specific customization
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
+ import { DEFAULT_MEMORY_CONFIG } from '../memory/types.js';
7
8
  /**
8
9
  * Default file extensions - expanded to support more languages
9
10
  * Used by kit_get_project_context and other tools
@@ -32,6 +33,20 @@ export const DEFAULT_EXTENSIONS = [
32
33
  '.yml', // Config files
33
34
  '.md', // Documentation
34
35
  ];
36
+ /**
37
+ * Merge DEFAULT_MEMORY_CONFIG with settings.memory; resolve wikiDir when not explicitly set.
38
+ */
39
+ export function resolveMemoryConfig(settings, workspaceRoot) {
40
+ const override = settings.memory ?? {};
41
+ return {
42
+ ...DEFAULT_MEMORY_CONFIG,
43
+ ...override,
44
+ enabled: override.enabled ?? DEFAULT_MEMORY_CONFIG.enabled,
45
+ wikiDir: typeof override.wikiDir === 'string' && override.wikiDir.length > 0
46
+ ? override.wikiDir
47
+ : path.join(workspaceRoot, '.agent-kit', 'wiki'),
48
+ };
49
+ }
35
50
  /**
36
51
  * Get file extensions - from settings.json or defaults
37
52
  * Allows project-specific customization
@@ -0,0 +1,16 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { MemoryIndexer } from '../memory/indexer.js';
3
+ import { MemoryStore } from '../memory/store.js';
4
+ import type { MemoryConfig } from '../memory/types.js';
5
+ import { type ProjectSettings } from './config.js';
6
+ /**
7
+ * Registers tool handlers onto an already-constructed indexer/store pair.
8
+ * Exported for unit testing with mocks.
9
+ */
10
+ export declare function registerMemoryToolHandlers(server: McpServer, indexer: MemoryIndexer, store: MemoryStore, config: MemoryConfig): void;
11
+ /**
12
+ * Initializes the memory subsystem and registers all memory tools.
13
+ * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
14
+ * Returns null when memory is disabled (settings.memory.enabled !== true).
15
+ */
16
+ export declare function registerMemoryTools(server: McpServer, settings: ProjectSettings, workspaceRoot: string): MemoryIndexer | null;
@@ -0,0 +1,75 @@
1
+ import * as path from 'path';
2
+ import { z } from 'zod';
3
+ import { Embedder } from '../memory/embedder.js';
4
+ import { MemoryIndexer } from '../memory/indexer.js';
5
+ import { MemoryStore } from '../memory/store.js';
6
+ import { mcpJson, mcpText } from '../utils/utils.js';
7
+ import { resolveMemoryConfig } from './config.js';
8
+ /**
9
+ * Registers tool handlers onto an already-constructed indexer/store pair.
10
+ * Exported for unit testing with mocks.
11
+ */
12
+ export function registerMemoryToolHandlers(server, indexer, store, config) {
13
+ server.tool('kit_memory_search', 'Search persistent memory for context relevant to the current question. Returns semantically relevant results using hybrid dense+BM25+RRF search.', {
14
+ query: z.string().min(1).describe('Search query'),
15
+ top_k: z.number().int().positive().optional().describe('Number of results to return'),
16
+ }, async ({ query, top_k }) => {
17
+ try {
18
+ const results = await indexer.search(query, top_k ?? config.topK);
19
+ if (results.length === 0) {
20
+ const degradedNote = !store.vecAvailable
21
+ ? '⚠️ Vector search unavailable — showing keyword-only results.\n\n'
22
+ : '';
23
+ return mcpText(`${degradedNote}No memories found for query: "${query}"`);
24
+ }
25
+ const degradedNote = !store.vecAvailable
26
+ ? '⚠️ Vector search unavailable — showing keyword-only results.\n\n'
27
+ : '';
28
+ const formatted = results
29
+ .map((r) => {
30
+ const displaySource = r.chunk.source.replace(/^compiled\//, '');
31
+ const content = r.contentSource === 'fallback'
32
+ ? `⚠️ Source file unavailable — showing matched chunk only:\n${r.chunk.content}`
33
+ : r.chunk.content;
34
+ return `### ${displaySource} (score: ${r.score.toFixed(3)})\n${content}`;
35
+ })
36
+ .join('\n\n---\n\n');
37
+ return mcpText(`${degradedNote}${formatted}`);
38
+ }
39
+ catch (err) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ return mcpText(`kit_memory_search failed: ${message}`);
42
+ }
43
+ });
44
+ server.tool('kit_memory_save', 'Save content to wiki/raw for inclusion after the next /wiki compile.', {
45
+ content: z.string().min(1).describe('Content to save to memory'),
46
+ }, async ({ content }) => {
47
+ try {
48
+ await indexer.save(content);
49
+ return mcpJson({
50
+ saved: true,
51
+ queued_for_compile: true,
52
+ message: 'Saved to wiki/raw — will be indexed after next /wiki compile',
53
+ });
54
+ }
55
+ catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ return mcpJson({ saved: false, error: message });
58
+ }
59
+ });
60
+ }
61
+ /**
62
+ * Initializes the memory subsystem and registers all memory tools.
63
+ * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
64
+ * Returns null when memory is disabled (settings.memory.enabled !== true).
65
+ */
66
+ export function registerMemoryTools(server, settings, workspaceRoot) {
67
+ if (settings.memory?.enabled !== true)
68
+ return null;
69
+ const config = resolveMemoryConfig(settings, workspaceRoot);
70
+ const store = new MemoryStore(path.join(config.wikiDir, 'index.db'), config);
71
+ const embedder = new Embedder(config.embeddingModel);
72
+ const indexer = new MemoryIndexer(store, embedder, config);
73
+ registerMemoryToolHandlers(server, indexer, store, config);
74
+ return indexer;
75
+ }
@@ -1,8 +1,10 @@
1
1
  export function getWorkspaceRoot() {
2
2
  return (process.env.WORKSPACE_DIR ||
3
- process.env.CODEX_WORKSPACE ||
3
+ process.env.CLAUDE_PROJECT_DIR ||
4
4
  process.env.CODEX_PROJECT_DIR ||
5
+ process.env.GEMINI_PROJECT_DIR ||
5
6
  process.env.CLAUDE_WORKSPACE ||
7
+ process.env.CODEX_WORKSPACE ||
6
8
  process.env.GEMINI_WORKSPACE ||
7
9
  process.env.INIT_CWD ||
8
10
  process.env.PWD ||