mcp-memory-keeper 0.10.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
package/dist/index.js ADDED
@@ -0,0 +1,4310 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.dbManager = exports._featureFlagManager = void 0;
37
+ exports.debugLog = debugLog;
38
+ exports.validatePaginationParams = validatePaginationParams;
39
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
40
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
41
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
42
+ const uuid_1 = require("uuid");
43
+ const crypto = __importStar(require("crypto"));
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const os = __importStar(require("os"));
47
+ const database_js_1 = require("./utils/database.js");
48
+ const knowledge_graph_js_1 = require("./utils/knowledge-graph.js");
49
+ const vector_store_js_1 = require("./utils/vector-store.js");
50
+ const timestamps_js_1 = require("./utils/timestamps.js");
51
+ const agents_js_1 = require("./utils/agents.js");
52
+ const retention_js_1 = require("./utils/retention.js");
53
+ const feature_flags_js_1 = require("./utils/feature-flags.js");
54
+ const RepositoryManager_js_1 = require("./repositories/RepositoryManager.js");
55
+ const simple_git_1 = require("simple-git");
56
+ const channels_js_1 = require("./utils/channels.js");
57
+ const contextWatchHandlers_js_1 = require("./handlers/contextWatchHandlers.js");
58
+ // Initialize database with migrations
59
+ const dbManager = new database_js_1.DatabaseManager({ filename: 'context.db' });
60
+ exports.dbManager = dbManager;
61
+ const db = dbManager.getDatabase();
62
+ // Initialize repository manager
63
+ const repositories = new RepositoryManager_js_1.RepositoryManager(dbManager);
64
+ // Initialize git - will be created per session as needed
65
+ // REMOVED: Global project directory was causing conflicts between sessions
66
+ // Initialize knowledge graph manager
67
+ const knowledgeGraph = new knowledge_graph_js_1.KnowledgeGraphManager(db);
68
+ // Initialize vector store
69
+ const vectorStore = new vector_store_js_1.VectorStore(db);
70
+ // Initialize multi-agent system
71
+ const agentCoordinator = new agents_js_1.AgentCoordinator();
72
+ const analyzerAgent = new agents_js_1.AnalyzerAgent(db, knowledgeGraph, vectorStore);
73
+ const synthesizerAgent = new agents_js_1.SynthesizerAgent(db, vectorStore);
74
+ agentCoordinator.registerAgent(analyzerAgent);
75
+ agentCoordinator.registerAgent(synthesizerAgent);
76
+ // Initialize retention manager
77
+ const _retentionManager = new retention_js_1.RetentionManager(dbManager);
78
+ // Initialize feature flag manager
79
+ const _featureFlagManager = new feature_flags_js_1.FeatureFlagManager(dbManager);
80
+ exports._featureFlagManager = _featureFlagManager;
81
+ // Initialize debug logging flag if it doesn't exist
82
+ try {
83
+ if (!_featureFlagManager.getFlagByKey('debug_logging')) {
84
+ _featureFlagManager.createFlag({
85
+ name: 'Debug Logging',
86
+ key: 'debug_logging',
87
+ enabled: Boolean(process.env.MCP_DEBUG_LOGGING),
88
+ description: 'Enable debug logging for development and troubleshooting',
89
+ category: 'debug',
90
+ tags: ['debug', 'logging'],
91
+ });
92
+ }
93
+ }
94
+ catch (_error) {
95
+ // Silently continue if flag creation fails (migrations might not be complete)
96
+ }
97
+ // Migration manager is no longer needed - watcher migrations are now applied by DatabaseManager
98
+ // Tables are now created by DatabaseManager in utils/database.ts
99
+ // Track current session
100
+ let currentSessionId = null;
101
+ // Debug logging utility
102
+ function debugLog(message, ...args) {
103
+ try {
104
+ if (_featureFlagManager.isEnabled('debug_logging') || process.env.MCP_DEBUG_LOGGING) {
105
+ // eslint-disable-next-line no-console
106
+ console.log(`[MCP-Memory-Keeper DEBUG] ${message}`, ...args);
107
+ }
108
+ }
109
+ catch (_error) {
110
+ // Silently fail if feature flags aren't available yet
111
+ }
112
+ }
113
+ function validatePaginationParams(params) {
114
+ const errors = [];
115
+ let limit = 25; // default
116
+ let offset = 0; // default
117
+ // Validate limit
118
+ if (params.limit !== undefined && params.limit !== null) {
119
+ const rawLimit = params.limit;
120
+ if (!Number.isInteger(rawLimit) || rawLimit <= 0) {
121
+ errors.push(`Invalid limit: expected positive integer, got ${typeof rawLimit} '${rawLimit}'`);
122
+ debugLog(`Pagination validation: Invalid limit ${rawLimit}, using default ${limit}`);
123
+ }
124
+ else {
125
+ limit = Math.min(Math.max(1, rawLimit), 100); // clamp between 1-100
126
+ if (limit !== rawLimit) {
127
+ debugLog(`Pagination validation: Clamped limit from ${rawLimit} to ${limit}`);
128
+ }
129
+ }
130
+ }
131
+ // Validate offset
132
+ if (params.offset !== undefined && params.offset !== null) {
133
+ const rawOffset = params.offset;
134
+ if (!Number.isInteger(rawOffset) || rawOffset < 0) {
135
+ errors.push(`Invalid offset: expected non-negative integer, got ${typeof rawOffset} '${rawOffset}'`);
136
+ debugLog(`Pagination validation: Invalid offset ${rawOffset}, using default ${offset}`);
137
+ }
138
+ else {
139
+ offset = rawOffset;
140
+ }
141
+ }
142
+ return { limit, offset, errors };
143
+ }
144
+ // Helper function to get or create default session
145
+ function ensureSession() {
146
+ if (!currentSessionId) {
147
+ const session = repositories.sessions.getLatest();
148
+ if (session) {
149
+ currentSessionId = session.id;
150
+ }
151
+ else {
152
+ // Create default session
153
+ const newSession = repositories.sessions.create({
154
+ name: 'Default Session',
155
+ description: 'Auto-created default session',
156
+ });
157
+ currentSessionId = newSession.id;
158
+ }
159
+ }
160
+ return currentSessionId;
161
+ }
162
+ // Helper to calculate file hash
163
+ function calculateFileHash(content) {
164
+ return crypto.createHash('sha256').update(content).digest('hex');
165
+ }
166
+ // Helper to calculate byte size of string
167
+ function calculateSize(value) {
168
+ return Buffer.byteLength(value, 'utf8');
169
+ }
170
+ // Helper to estimate token count from text
171
+ // Rough estimate: 1 token ≈ 4 characters
172
+ function estimateTokens(text) {
173
+ return Math.ceil(text.length / 4);
174
+ }
175
+ // Helper to calculate total response size and token estimate
176
+ function calculateResponseMetrics(items) {
177
+ let totalSize = 0;
178
+ for (const item of items) {
179
+ const itemSize = item.size || calculateSize(item.value);
180
+ totalSize += itemSize;
181
+ }
182
+ // Convert to JSON string to get actual response size
183
+ const jsonString = JSON.stringify(items);
184
+ const estimatedTokens = estimateTokens(jsonString);
185
+ const averageSize = items.length > 0 ? Math.round(totalSize / items.length) : 0;
186
+ return { totalSize, estimatedTokens, averageSize };
187
+ }
188
+ // Helper to parse relative time strings
189
+ function parseRelativeTime(relativeTime) {
190
+ const now = new Date();
191
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
192
+ if (relativeTime === 'today') {
193
+ return today.toISOString();
194
+ }
195
+ else if (relativeTime === 'yesterday') {
196
+ return new Date(today.getTime() - 24 * 60 * 60 * 1000).toISOString();
197
+ }
198
+ else if (relativeTime.match(/^(\d+) hours? ago$/)) {
199
+ const hours = parseInt(relativeTime.match(/^(\d+)/)[1]);
200
+ return new Date(now.getTime() - hours * 60 * 60 * 1000).toISOString();
201
+ }
202
+ else if (relativeTime.match(/^(\d+) days? ago$/)) {
203
+ const days = parseInt(relativeTime.match(/^(\d+)/)[1]);
204
+ return new Date(now.getTime() - days * 24 * 60 * 60 * 1000).toISOString();
205
+ }
206
+ else if (relativeTime === 'this week') {
207
+ const startOfWeek = new Date(today);
208
+ startOfWeek.setDate(today.getDate() - today.getDay());
209
+ return startOfWeek.toISOString();
210
+ }
211
+ else if (relativeTime === 'last week') {
212
+ const startOfLastWeek = new Date(today);
213
+ startOfLastWeek.setDate(today.getDate() - today.getDay() - 7);
214
+ return startOfLastWeek.toISOString();
215
+ }
216
+ return null;
217
+ }
218
+ // Helper to get project directory setup message
219
+ function getProjectDirectorySetupMessage() {
220
+ return `⚠️ No project directory set for git tracking!
221
+
222
+ To enable git tracking for your project, use one of these methods:
223
+
224
+ 1. For the current session:
225
+ context_set_project_dir({ projectDir: "/path/to/your/project" })
226
+
227
+ 2. When starting a new session:
228
+ context_session_start({ name: "My Session", projectDir: "/path/to/your/project" })
229
+
230
+ This allows the MCP server to track git changes in your actual project directory.`;
231
+ }
232
+ // Helper to get git status for a session
233
+ async function getGitStatus(sessionId) {
234
+ // Get the current session's working directory
235
+ const session = sessionId
236
+ ? repositories.sessions.getById(sessionId)
237
+ : repositories.sessions.getById(currentSessionId || '');
238
+ if (!session || !session.working_directory) {
239
+ return { status: 'No project directory set', branch: 'none' };
240
+ }
241
+ try {
242
+ const git = (0, simple_git_1.simpleGit)(session.working_directory);
243
+ const status = await git.status();
244
+ const branch = await git.branch();
245
+ return {
246
+ status: JSON.stringify({
247
+ modified: status.modified,
248
+ created: status.created,
249
+ deleted: status.deleted,
250
+ staged: status.staged,
251
+ ahead: status.ahead,
252
+ behind: status.behind,
253
+ }),
254
+ branch: branch.current,
255
+ };
256
+ }
257
+ catch (_e) {
258
+ return { status: 'No git repository', branch: 'none' };
259
+ }
260
+ }
261
+ // Helper to create summary
262
+ function createSummary(items, options) {
263
+ const { categories, maxLength = 1000 } = options;
264
+ let filteredItems = items;
265
+ if (categories && categories.length > 0) {
266
+ filteredItems = items.filter(item => categories.includes(item.category));
267
+ }
268
+ // Group by category
269
+ const grouped = filteredItems.reduce((acc, item) => {
270
+ const cat = item.category || 'uncategorized';
271
+ if (!acc[cat])
272
+ acc[cat] = [];
273
+ acc[cat].push(item);
274
+ return acc;
275
+ }, {});
276
+ // Build summary
277
+ let summary = '# Context Summary\n\n';
278
+ // High priority items first
279
+ const highPriorityItems = filteredItems.filter(item => item.priority === 'high');
280
+ if (highPriorityItems.length > 0) {
281
+ summary += '## High Priority Items\n';
282
+ highPriorityItems.forEach(item => {
283
+ summary += `- **${item.key}**: ${item.value.substring(0, 200)}${item.value.length > 200 ? '...' : ''}\n`;
284
+ });
285
+ summary += '\n';
286
+ }
287
+ // Then by category
288
+ Object.entries(grouped).forEach(([category, categoryItems]) => {
289
+ if (category !== 'uncategorized') {
290
+ summary += `## ${category.charAt(0).toUpperCase() + category.slice(1)}\n`;
291
+ categoryItems.forEach((item) => {
292
+ if (item.priority !== 'high') {
293
+ // Already shown above
294
+ summary += `- ${item.key}: ${item.value.substring(0, 100)}${item.value.length > 100 ? '...' : ''}\n`;
295
+ }
296
+ });
297
+ summary += '\n';
298
+ }
299
+ });
300
+ // Truncate if needed
301
+ if (summary.length > maxLength) {
302
+ summary = summary.substring(0, maxLength - 3) + '...';
303
+ }
304
+ return summary;
305
+ }
306
+ // Create MCP server
307
+ const server = new index_js_1.Server({
308
+ name: 'memory-keeper',
309
+ version: '0.10.0',
310
+ }, {
311
+ capabilities: {
312
+ tools: {},
313
+ },
314
+ });
315
+ // Main request handler
316
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
317
+ const toolName = request.params.name;
318
+ const args = request.params.arguments;
319
+ switch (toolName) {
320
+ // Session Management
321
+ case 'context_session_start': {
322
+ const { name, description, continueFrom, projectDir, defaultChannel } = args;
323
+ // Project directory will be saved with the session if provided
324
+ // Get current git branch if available
325
+ let branch = null;
326
+ let gitDetected = false;
327
+ try {
328
+ const checkPath = projectDir || process.cwd();
329
+ // Try to detect if directory has git
330
+ const gitHeadPath = path.join(checkPath, '.git', 'HEAD');
331
+ if (fs.existsSync(gitHeadPath)) {
332
+ // Use simple-git to get proper branch info
333
+ const tempGit = (0, simple_git_1.simpleGit)(checkPath);
334
+ const branchInfo = await tempGit.branch();
335
+ branch = branchInfo.current;
336
+ gitDetected = true;
337
+ }
338
+ }
339
+ catch (_e) {
340
+ // Ignore git errors
341
+ }
342
+ // Derive default channel if not provided
343
+ let channel = defaultChannel;
344
+ if (!channel) {
345
+ channel = (0, channels_js_1.deriveDefaultChannel)(branch || undefined, name || undefined);
346
+ }
347
+ // Create new session using repository
348
+ const session = repositories.sessions.create({
349
+ name: name || `Session ${new Date().toISOString()}`,
350
+ description: description || '',
351
+ branch: branch || undefined,
352
+ working_directory: projectDir || undefined,
353
+ defaultChannel: channel,
354
+ });
355
+ // Copy context from previous session if specified
356
+ if (continueFrom) {
357
+ repositories.contexts.copyBetweenSessions(continueFrom, session.id);
358
+ }
359
+ currentSessionId = session.id;
360
+ let statusMessage = `Started new session: ${session.id}\nName: ${name || 'Unnamed'}\nChannel: ${channel}`;
361
+ if (projectDir) {
362
+ statusMessage += `\nProject directory: ${projectDir}`;
363
+ if (gitDetected) {
364
+ statusMessage += `\nGit branch: ${branch || 'unknown'}`;
365
+ }
366
+ else {
367
+ statusMessage += `\nGit: No repository found in project directory`;
368
+ }
369
+ }
370
+ else {
371
+ statusMessage += `\nGit branch: ${branch || 'unknown'}`;
372
+ // Provide helpful guidance about setting project directory
373
+ const cwdHasGit = fs.existsSync(path.join(process.cwd(), '.git'));
374
+ if (cwdHasGit) {
375
+ statusMessage += `\n\n💡 Tip: Your current directory has a git repository. To enable full git tracking, start a session with:\ncontext_session_start({ name: "${name || 'My Session'}", projectDir: "${process.cwd()}" })`;
376
+ }
377
+ else {
378
+ // Check for git repos in immediate subdirectories
379
+ const subdirs = fs
380
+ .readdirSync(process.cwd(), { withFileTypes: true })
381
+ .filter(dirent => dirent.isDirectory())
382
+ .map(dirent => dirent.name)
383
+ .filter(name => !name.startsWith('.'));
384
+ const gitSubdirs = subdirs.filter(dir => {
385
+ try {
386
+ return fs.existsSync(path.join(process.cwd(), dir, '.git'));
387
+ }
388
+ catch {
389
+ return false;
390
+ }
391
+ });
392
+ if (gitSubdirs.length > 0) {
393
+ statusMessage += `\n\n💡 Found git repositories in: ${gitSubdirs.join(', ')}`;
394
+ statusMessage += `\nTo enable git tracking, start a session with your project directory:`;
395
+ statusMessage += `\ncontext_session_start({ name: "${name || 'My Session'}", projectDir: "${path.join(process.cwd(), gitSubdirs[0])}" })`;
396
+ }
397
+ else {
398
+ statusMessage += `\n\n💡 To enable git tracking, start a session with your project directory:`;
399
+ statusMessage += `\ncontext_session_start({ name: "${name || 'My Session'}", projectDir: "/path/to/your/project" })`;
400
+ }
401
+ }
402
+ }
403
+ return {
404
+ content: [
405
+ {
406
+ type: 'text',
407
+ text: statusMessage,
408
+ },
409
+ ],
410
+ };
411
+ }
412
+ case 'context_set_project_dir': {
413
+ const { projectDir } = args;
414
+ const sessionId = ensureSession();
415
+ if (!projectDir) {
416
+ throw new Error('Project directory path is required');
417
+ }
418
+ // Verify the directory exists
419
+ if (!fs.existsSync(projectDir)) {
420
+ return {
421
+ content: [
422
+ {
423
+ type: 'text',
424
+ text: `Error: Directory not found: ${projectDir}`,
425
+ },
426
+ ],
427
+ };
428
+ }
429
+ // Update the current session's working directory
430
+ repositories.sessions.update(sessionId, { working_directory: projectDir });
431
+ // Try to get git info to verify it's a git repo
432
+ let gitInfo = 'No git repository found';
433
+ try {
434
+ const git = (0, simple_git_1.simpleGit)(projectDir);
435
+ const branchInfo = await git.branch();
436
+ const status = await git.status();
437
+ gitInfo = `Git repository detected\nBranch: ${branchInfo.current}\nStatus: ${status.modified.length} modified, ${status.created.length} new, ${status.deleted.length} deleted`;
438
+ }
439
+ catch (_e) {
440
+ // Not a git repo, that's okay
441
+ }
442
+ return {
443
+ content: [
444
+ {
445
+ type: 'text',
446
+ text: `Project directory set for session ${sessionId.substring(0, 8)}: ${projectDir}\n\n${gitInfo}`,
447
+ },
448
+ ],
449
+ };
450
+ }
451
+ case 'context_session_list': {
452
+ const { limit = 10 } = args;
453
+ const sessions = db
454
+ .prepare(`
455
+ SELECT id, name, description, branch, created_at,
456
+ (SELECT COUNT(*) FROM context_items WHERE session_id = sessions.id) as item_count
457
+ FROM sessions
458
+ ORDER BY created_at DESC
459
+ LIMIT ?
460
+ `)
461
+ .all(limit);
462
+ const sessionList = sessions
463
+ .map((s) => `• ${s.name} (${s.id.substring(0, 8)})\n Created: ${s.created_at}\n Items: ${s.item_count}\n Branch: ${s.branch || 'unknown'}`)
464
+ .join('\n\n');
465
+ return {
466
+ content: [
467
+ {
468
+ type: 'text',
469
+ text: `Recent sessions:\n\n${sessionList}`,
470
+ },
471
+ ],
472
+ };
473
+ }
474
+ // Enhanced Context Storage
475
+ case 'context_save': {
476
+ const { key, value, category, priority = 'normal', private: isPrivate = false, channel, } = args;
477
+ try {
478
+ const sessionId = ensureSession();
479
+ // Verify session exists before saving context
480
+ const session = repositories.sessions.getById(sessionId);
481
+ if (!session) {
482
+ // Session was deleted or corrupted, create a new one
483
+ console.warn(`Session ${sessionId} not found, creating new session`);
484
+ const newSession = repositories.sessions.create({
485
+ name: 'Recovery Session',
486
+ description: 'Auto-created after session corruption',
487
+ });
488
+ currentSessionId = newSession.id;
489
+ const _contextItem = repositories.contexts.save(newSession.id, {
490
+ key,
491
+ value,
492
+ category,
493
+ priority: priority,
494
+ isPrivate,
495
+ channel,
496
+ });
497
+ return {
498
+ content: [
499
+ {
500
+ type: 'text',
501
+ text: `Saved: ${key}\nCategory: ${category || 'none'}\nPriority: ${priority}\nSession: ${newSession.id.substring(0, 8)} (recovered)`,
502
+ },
503
+ ],
504
+ };
505
+ }
506
+ const contextItem = repositories.contexts.save(sessionId, {
507
+ key,
508
+ value,
509
+ category,
510
+ priority: priority,
511
+ isPrivate,
512
+ channel,
513
+ });
514
+ // Create embedding for semantic search
515
+ try {
516
+ const content = `${key}: ${value}`;
517
+ const metadata = { key, category, priority };
518
+ await vectorStore.storeDocument(contextItem.id, content, metadata);
519
+ }
520
+ catch (error) {
521
+ // Log but don't fail the save operation
522
+ console.error('Failed to create embedding:', error);
523
+ }
524
+ return {
525
+ content: [
526
+ {
527
+ type: 'text',
528
+ text: `Saved: ${key}\nCategory: ${category || 'none'}\nPriority: ${priority}\nChannel: ${contextItem.channel || 'general'}\nSession: ${sessionId.substring(0, 8)}`,
529
+ },
530
+ ],
531
+ };
532
+ }
533
+ catch (error) {
534
+ console.error('Context save error:', error);
535
+ // If it's a foreign key constraint error, try recovery
536
+ if (error.message?.includes('FOREIGN KEY constraint failed')) {
537
+ try {
538
+ console.warn('Foreign key constraint failed, attempting recovery...');
539
+ const newSession = repositories.sessions.create({
540
+ name: 'Emergency Recovery Session',
541
+ description: 'Created due to foreign key constraint failure',
542
+ });
543
+ currentSessionId = newSession.id;
544
+ const _contextItem = repositories.contexts.save(newSession.id, {
545
+ key,
546
+ value,
547
+ category,
548
+ priority: priority,
549
+ isPrivate,
550
+ channel,
551
+ });
552
+ return {
553
+ content: [
554
+ {
555
+ type: 'text',
556
+ text: `Saved: ${key}\nCategory: ${category || 'none'}\nPriority: ${priority}\nSession: ${newSession.id.substring(0, 8)} (emergency recovery)`,
557
+ },
558
+ ],
559
+ };
560
+ }
561
+ catch (recoveryError) {
562
+ return {
563
+ content: [
564
+ {
565
+ type: 'text',
566
+ text: `Failed to save context item: ${recoveryError.message}`,
567
+ },
568
+ ],
569
+ };
570
+ }
571
+ }
572
+ return {
573
+ content: [
574
+ {
575
+ type: 'text',
576
+ text: `Failed to save context item: ${error.message}`,
577
+ },
578
+ ],
579
+ };
580
+ }
581
+ }
582
+ case 'context_get': {
583
+ const { key, category, channel, channels, sessionId: specificSessionId, includeMetadata, sort, limit: rawLimit, offset: rawOffset, createdAfter, createdBefore, keyPattern, priorities, } = args;
584
+ const targetSessionId = specificSessionId || currentSessionId || ensureSession();
585
+ // Validate pagination parameters with proper defaults
586
+ // Default limit for context_get should be 100 to match repository defaults
587
+ const paginationValidation = validatePaginationParams({
588
+ limit: rawLimit !== undefined ? rawLimit : 100,
589
+ offset: rawOffset,
590
+ });
591
+ const { limit, offset, errors: paginationErrors } = paginationValidation;
592
+ // Log pagination validation errors for debugging
593
+ if (paginationErrors.length > 0) {
594
+ debugLog('context_get pagination validation errors:', paginationErrors);
595
+ }
596
+ // Always use enhanced query to ensure consistent pagination
597
+ // This prevents token limit issues when querying large datasets
598
+ // Removed the conditional check since we always want to use this path
599
+ {
600
+ const result = repositories.contexts.queryEnhanced({
601
+ sessionId: targetSessionId,
602
+ key,
603
+ category,
604
+ channel,
605
+ channels,
606
+ sort,
607
+ limit,
608
+ offset,
609
+ createdAfter,
610
+ createdBefore,
611
+ keyPattern,
612
+ priorities,
613
+ includeMetadata,
614
+ });
615
+ if (result.items.length === 0) {
616
+ return {
617
+ content: [
618
+ {
619
+ type: 'text',
620
+ text: 'No matching context found',
621
+ },
622
+ ],
623
+ };
624
+ }
625
+ // Calculate response metrics
626
+ const metrics = calculateResponseMetrics(result.items);
627
+ const TOKEN_LIMIT = 20000; // Conservative limit to stay well under MCP's 25k limit
628
+ // Check if we're approaching token limits
629
+ const isApproachingLimit = metrics.estimatedTokens > TOKEN_LIMIT;
630
+ // Calculate pagination metadata
631
+ // Use the validated limit and offset from paginationValidation
632
+ const effectiveLimit = limit; // Already validated and defaulted
633
+ const effectiveOffset = offset; // Already validated and defaulted
634
+ const currentPage = effectiveLimit > 0 ? Math.floor(effectiveOffset / effectiveLimit) + 1 : 1;
635
+ const totalPages = effectiveLimit > 0 ? Math.ceil(result.totalCount / effectiveLimit) : 1;
636
+ const hasNextPage = currentPage < totalPages;
637
+ const hasPreviousPage = currentPage > 1;
638
+ // Track whether defaults were applied
639
+ const defaultsApplied = {
640
+ limit: rawLimit === undefined,
641
+ sort: sort === undefined,
642
+ };
643
+ // Enhanced response format
644
+ if (includeMetadata) {
645
+ const itemsWithMetadata = result.items.map(item => ({
646
+ key: item.key,
647
+ value: item.value,
648
+ category: item.category,
649
+ priority: item.priority,
650
+ channel: item.channel,
651
+ metadata: item.metadata ? JSON.parse(item.metadata) : null,
652
+ size: item.size || calculateSize(item.value),
653
+ created_at: item.created_at,
654
+ updated_at: item.updated_at,
655
+ }));
656
+ const response = {
657
+ items: itemsWithMetadata,
658
+ pagination: {
659
+ total: result.totalCount,
660
+ returned: result.items.length,
661
+ offset: effectiveOffset,
662
+ hasMore: hasNextPage,
663
+ nextOffset: hasNextPage ? effectiveOffset + effectiveLimit : null,
664
+ // Extended pagination metadata
665
+ totalCount: result.totalCount,
666
+ page: currentPage,
667
+ pageSize: effectiveLimit,
668
+ totalPages: totalPages,
669
+ hasNextPage: hasNextPage,
670
+ hasPreviousPage: hasPreviousPage,
671
+ previousOffset: hasPreviousPage
672
+ ? Math.max(0, effectiveOffset - effectiveLimit)
673
+ : null,
674
+ // Size information
675
+ totalSize: metrics.totalSize,
676
+ averageSize: metrics.averageSize,
677
+ // Defaults applied
678
+ defaultsApplied: defaultsApplied,
679
+ },
680
+ };
681
+ // Add warning if approaching token limits
682
+ if (isApproachingLimit) {
683
+ response.pagination.warning =
684
+ 'Large result set. Consider using smaller limit or more specific filters.';
685
+ }
686
+ return {
687
+ content: [
688
+ {
689
+ type: 'text',
690
+ text: JSON.stringify(response, null, 2),
691
+ },
692
+ ],
693
+ };
694
+ }
695
+ // Return enhanced format for all queries to support pagination
696
+ const response = {
697
+ items: result.items,
698
+ pagination: {
699
+ total: result.totalCount,
700
+ returned: result.items.length,
701
+ offset: effectiveOffset,
702
+ hasMore: hasNextPage,
703
+ nextOffset: hasNextPage ? effectiveOffset + effectiveLimit : null,
704
+ },
705
+ };
706
+ // Add warning if approaching token limits
707
+ if (isApproachingLimit) {
708
+ response.pagination.warning =
709
+ 'Large result set. Consider using smaller limit or more specific filters.';
710
+ }
711
+ return {
712
+ content: [
713
+ {
714
+ type: 'text',
715
+ text: JSON.stringify(response, null, 2),
716
+ },
717
+ ],
718
+ };
719
+ }
720
+ }
721
+ // File Caching
722
+ case 'context_cache_file': {
723
+ const { filePath, content } = args;
724
+ const sessionId = ensureSession();
725
+ const hash = calculateFileHash(content);
726
+ const stmt = db.prepare(`
727
+ INSERT OR REPLACE INTO file_cache (id, session_id, file_path, content, hash)
728
+ VALUES (?, ?, ?, ?, ?)
729
+ `);
730
+ stmt.run((0, uuid_1.v4)(), sessionId, filePath, content, hash);
731
+ return {
732
+ content: [
733
+ {
734
+ type: 'text',
735
+ text: `Cached file: ${filePath}\nHash: ${hash.substring(0, 16)}...\nSize: ${content.length} bytes`,
736
+ },
737
+ ],
738
+ };
739
+ }
740
+ case 'context_file_changed': {
741
+ const { filePath, currentContent } = args;
742
+ const sessionId = ensureSession();
743
+ const cached = db
744
+ .prepare('SELECT hash, content FROM file_cache WHERE session_id = ? AND file_path = ?')
745
+ .get(sessionId, filePath);
746
+ if (!cached) {
747
+ return {
748
+ content: [
749
+ {
750
+ type: 'text',
751
+ text: `No cached version found for: ${filePath}`,
752
+ },
753
+ ],
754
+ };
755
+ }
756
+ const currentHash = currentContent ? calculateFileHash(currentContent) : null;
757
+ const hasChanged = currentHash !== cached.hash;
758
+ return {
759
+ content: [
760
+ {
761
+ type: 'text',
762
+ text: `File: ${filePath}\nChanged: ${hasChanged}\nCached hash: ${cached.hash.substring(0, 16)}...\nCurrent hash: ${currentHash ? currentHash.substring(0, 16) + '...' : 'N/A'}`,
763
+ },
764
+ ],
765
+ };
766
+ }
767
+ case 'context_status': {
768
+ const sessionId = currentSessionId || ensureSession();
769
+ const stats = db
770
+ .prepare(`
771
+ SELECT
772
+ (SELECT COUNT(*) FROM context_items WHERE session_id = ?) as item_count,
773
+ (SELECT COUNT(*) FROM file_cache WHERE session_id = ?) as file_count,
774
+ (SELECT created_at FROM sessions WHERE id = ?) as session_created,
775
+ (SELECT name FROM sessions WHERE id = ?) as session_name
776
+ `)
777
+ .get(sessionId, sessionId, sessionId, sessionId);
778
+ const recentItems = db
779
+ .prepare(`
780
+ SELECT key, category, priority FROM context_items
781
+ WHERE session_id = ?
782
+ ORDER BY created_at DESC
783
+ LIMIT 5
784
+ `)
785
+ .all(sessionId);
786
+ const recentList = recentItems
787
+ .map((item) => ` • [${item.priority}] ${item.key} (${item.category || 'uncategorized'})`)
788
+ .join('\n');
789
+ return {
790
+ content: [
791
+ {
792
+ type: 'text',
793
+ text: `Current Session: ${stats.session_name}
794
+ Session ID: ${sessionId.substring(0, 8)}
795
+ Created: ${stats.session_created}
796
+ Context Items: ${stats.item_count}
797
+ Cached Files: ${stats.file_count}
798
+
799
+ Recent Items:
800
+ ${recentList || ' None'}`,
801
+ },
802
+ ],
803
+ };
804
+ }
805
+ // Phase 2: Checkpoint System
806
+ case 'context_checkpoint': {
807
+ const { name, description, includeFiles = true, includeGitStatus = true } = args;
808
+ const sessionId = ensureSession();
809
+ const checkpointId = (0, uuid_1.v4)();
810
+ // Get git status if requested
811
+ let gitStatus = null;
812
+ let gitBranch = null;
813
+ if (includeGitStatus) {
814
+ const gitInfo = await getGitStatus();
815
+ gitStatus = gitInfo.status;
816
+ gitBranch = gitInfo.branch;
817
+ }
818
+ // Create checkpoint
819
+ db.prepare(`
820
+ INSERT INTO checkpoints (id, session_id, name, description, git_status, git_branch)
821
+ VALUES (?, ?, ?, ?, ?, ?)
822
+ `).run(checkpointId, sessionId, name, description || '', gitStatus, gitBranch);
823
+ // Save context items
824
+ const contextItems = db
825
+ .prepare('SELECT id FROM context_items WHERE session_id = ?')
826
+ .all(sessionId);
827
+ const itemStmt = db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)');
828
+ for (const item of contextItems) {
829
+ itemStmt.run((0, uuid_1.v4)(), checkpointId, item.id);
830
+ }
831
+ // Save file cache if requested
832
+ let fileCount = 0;
833
+ if (includeFiles) {
834
+ const files = db.prepare('SELECT id FROM file_cache WHERE session_id = ?').all(sessionId);
835
+ const fileStmt = db.prepare('INSERT INTO checkpoint_files (id, checkpoint_id, file_cache_id) VALUES (?, ?, ?)');
836
+ for (const file of files) {
837
+ fileStmt.run((0, uuid_1.v4)(), checkpointId, file.id);
838
+ fileCount++;
839
+ }
840
+ }
841
+ let statusText = `Created checkpoint: ${name}
842
+ ID: ${checkpointId.substring(0, 8)}
843
+ Context items: ${contextItems.length}
844
+ Cached files: ${fileCount}
845
+ Git branch: ${gitBranch || 'none'}
846
+ Git status: ${gitStatus ? 'captured' : 'not captured'}`;
847
+ // Add helpful message if git status was requested but no project directory is set
848
+ const currentSession = repositories.sessions.getById(sessionId);
849
+ if (includeGitStatus && (!currentSession || !currentSession.working_directory)) {
850
+ statusText += `\n\n💡 Note: Git status was requested but no project directory is set.
851
+ To enable git tracking, use context_set_project_dir with your project path.`;
852
+ }
853
+ return {
854
+ content: [
855
+ {
856
+ type: 'text',
857
+ text: statusText,
858
+ },
859
+ ],
860
+ };
861
+ }
862
+ case 'context_restore_checkpoint': {
863
+ const { name, checkpointId, restoreFiles = true } = args;
864
+ // Find checkpoint
865
+ let checkpoint;
866
+ if (checkpointId) {
867
+ checkpoint = db.prepare('SELECT * FROM checkpoints WHERE id = ?').get(checkpointId);
868
+ }
869
+ else if (name) {
870
+ checkpoint = db
871
+ .prepare('SELECT * FROM checkpoints ORDER BY created_at DESC')
872
+ .all()
873
+ .find((cp) => cp.name === name);
874
+ }
875
+ else {
876
+ // Get latest checkpoint
877
+ checkpoint = db.prepare('SELECT * FROM checkpoints ORDER BY created_at DESC LIMIT 1').get();
878
+ }
879
+ if (!checkpoint) {
880
+ return {
881
+ content: [
882
+ {
883
+ type: 'text',
884
+ text: 'No checkpoint found',
885
+ },
886
+ ],
887
+ };
888
+ }
889
+ const cp = checkpoint;
890
+ // Start new session from checkpoint
891
+ const newSessionId = (0, uuid_1.v4)();
892
+ db.prepare(`
893
+ INSERT INTO sessions (id, name, description, branch, working_directory)
894
+ VALUES (?, ?, ?, ?, ?)
895
+ `).run(newSessionId, `Restored from: ${cp.name}`, `Checkpoint ${cp.id.substring(0, 8)} created at ${cp.created_at}`, cp.git_branch, null);
896
+ // Restore context items
897
+ const contextItems = db
898
+ .prepare(`
899
+ SELECT ci.* FROM context_items ci
900
+ JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
901
+ WHERE cpi.checkpoint_id = ?
902
+ `)
903
+ .all(cp.id);
904
+ const itemStmt = db.prepare(`
905
+ INSERT INTO context_items (id, session_id, key, value, category, priority, size, created_at, updated_at)
906
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
907
+ `);
908
+ for (const item of contextItems) {
909
+ const itemData = item;
910
+ itemStmt.run((0, uuid_1.v4)(), newSessionId, itemData.key, itemData.value, itemData.category, itemData.priority, itemData.size || calculateSize(itemData.value), itemData.created_at);
911
+ }
912
+ // Restore file cache if requested
913
+ let fileCount = 0;
914
+ if (restoreFiles) {
915
+ const files = db
916
+ .prepare(`
917
+ SELECT fc.* FROM file_cache fc
918
+ JOIN checkpoint_files cpf ON fc.id = cpf.file_cache_id
919
+ WHERE cpf.checkpoint_id = ?
920
+ `)
921
+ .all(cp.id);
922
+ const fileStmt = db.prepare(`
923
+ INSERT INTO file_cache (id, session_id, file_path, content, hash, last_read)
924
+ VALUES (?, ?, ?, ?, ?, ?)
925
+ `);
926
+ for (const file of files) {
927
+ fileStmt.run((0, uuid_1.v4)(), newSessionId, file.file_path, file.content, file.hash, file.last_read);
928
+ fileCount++;
929
+ }
930
+ }
931
+ currentSessionId = newSessionId;
932
+ // Get session information for enhanced messaging
933
+ const sessionCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get();
934
+ const originalSession = db
935
+ .prepare('SELECT name FROM sessions WHERE id = ?')
936
+ .get(cp.session_id);
937
+ return {
938
+ content: [
939
+ {
940
+ type: 'text',
941
+ text: `✅ Successfully restored from checkpoint: ${cp.name}
942
+
943
+ 🔄 Data Safety: A new session was created to preserve your current work
944
+ 📋 New Session: ${newSessionId.substring(0, 8)} ("${`Restored from: ${cp.name}`}")
945
+ 🔙 Original Session: ${originalSession?.name || 'Unknown'} remains accessible
946
+
947
+ 📊 Restored Data:
948
+ - Context items: ${contextItems.length}
949
+ - Files: ${fileCount}
950
+ - Git branch: ${cp.git_branch || 'none'}
951
+ - Checkpoint created: ${cp.created_at}
952
+
953
+ 💡 Next Steps:
954
+ - You are now working in the restored session
955
+ - Your previous work is safely preserved in session ${sessionCount.count - 1}
956
+ - Use context_session_list to see all sessions
957
+ - Switch sessions anytime without losing data
958
+
959
+ 🆘 Need your previous work? Use context_search_all to find items across sessions`,
960
+ },
961
+ ],
962
+ };
963
+ }
964
+ // Phase 2: Summarization
965
+ case 'context_summarize': {
966
+ const { sessionId: specificSessionId, categories, maxLength } = args;
967
+ const targetSessionId = specificSessionId || currentSessionId || ensureSession();
968
+ const items = db
969
+ .prepare(`
970
+ SELECT * FROM context_items
971
+ WHERE session_id = ?
972
+ ORDER BY priority DESC, created_at DESC
973
+ `)
974
+ .all(targetSessionId);
975
+ const summary = createSummary(items, { categories, maxLength });
976
+ return {
977
+ content: [
978
+ {
979
+ type: 'text',
980
+ text: summary,
981
+ },
982
+ ],
983
+ };
984
+ }
985
+ // Phase 3: Smart Compaction Helper
986
+ case 'context_prepare_compaction': {
987
+ const sessionId = ensureSession();
988
+ // Get all high priority items
989
+ const highPriorityItems = db
990
+ .prepare(`
991
+ SELECT * FROM context_items
992
+ WHERE session_id = ? AND priority = 'high'
993
+ ORDER BY created_at DESC
994
+ `)
995
+ .all(sessionId);
996
+ // Get recent tasks
997
+ const recentTasks = db
998
+ .prepare(`
999
+ SELECT * FROM context_items
1000
+ WHERE session_id = ? AND category = 'task'
1001
+ ORDER BY created_at DESC LIMIT 10
1002
+ `)
1003
+ .all(sessionId);
1004
+ // Get all decisions
1005
+ const decisions = db
1006
+ .prepare(`
1007
+ SELECT * FROM context_items
1008
+ WHERE session_id = ? AND category = 'decision'
1009
+ ORDER BY created_at DESC
1010
+ `)
1011
+ .all(sessionId);
1012
+ // Get files that changed
1013
+ const changedFiles = db
1014
+ .prepare(`
1015
+ SELECT file_path, hash FROM file_cache
1016
+ WHERE session_id = ?
1017
+ `)
1018
+ .all(sessionId);
1019
+ // Auto-create checkpoint
1020
+ const checkpointId = (0, uuid_1.v4)();
1021
+ const checkpointName = `auto-compaction-${new Date().toISOString()}`;
1022
+ const gitInfo = await getGitStatus();
1023
+ db.prepare(`
1024
+ INSERT INTO checkpoints (id, session_id, name, description, git_status, git_branch)
1025
+ VALUES (?, ?, ?, ?, ?, ?)
1026
+ `).run(checkpointId, sessionId, checkpointName, 'Automatic checkpoint before compaction', gitInfo.status, gitInfo.branch);
1027
+ // Save all context items to checkpoint
1028
+ const allItems = db
1029
+ .prepare('SELECT id FROM context_items WHERE session_id = ?')
1030
+ .all(sessionId);
1031
+ const itemStmt = db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)');
1032
+ for (const item of allItems) {
1033
+ itemStmt.run((0, uuid_1.v4)(), checkpointId, item.id);
1034
+ }
1035
+ // Generate summary for next session
1036
+ const summary = createSummary([...highPriorityItems, ...recentTasks, ...decisions], {
1037
+ maxLength: 2000,
1038
+ });
1039
+ // Determine next steps
1040
+ const nextSteps = [];
1041
+ const unfinishedTasks = recentTasks.filter((t) => !t.value.toLowerCase().includes('completed') && !t.value.toLowerCase().includes('done'));
1042
+ unfinishedTasks.forEach((task) => {
1043
+ nextSteps.push(`Continue: ${task.key}`);
1044
+ });
1045
+ // Save prepared context
1046
+ const preparedContext = {
1047
+ checkpoint: checkpointName,
1048
+ summary,
1049
+ nextSteps,
1050
+ criticalItems: highPriorityItems.map((i) => ({ key: i.key, value: i.value })),
1051
+ decisions: decisions.map((d) => ({ key: d.key, value: d.value })),
1052
+ filesModified: changedFiles.length,
1053
+ gitBranch: gitInfo.branch,
1054
+ };
1055
+ // Save as special context item
1056
+ const preparedValue = JSON.stringify(preparedContext);
1057
+ db.prepare(`
1058
+ INSERT OR REPLACE INTO context_items (id, session_id, key, value, category, priority, size, updated_at)
1059
+ VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1060
+ `).run((0, uuid_1.v4)(), sessionId, '_prepared_compaction', preparedValue, 'system', 'high', calculateSize(preparedValue));
1061
+ return {
1062
+ content: [
1063
+ {
1064
+ type: 'text',
1065
+ text: `Prepared for compaction:
1066
+
1067
+ Checkpoint: ${checkpointName}
1068
+ Critical items saved: ${highPriorityItems.length}
1069
+ Decisions preserved: ${decisions.length}
1070
+ Next steps identified: ${nextSteps.length}
1071
+ Files tracked: ${changedFiles.length}
1072
+
1073
+ Summary:
1074
+ ${summary.substring(0, 500)}${summary.length > 500 ? '...' : ''}
1075
+
1076
+ Next Steps:
1077
+ ${nextSteps.join('\n')}
1078
+
1079
+ To restore after compaction:
1080
+ mcp_context_restore_checkpoint({ name: "${checkpointName}" })`,
1081
+ },
1082
+ ],
1083
+ };
1084
+ }
1085
+ // Phase 3: Git Integration
1086
+ case 'context_git_commit': {
1087
+ const { message, autoSave = true } = args;
1088
+ const sessionId = ensureSession();
1089
+ // Check if project directory is set for this session
1090
+ const session = repositories.sessions.getById(sessionId);
1091
+ if (!session || !session.working_directory) {
1092
+ return {
1093
+ content: [
1094
+ {
1095
+ type: 'text',
1096
+ text: getProjectDirectorySetupMessage(),
1097
+ },
1098
+ ],
1099
+ };
1100
+ }
1101
+ if (autoSave) {
1102
+ // Save current context state
1103
+ const timestamp = new Date().toISOString();
1104
+ const commitValue = message || 'No commit message';
1105
+ db.prepare(`
1106
+ INSERT OR REPLACE INTO context_items (id, session_id, key, value, category, priority, size, updated_at)
1107
+ VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1108
+ `).run((0, uuid_1.v4)(), sessionId, `commit_${timestamp}`, commitValue, 'git', 'normal', calculateSize(commitValue));
1109
+ // Create checkpoint
1110
+ const checkpointId = (0, uuid_1.v4)();
1111
+ const checkpointName = `git-commit-${timestamp}`;
1112
+ const gitInfo = await getGitStatus();
1113
+ db.prepare(`
1114
+ INSERT INTO checkpoints (id, session_id, name, description, git_status, git_branch)
1115
+ VALUES (?, ?, ?, ?, ?, ?)
1116
+ `).run(checkpointId, sessionId, checkpointName, `Git commit: ${message || 'No message'}`, gitInfo.status, gitInfo.branch);
1117
+ // Link current context to checkpoint
1118
+ const items = db
1119
+ .prepare('SELECT id FROM context_items WHERE session_id = ?')
1120
+ .all(sessionId);
1121
+ const itemStmt = db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)');
1122
+ for (const item of items) {
1123
+ itemStmt.run((0, uuid_1.v4)(), checkpointId, item.id);
1124
+ }
1125
+ }
1126
+ // Execute git commit
1127
+ try {
1128
+ const git = (0, simple_git_1.simpleGit)(session.working_directory);
1129
+ await git.add('.');
1130
+ const commitResult = await git.commit(message || 'Commit via Memory Keeper');
1131
+ return {
1132
+ content: [
1133
+ {
1134
+ type: 'text',
1135
+ text: `Git commit successful!
1136
+ Commit: ${commitResult.commit}
1137
+ Context saved: ${autoSave ? 'Yes' : 'No'}
1138
+ Checkpoint: ${autoSave ? `git-commit-${new Date().toISOString()}` : 'None'}`,
1139
+ },
1140
+ ],
1141
+ };
1142
+ }
1143
+ catch (error) {
1144
+ return {
1145
+ content: [
1146
+ {
1147
+ type: 'text',
1148
+ text: `Git commit failed: ${error.message}`,
1149
+ },
1150
+ ],
1151
+ };
1152
+ }
1153
+ }
1154
+ // Phase 3: Context Search
1155
+ case 'context_search': {
1156
+ const { query, searchIn = ['key', 'value'], sessionId: specificSessionId, category, channel, channels, sort, limit, offset, createdAfter, createdBefore, keyPattern, priorities, includeMetadata, } = args;
1157
+ const targetSessionId = specificSessionId || currentSessionId || ensureSession();
1158
+ // Use enhanced search for all cases
1159
+ const result = repositories.contexts.searchEnhanced({
1160
+ query,
1161
+ sessionId: targetSessionId,
1162
+ searchIn,
1163
+ category,
1164
+ channel,
1165
+ channels,
1166
+ sort,
1167
+ limit,
1168
+ offset,
1169
+ createdAfter,
1170
+ createdBefore,
1171
+ keyPattern,
1172
+ priorities,
1173
+ includeMetadata,
1174
+ });
1175
+ if (result.items.length === 0) {
1176
+ return {
1177
+ content: [
1178
+ {
1179
+ type: 'text',
1180
+ text: `No results found for: "${query}"`,
1181
+ },
1182
+ ],
1183
+ };
1184
+ }
1185
+ // Enhanced response format with metadata
1186
+ if (includeMetadata) {
1187
+ const itemsWithMetadata = result.items.map(item => ({
1188
+ key: item.key,
1189
+ value: item.value,
1190
+ category: item.category,
1191
+ priority: item.priority,
1192
+ channel: item.channel,
1193
+ metadata: item.metadata ? JSON.parse(item.metadata) : null,
1194
+ size: item.size || calculateSize(item.value),
1195
+ created_at: item.created_at,
1196
+ updated_at: item.updated_at,
1197
+ }));
1198
+ return {
1199
+ content: [
1200
+ {
1201
+ type: 'text',
1202
+ text: JSON.stringify({
1203
+ items: itemsWithMetadata,
1204
+ totalCount: result.totalCount,
1205
+ page: offset && limit ? Math.floor(offset / limit) + 1 : 1,
1206
+ pageSize: limit || result.items.length,
1207
+ query,
1208
+ }, null, 2),
1209
+ },
1210
+ ],
1211
+ };
1212
+ }
1213
+ // Backward compatible format
1214
+ const resultText = result.items
1215
+ .map((r) => `• [${r.priority}] ${r.key} (${r.category || 'none'})\n ${r.value.substring(0, 100)}${r.value.length > 100 ? '...' : ''}`)
1216
+ .join('\n\n');
1217
+ return {
1218
+ content: [
1219
+ {
1220
+ type: 'text',
1221
+ text: `Found ${result.items.length} results for "${query}":\n\n${resultText}`,
1222
+ },
1223
+ ],
1224
+ };
1225
+ }
1226
+ // Phase 3: Export/Import
1227
+ case 'context_export': {
1228
+ const { sessionId: specificSessionId, format = 'json', includeStats = false, confirmEmpty = false, } = args;
1229
+ const targetSessionId = specificSessionId || currentSessionId;
1230
+ // Phase 1: Validation
1231
+ if (!targetSessionId) {
1232
+ throw new Error('No session ID provided and no current session active');
1233
+ }
1234
+ // Check if session exists
1235
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(targetSessionId);
1236
+ if (!session) {
1237
+ throw new Error(`Session not found: ${targetSessionId}`);
1238
+ }
1239
+ // Get session data
1240
+ const contextItems = db
1241
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
1242
+ .all(targetSessionId);
1243
+ const fileCache = db
1244
+ .prepare('SELECT * FROM file_cache WHERE session_id = ?')
1245
+ .all(targetSessionId);
1246
+ const checkpoints = db
1247
+ .prepare('SELECT * FROM checkpoints WHERE session_id = ?')
1248
+ .all(targetSessionId);
1249
+ // Check if session is empty
1250
+ const isEmpty = contextItems.length === 0 && fileCache.length === 0 && checkpoints.length === 0;
1251
+ if (isEmpty && !confirmEmpty) {
1252
+ return {
1253
+ content: [
1254
+ {
1255
+ type: 'text',
1256
+ text: 'Warning: Session appears to be empty. No context items, files, or checkpoints found.\n\nTo export anyway, use confirmEmpty: true',
1257
+ },
1258
+ ],
1259
+ isEmpty: true,
1260
+ requiresConfirmation: true,
1261
+ };
1262
+ }
1263
+ const exportData = {
1264
+ version: '0.4.0',
1265
+ exported: new Date().toISOString(),
1266
+ session,
1267
+ contextItems,
1268
+ fileCache,
1269
+ checkpoints,
1270
+ metadata: {
1271
+ itemCount: contextItems.length,
1272
+ fileCount: fileCache.length,
1273
+ checkpointCount: checkpoints.length,
1274
+ totalSize: JSON.stringify({ contextItems, fileCache, checkpoints }).length,
1275
+ },
1276
+ };
1277
+ if (format === 'json') {
1278
+ const exportPath = path.join(os.tmpdir(), `memory-keeper-export-${targetSessionId.substring(0, 8)}.json`);
1279
+ // Check write permissions
1280
+ try {
1281
+ fs.writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
1282
+ }
1283
+ catch (error) {
1284
+ if (error.code === 'EACCES') {
1285
+ throw new Error(`Permission denied: Cannot write to ${exportPath}`);
1286
+ }
1287
+ throw error;
1288
+ }
1289
+ const stats = {
1290
+ items: contextItems.length,
1291
+ files: fileCache.length,
1292
+ checkpoints: checkpoints.length,
1293
+ size: fs.statSync(exportPath).size,
1294
+ };
1295
+ return {
1296
+ content: [
1297
+ {
1298
+ type: 'text',
1299
+ text: includeStats
1300
+ ? `✅ Successfully exported session "${session.name}" to: ${exportPath}
1301
+
1302
+ 📊 Export Statistics:
1303
+ - Context Items: ${stats.items}
1304
+ - Cached Files: ${stats.files}
1305
+ - Checkpoints: ${stats.checkpoints}
1306
+ - Export Size: ${(stats.size / 1024).toFixed(2)} KB
1307
+
1308
+ Session ID: ${targetSessionId}`
1309
+ : `Exported session to: ${exportPath}
1310
+ Items: ${stats.items}
1311
+ Files: ${stats.files}`,
1312
+ },
1313
+ ],
1314
+ exportPath,
1315
+ statistics: stats,
1316
+ };
1317
+ }
1318
+ // Inline format
1319
+ return {
1320
+ content: [
1321
+ {
1322
+ type: 'text',
1323
+ text: JSON.stringify(exportData, null, 2),
1324
+ },
1325
+ ],
1326
+ statistics: {
1327
+ items: contextItems.length,
1328
+ files: fileCache.length,
1329
+ checkpoints: checkpoints.length,
1330
+ },
1331
+ };
1332
+ }
1333
+ case 'context_import': {
1334
+ const { filePath, merge = false } = args;
1335
+ try {
1336
+ const importData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
1337
+ // Create new session or merge
1338
+ let targetSessionId;
1339
+ if (merge && currentSessionId) {
1340
+ targetSessionId = currentSessionId;
1341
+ }
1342
+ else {
1343
+ targetSessionId = (0, uuid_1.v4)();
1344
+ const importedSession = importData.session;
1345
+ db.prepare(`
1346
+ INSERT INTO sessions (id, name, description, branch, working_directory, created_at)
1347
+ VALUES (?, ?, ?, ?, ?, ?)
1348
+ `).run(targetSessionId, `Imported: ${importedSession.name}`, `Imported from ${filePath} on ${new Date().toISOString()}`, importedSession.branch, null, new Date().toISOString());
1349
+ currentSessionId = targetSessionId;
1350
+ }
1351
+ // Import context items
1352
+ const itemStmt = db.prepare(`
1353
+ INSERT OR REPLACE INTO context_items (id, session_id, key, value, category, priority, size, created_at, updated_at)
1354
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1355
+ `);
1356
+ let itemCount = 0;
1357
+ for (const item of importData.contextItems) {
1358
+ itemStmt.run((0, uuid_1.v4)(), targetSessionId, item.key, item.value, item.category, item.priority, item.size || calculateSize(item.value), item.created_at);
1359
+ itemCount++;
1360
+ }
1361
+ // Import file cache
1362
+ const fileStmt = db.prepare(`
1363
+ INSERT OR REPLACE INTO file_cache (id, session_id, file_path, content, hash, last_read)
1364
+ VALUES (?, ?, ?, ?, ?, ?)
1365
+ `);
1366
+ let fileCount = 0;
1367
+ for (const file of importData.fileCache || []) {
1368
+ fileStmt.run((0, uuid_1.v4)(), targetSessionId, file.file_path, file.content, file.hash, file.last_read);
1369
+ fileCount++;
1370
+ }
1371
+ return {
1372
+ content: [
1373
+ {
1374
+ type: 'text',
1375
+ text: `Import successful!
1376
+ Session: ${targetSessionId.substring(0, 8)}
1377
+ Context items: ${itemCount}
1378
+ Files: ${fileCount}
1379
+ Mode: ${merge ? 'Merged' : 'New session'}`,
1380
+ },
1381
+ ],
1382
+ };
1383
+ }
1384
+ catch (error) {
1385
+ return {
1386
+ content: [
1387
+ {
1388
+ type: 'text',
1389
+ text: `Import failed: ${error.message}`,
1390
+ },
1391
+ ],
1392
+ };
1393
+ }
1394
+ }
1395
+ // Phase 4.1: Knowledge Graph Tools
1396
+ case 'context_analyze': {
1397
+ const { sessionId, categories } = args;
1398
+ const targetSessionId = sessionId || ensureSession();
1399
+ try {
1400
+ // Get context items to analyze
1401
+ let query = 'SELECT * FROM context_items WHERE session_id = ?';
1402
+ const params = [targetSessionId];
1403
+ if (categories && categories.length > 0) {
1404
+ query += ` AND category IN (${categories.map(() => '?').join(',')})`;
1405
+ params.push(...categories);
1406
+ }
1407
+ const items = db.prepare(query).all(...params);
1408
+ let entitiesCreated = 0;
1409
+ let relationsCreated = 0;
1410
+ // Analyze each context item
1411
+ for (const item of items) {
1412
+ const analysis = knowledgeGraph.analyzeContext(targetSessionId, item.value);
1413
+ // Create entities
1414
+ for (const entityData of analysis.entities) {
1415
+ const existing = knowledgeGraph.findEntity(targetSessionId, entityData.name, entityData.type);
1416
+ if (!existing) {
1417
+ knowledgeGraph.createEntity(targetSessionId, entityData.type, entityData.name, {
1418
+ confidence: entityData.confidence,
1419
+ source: item.key,
1420
+ });
1421
+ entitiesCreated++;
1422
+ }
1423
+ }
1424
+ // Create relations
1425
+ for (const relationData of analysis.relations) {
1426
+ const subject = knowledgeGraph.findEntity(targetSessionId, relationData.subject);
1427
+ const object = knowledgeGraph.findEntity(targetSessionId, relationData.object);
1428
+ if (subject && object) {
1429
+ knowledgeGraph.createRelation(targetSessionId, subject.id, relationData.predicate, object.id, relationData.confidence);
1430
+ relationsCreated++;
1431
+ }
1432
+ }
1433
+ }
1434
+ // Get summary statistics
1435
+ const entityStats = db
1436
+ .prepare(`
1437
+ SELECT type, COUNT(*) as count
1438
+ FROM entities
1439
+ WHERE session_id = ?
1440
+ GROUP BY type
1441
+ `)
1442
+ .all(targetSessionId);
1443
+ return {
1444
+ content: [
1445
+ {
1446
+ type: 'text',
1447
+ text: `Analysis complete!
1448
+ Items analyzed: ${items.length}
1449
+ Entities created: ${entitiesCreated}
1450
+ Relations created: ${relationsCreated}
1451
+
1452
+ Entity breakdown:
1453
+ ${entityStats.map(s => `- ${s.type}: ${s.count}`).join('\n')}`,
1454
+ },
1455
+ ],
1456
+ };
1457
+ }
1458
+ catch (error) {
1459
+ return {
1460
+ content: [
1461
+ {
1462
+ type: 'text',
1463
+ text: `Analysis failed: ${error.message}`,
1464
+ },
1465
+ ],
1466
+ };
1467
+ }
1468
+ }
1469
+ case 'context_find_related': {
1470
+ const { key, relationTypes, maxDepth = 2 } = args;
1471
+ const sessionId = ensureSession();
1472
+ try {
1473
+ // First try to find as entity
1474
+ let entity = knowledgeGraph.findEntity(sessionId, key);
1475
+ // If not found as entity, check if it's a context key
1476
+ if (!entity) {
1477
+ const contextItem = db
1478
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
1479
+ .get(sessionId, key);
1480
+ if (contextItem) {
1481
+ // Try to extract entities from the context value
1482
+ const analysis = knowledgeGraph.analyzeContext(sessionId, contextItem.value);
1483
+ if (analysis.entities.length > 0) {
1484
+ entity = knowledgeGraph.findEntity(sessionId, analysis.entities[0].name);
1485
+ }
1486
+ }
1487
+ }
1488
+ if (!entity) {
1489
+ return {
1490
+ content: [
1491
+ {
1492
+ type: 'text',
1493
+ text: `No entity found for key: ${key}`,
1494
+ },
1495
+ ],
1496
+ };
1497
+ }
1498
+ // Get connected entities
1499
+ const connectedIds = knowledgeGraph.getConnectedEntities(entity.id, maxDepth);
1500
+ // Get details for connected entities
1501
+ const entities = Array.from(connectedIds).map(id => {
1502
+ const entityData = db.prepare('SELECT * FROM entities WHERE id = ?').get(id);
1503
+ const relations = knowledgeGraph.getRelations(id);
1504
+ const observations = knowledgeGraph.getObservations(id);
1505
+ return {
1506
+ ...entityData,
1507
+ attributes: entityData.attributes ? JSON.parse(entityData.attributes) : {},
1508
+ relations: relations.length,
1509
+ observations: observations.length,
1510
+ };
1511
+ });
1512
+ // Filter by relation types if specified
1513
+ let relevantRelations = knowledgeGraph.getRelations(entity.id);
1514
+ if (relationTypes && relationTypes.length > 0) {
1515
+ relevantRelations = relevantRelations.filter(r => relationTypes.includes(r.predicate));
1516
+ }
1517
+ return {
1518
+ content: [
1519
+ {
1520
+ type: 'text',
1521
+ text: `Related entities for "${key}":
1522
+
1523
+ Found ${entities.length} connected entities (max depth: ${maxDepth})
1524
+
1525
+ Main entity:
1526
+ - Type: ${entity.type}
1527
+ - Name: ${entity.name}
1528
+ - Direct relations: ${relevantRelations.length}
1529
+
1530
+ Connected entities:
1531
+ ${entities
1532
+ .slice(0, 20)
1533
+ .map(e => `- ${e.type}: ${e.name} (${e.relations} relations, ${e.observations} observations)`)
1534
+ .join('\n')}
1535
+ ${entities.length > 20 ? `\n... and ${entities.length - 20} more` : ''}`,
1536
+ },
1537
+ ],
1538
+ };
1539
+ }
1540
+ catch (error) {
1541
+ return {
1542
+ content: [
1543
+ {
1544
+ type: 'text',
1545
+ text: `Find related failed: ${error.message}`,
1546
+ },
1547
+ ],
1548
+ };
1549
+ }
1550
+ }
1551
+ case 'context_visualize': {
1552
+ const { type = 'graph', entityTypes, sessionId } = args;
1553
+ const targetSessionId = sessionId || ensureSession();
1554
+ try {
1555
+ if (type === 'graph') {
1556
+ const graphData = knowledgeGraph.getGraphData(targetSessionId, entityTypes);
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: 'text',
1561
+ text: JSON.stringify(graphData, null, 2),
1562
+ },
1563
+ ],
1564
+ };
1565
+ }
1566
+ else if (type === 'timeline') {
1567
+ // Get time-based data
1568
+ const timeline = db
1569
+ .prepare(`
1570
+ SELECT
1571
+ strftime('%Y-%m-%d %H:00', created_at) as hour,
1572
+ COUNT(*) as events,
1573
+ GROUP_CONCAT(DISTINCT category) as categories
1574
+ FROM context_items
1575
+ WHERE session_id = ?
1576
+ GROUP BY hour
1577
+ ORDER BY hour DESC
1578
+ LIMIT 24
1579
+ `)
1580
+ .all(targetSessionId);
1581
+ return {
1582
+ content: [
1583
+ {
1584
+ type: 'text',
1585
+ text: JSON.stringify({
1586
+ type: 'timeline',
1587
+ data: timeline,
1588
+ }, null, 2),
1589
+ },
1590
+ ],
1591
+ };
1592
+ }
1593
+ else if (type === 'heatmap') {
1594
+ // Get category/priority heatmap data
1595
+ const heatmap = db
1596
+ .prepare(`
1597
+ SELECT
1598
+ category,
1599
+ priority,
1600
+ COUNT(*) as count
1601
+ FROM context_items
1602
+ WHERE session_id = ?
1603
+ GROUP BY category, priority
1604
+ `)
1605
+ .all(targetSessionId);
1606
+ return {
1607
+ content: [
1608
+ {
1609
+ type: 'text',
1610
+ text: JSON.stringify({
1611
+ type: 'heatmap',
1612
+ data: heatmap,
1613
+ }, null, 2),
1614
+ },
1615
+ ],
1616
+ };
1617
+ }
1618
+ return {
1619
+ content: [
1620
+ {
1621
+ type: 'text',
1622
+ text: `Unknown visualization type: ${type}`,
1623
+ },
1624
+ ],
1625
+ };
1626
+ }
1627
+ catch (error) {
1628
+ return {
1629
+ content: [
1630
+ {
1631
+ type: 'text',
1632
+ text: `Visualization failed: ${error.message}`,
1633
+ },
1634
+ ],
1635
+ };
1636
+ }
1637
+ }
1638
+ // Phase 4.2: Semantic Search
1639
+ case 'context_semantic_search': {
1640
+ const { query, topK = 10, minSimilarity = 0.3, sessionId } = args;
1641
+ const targetSessionId = sessionId || ensureSession();
1642
+ try {
1643
+ // Ensure embeddings are up to date for the session
1644
+ const _embeddingCount = await vectorStore.updateSessionEmbeddings(targetSessionId);
1645
+ // Perform semantic search
1646
+ const results = await vectorStore.searchInSession(targetSessionId, query, topK, minSimilarity);
1647
+ if (results.length === 0) {
1648
+ return {
1649
+ content: [
1650
+ {
1651
+ type: 'text',
1652
+ text: `No results found for query: "${query}"`,
1653
+ },
1654
+ ],
1655
+ };
1656
+ }
1657
+ // Format results
1658
+ let response = `Found ${results.length} results for: "${query}"\n\n`;
1659
+ results.forEach((result, index) => {
1660
+ const similarity = (result.similarity * 100).toFixed(1);
1661
+ response += `${index + 1}. [${similarity}% match]\n`;
1662
+ // Extract key and value from content
1663
+ const colonIndex = result.content.indexOf(':');
1664
+ if (colonIndex > -1) {
1665
+ const key = result.content.substring(0, colonIndex);
1666
+ const value = result.content.substring(colonIndex + 1).trim();
1667
+ response += ` Key: ${key}\n`;
1668
+ response += ` Value: ${value.substring(0, 200)}${value.length > 200 ? '...' : ''}\n`;
1669
+ }
1670
+ else {
1671
+ response += ` ${result.content.substring(0, 200)}${result.content.length > 200 ? '...' : ''}\n`;
1672
+ }
1673
+ if (result.metadata) {
1674
+ if (result.metadata.category) {
1675
+ response += ` Category: ${result.metadata.category}`;
1676
+ }
1677
+ if (result.metadata.priority) {
1678
+ response += `, Priority: ${result.metadata.priority}`;
1679
+ }
1680
+ response += '\n';
1681
+ }
1682
+ response += '\n';
1683
+ });
1684
+ return {
1685
+ content: [
1686
+ {
1687
+ type: 'text',
1688
+ text: response,
1689
+ },
1690
+ ],
1691
+ };
1692
+ }
1693
+ catch (error) {
1694
+ return {
1695
+ content: [
1696
+ {
1697
+ type: 'text',
1698
+ text: `Semantic search failed: ${error.message}`,
1699
+ },
1700
+ ],
1701
+ };
1702
+ }
1703
+ }
1704
+ // Phase 4.3: Multi-Agent System
1705
+ case 'context_delegate': {
1706
+ const { taskType, input, sessionId, chain = false } = args;
1707
+ const targetSessionId = sessionId || ensureSession();
1708
+ try {
1709
+ // Create agent task
1710
+ const task = {
1711
+ id: (0, uuid_1.v4)(),
1712
+ type: taskType,
1713
+ input: {
1714
+ ...input,
1715
+ sessionId: targetSessionId,
1716
+ },
1717
+ };
1718
+ // Process with agents
1719
+ let results;
1720
+ if (chain && Array.isArray(input)) {
1721
+ // Process as a chain of tasks
1722
+ const tasks = input.map((inp, index) => ({
1723
+ id: (0, uuid_1.v4)(),
1724
+ type: Array.isArray(taskType) ? taskType[index] : taskType,
1725
+ input: { ...inp, sessionId: targetSessionId },
1726
+ }));
1727
+ results = await agentCoordinator.processChain(tasks);
1728
+ }
1729
+ else {
1730
+ // Single task delegation
1731
+ results = await agentCoordinator.delegate(task);
1732
+ }
1733
+ // Format response
1734
+ let response = `Agent Processing Results:\n\n`;
1735
+ for (const result of results) {
1736
+ response += `## ${result.agentType.toUpperCase()} Agent\n`;
1737
+ response += `Confidence: ${(result.confidence * 100).toFixed(0)}%\n`;
1738
+ response += `Processing Time: ${result.processingTime}ms\n`;
1739
+ if (result.reasoning) {
1740
+ response += `Reasoning: ${result.reasoning}\n`;
1741
+ }
1742
+ response += `\nOutput:\n`;
1743
+ response += JSON.stringify(result.output, null, 2);
1744
+ response += '\n\n---\n\n';
1745
+ }
1746
+ // Get best result if multiple agents processed
1747
+ if (results.length > 1) {
1748
+ const best = agentCoordinator.getBestResult(task.id);
1749
+ if (best) {
1750
+ response += `\n## Best Result (${best.agentType}, ${(best.confidence * 100).toFixed(0)}% confidence):\n`;
1751
+ response += JSON.stringify(best.output, null, 2);
1752
+ }
1753
+ }
1754
+ return {
1755
+ content: [
1756
+ {
1757
+ type: 'text',
1758
+ text: response,
1759
+ },
1760
+ ],
1761
+ };
1762
+ }
1763
+ catch (error) {
1764
+ return {
1765
+ content: [
1766
+ {
1767
+ type: 'text',
1768
+ text: `Agent delegation failed: ${error.message}`,
1769
+ },
1770
+ ],
1771
+ };
1772
+ }
1773
+ }
1774
+ // Phase 4.4: Session Branching
1775
+ case 'context_branch_session': {
1776
+ const { branchName, copyDepth = 'shallow' } = args;
1777
+ const sourceSessionId = ensureSession();
1778
+ try {
1779
+ // Get source session info
1780
+ const sourceSession = db
1781
+ .prepare('SELECT * FROM sessions WHERE id = ?')
1782
+ .get(sourceSessionId);
1783
+ if (!sourceSession) {
1784
+ throw new Error('Source session not found');
1785
+ }
1786
+ // Create new branch session
1787
+ const branchId = (0, uuid_1.v4)();
1788
+ db.prepare(`
1789
+ INSERT INTO sessions (id, name, description, branch, working_directory, parent_id)
1790
+ VALUES (?, ?, ?, ?, ?, ?)
1791
+ `).run(branchId, branchName, `Branch of ${sourceSession.name} created at ${new Date().toISOString()}`, sourceSession.branch, null, sourceSessionId);
1792
+ if (copyDepth === 'deep') {
1793
+ // Copy all context items
1794
+ const items = db
1795
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
1796
+ .all(sourceSessionId);
1797
+ const stmt = db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
1798
+ for (const item of items) {
1799
+ stmt.run((0, uuid_1.v4)(), branchId, item.key, item.value, item.category, item.priority, item.created_at);
1800
+ }
1801
+ // Copy file cache
1802
+ const files = db
1803
+ .prepare('SELECT * FROM file_cache WHERE session_id = ?')
1804
+ .all(sourceSessionId);
1805
+ const fileStmt = db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash, last_read) VALUES (?, ?, ?, ?, ?, ?)');
1806
+ for (const file of files) {
1807
+ fileStmt.run((0, uuid_1.v4)(), branchId, file.file_path, file.content, file.hash, file.last_read);
1808
+ }
1809
+ }
1810
+ else {
1811
+ // Shallow copy - only copy high priority items
1812
+ const items = db
1813
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND priority = ?')
1814
+ .all(sourceSessionId, 'high');
1815
+ const stmt = db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
1816
+ for (const item of items) {
1817
+ stmt.run((0, uuid_1.v4)(), branchId, item.key, item.value, item.category, item.priority, item.created_at);
1818
+ }
1819
+ }
1820
+ // Switch to the new branch
1821
+ currentSessionId = branchId;
1822
+ return {
1823
+ content: [
1824
+ {
1825
+ type: 'text',
1826
+ text: `Created branch session: ${branchName}
1827
+ ID: ${branchId}
1828
+ Parent: ${sourceSession.name} (${sourceSessionId.substring(0, 8)})
1829
+ Copy depth: ${copyDepth}
1830
+ Items copied: ${copyDepth === 'deep' ? 'All' : 'High priority only'}
1831
+
1832
+ Now working in branch: ${branchName}`,
1833
+ },
1834
+ ],
1835
+ };
1836
+ }
1837
+ catch (error) {
1838
+ return {
1839
+ content: [
1840
+ {
1841
+ type: 'text',
1842
+ text: `Branch creation failed: ${error.message}`,
1843
+ },
1844
+ ],
1845
+ };
1846
+ }
1847
+ }
1848
+ // Phase 4.4: Session Merging
1849
+ case 'context_merge_sessions': {
1850
+ const { sourceSessionId, conflictResolution = 'keep_current' } = args;
1851
+ const targetSessionId = ensureSession();
1852
+ try {
1853
+ // Get both sessions
1854
+ const sourceSession = db
1855
+ .prepare('SELECT * FROM sessions WHERE id = ?')
1856
+ .get(sourceSessionId);
1857
+ const targetSession = db
1858
+ .prepare('SELECT * FROM sessions WHERE id = ?')
1859
+ .get(targetSessionId);
1860
+ if (!sourceSession) {
1861
+ throw new Error('Source session not found');
1862
+ }
1863
+ // Get items from source session
1864
+ const sourceItems = db
1865
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
1866
+ .all(sourceSessionId);
1867
+ let merged = 0;
1868
+ let skipped = 0;
1869
+ for (const item of sourceItems) {
1870
+ // Check if item exists in target
1871
+ const existing = db
1872
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
1873
+ .get(targetSessionId, item.key);
1874
+ if (existing) {
1875
+ // Handle conflict
1876
+ if (conflictResolution === 'keep_source' ||
1877
+ (conflictResolution === 'keep_newest' &&
1878
+ new Date(item.created_at) > new Date(existing.created_at))) {
1879
+ db.prepare('UPDATE context_items SET value = ?, category = ?, priority = ? WHERE session_id = ? AND key = ?').run(item.value, item.category, item.priority, targetSessionId, item.key);
1880
+ merged++;
1881
+ }
1882
+ else {
1883
+ skipped++;
1884
+ }
1885
+ }
1886
+ else {
1887
+ // No conflict, insert item
1888
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), targetSessionId, item.key, item.value, item.category, item.priority, item.created_at);
1889
+ merged++;
1890
+ }
1891
+ }
1892
+ return {
1893
+ content: [
1894
+ {
1895
+ type: 'text',
1896
+ text: `Merge completed!
1897
+ Source: ${sourceSession.name} (${sourceSessionId.substring(0, 8)})
1898
+ Target: ${targetSession.name} (${targetSessionId.substring(0, 8)})
1899
+ Items merged: ${merged}
1900
+ Items skipped: ${skipped}
1901
+ Conflict resolution: ${conflictResolution}`,
1902
+ },
1903
+ ],
1904
+ };
1905
+ }
1906
+ catch (error) {
1907
+ return {
1908
+ content: [
1909
+ {
1910
+ type: 'text',
1911
+ text: `Session merge failed: ${error.message}`,
1912
+ },
1913
+ ],
1914
+ };
1915
+ }
1916
+ }
1917
+ // Phase 4.4: Journal Entry
1918
+ case 'context_journal_entry': {
1919
+ const { entry, tags = [], mood } = args;
1920
+ const sessionId = ensureSession();
1921
+ try {
1922
+ const id = (0, uuid_1.v4)();
1923
+ db.prepare(`
1924
+ INSERT INTO journal_entries (id, session_id, entry, tags, mood)
1925
+ VALUES (?, ?, ?, ?, ?)
1926
+ `).run(id, sessionId, entry, JSON.stringify(tags), mood);
1927
+ return {
1928
+ content: [
1929
+ {
1930
+ type: 'text',
1931
+ text: `Journal entry added!
1932
+ Time: ${new Date().toISOString()}
1933
+ Mood: ${mood || 'not specified'}
1934
+ Tags: ${tags.join(', ') || 'none'}
1935
+ Entry saved with ID: ${id.substring(0, 8)}`,
1936
+ },
1937
+ ],
1938
+ };
1939
+ }
1940
+ catch (error) {
1941
+ return {
1942
+ content: [
1943
+ {
1944
+ type: 'text',
1945
+ text: `Journal entry failed: ${error.message}`,
1946
+ },
1947
+ ],
1948
+ };
1949
+ }
1950
+ }
1951
+ // Phase 4.4: Timeline
1952
+ case 'context_timeline': {
1953
+ const { startDate, endDate, groupBy = 'day', sessionId, categories, relativeTime, itemsPerPeriod, includeItems, minItemsPerPeriod, showEmpty, } = args;
1954
+ const targetSessionId = sessionId || ensureSession();
1955
+ try {
1956
+ // Use the enhanced timeline method
1957
+ const timeline = repositories.contexts.getTimelineData({
1958
+ sessionId: targetSessionId,
1959
+ startDate,
1960
+ endDate,
1961
+ categories,
1962
+ relativeTime,
1963
+ itemsPerPeriod,
1964
+ includeItems,
1965
+ groupBy,
1966
+ minItemsPerPeriod,
1967
+ showEmpty,
1968
+ });
1969
+ // Get journal entries for the same period
1970
+ let journalQuery = 'SELECT * FROM journal_entries WHERE session_id = ?';
1971
+ const journalParams = [targetSessionId];
1972
+ // Calculate effective dates based on relativeTime if needed
1973
+ let effectiveStartDate = startDate;
1974
+ let effectiveEndDate = endDate;
1975
+ if (relativeTime) {
1976
+ const now = new Date();
1977
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1978
+ if (relativeTime === 'today') {
1979
+ effectiveStartDate = today.toISOString();
1980
+ effectiveEndDate = new Date(today.getTime() + 24 * 60 * 60 * 1000).toISOString();
1981
+ }
1982
+ else if (relativeTime === 'yesterday') {
1983
+ const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
1984
+ effectiveStartDate = yesterday.toISOString();
1985
+ effectiveEndDate = today.toISOString();
1986
+ }
1987
+ else if (relativeTime.match(/^(\d+) hours? ago$/)) {
1988
+ const hours = parseInt(relativeTime.match(/^(\d+)/)[1]);
1989
+ effectiveStartDate = new Date(now.getTime() - hours * 60 * 60 * 1000).toISOString();
1990
+ }
1991
+ else if (relativeTime.match(/^(\d+) days? ago$/)) {
1992
+ const days = parseInt(relativeTime.match(/^(\d+)/)[1]);
1993
+ effectiveStartDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000).toISOString();
1994
+ }
1995
+ else if (relativeTime === 'this week') {
1996
+ const startOfWeek = new Date(today);
1997
+ startOfWeek.setDate(today.getDate() - today.getDay());
1998
+ effectiveStartDate = startOfWeek.toISOString();
1999
+ }
2000
+ else if (relativeTime === 'last week') {
2001
+ const startOfLastWeek = new Date(today);
2002
+ startOfLastWeek.setDate(today.getDate() - today.getDay() - 7);
2003
+ const endOfLastWeek = new Date(startOfLastWeek);
2004
+ endOfLastWeek.setDate(startOfLastWeek.getDate() + 7);
2005
+ effectiveStartDate = startOfLastWeek.toISOString();
2006
+ effectiveEndDate = endOfLastWeek.toISOString();
2007
+ }
2008
+ }
2009
+ if (effectiveStartDate) {
2010
+ journalQuery += ' AND created_at >= ?';
2011
+ journalParams.push(effectiveStartDate);
2012
+ }
2013
+ if (effectiveEndDate) {
2014
+ journalQuery += ' AND created_at <= ?';
2015
+ journalParams.push(effectiveEndDate);
2016
+ }
2017
+ const journals = db
2018
+ .prepare(journalQuery + ' ORDER BY created_at')
2019
+ .all(...journalParams);
2020
+ // Format enhanced timeline response
2021
+ const timelineData = {
2022
+ session_id: targetSessionId,
2023
+ period: {
2024
+ start: effectiveStartDate || startDate || 'beginning',
2025
+ end: effectiveEndDate || endDate || 'now',
2026
+ relative: relativeTime || null,
2027
+ },
2028
+ groupBy,
2029
+ filters: {
2030
+ categories: categories || null,
2031
+ },
2032
+ timeline: timeline.map(period => {
2033
+ const result = {
2034
+ period: period.period,
2035
+ count: period.count,
2036
+ };
2037
+ if (includeItems && period.items) {
2038
+ result.items = period.items.map((item) => ({
2039
+ key: item.key,
2040
+ value: item.value,
2041
+ category: item.category,
2042
+ priority: item.priority,
2043
+ created_at: item.created_at,
2044
+ }));
2045
+ if (period.hasMore) {
2046
+ result.hasMore = true;
2047
+ result.totalCount = period.totalCount;
2048
+ }
2049
+ }
2050
+ return result;
2051
+ }),
2052
+ journal_entries: journals.map((journal) => ({
2053
+ entry: journal.entry,
2054
+ tags: JSON.parse(journal.tags || '[]'),
2055
+ mood: journal.mood,
2056
+ created_at: journal.created_at,
2057
+ })),
2058
+ };
2059
+ return {
2060
+ content: [
2061
+ {
2062
+ type: 'text',
2063
+ text: JSON.stringify(timelineData, null, 2),
2064
+ },
2065
+ ],
2066
+ };
2067
+ }
2068
+ catch (error) {
2069
+ return {
2070
+ content: [
2071
+ {
2072
+ type: 'text',
2073
+ text: `Timeline generation failed: ${error.message}`,
2074
+ },
2075
+ ],
2076
+ };
2077
+ }
2078
+ }
2079
+ // Phase 4.4: Progressive Compression
2080
+ case 'context_compress': {
2081
+ const { olderThan, preserveCategories = [], targetSize: _targetSize, sessionId } = args;
2082
+ const targetSessionId = sessionId || ensureSession();
2083
+ try {
2084
+ // Build query for items to compress
2085
+ let query = 'SELECT * FROM context_items WHERE session_id = ?';
2086
+ const params = [targetSessionId];
2087
+ if (olderThan) {
2088
+ query += ' AND created_at < ?';
2089
+ params.push(olderThan);
2090
+ }
2091
+ if (preserveCategories.length > 0) {
2092
+ query += ` AND category NOT IN (${preserveCategories.map(() => '?').join(',')})`;
2093
+ params.push(...preserveCategories);
2094
+ }
2095
+ const itemsToCompress = db.prepare(query).all(...params);
2096
+ if (itemsToCompress.length === 0) {
2097
+ return {
2098
+ content: [
2099
+ {
2100
+ type: 'text',
2101
+ text: 'No items found to compress with given criteria.',
2102
+ },
2103
+ ],
2104
+ };
2105
+ }
2106
+ // Group items by category for compression
2107
+ const categoryGroups = {};
2108
+ for (const item of itemsToCompress) {
2109
+ const category = item.category || 'uncategorized';
2110
+ if (!categoryGroups[category]) {
2111
+ categoryGroups[category] = [];
2112
+ }
2113
+ categoryGroups[category].push(item);
2114
+ }
2115
+ // Compress each category group
2116
+ const compressed = [];
2117
+ for (const [category, items] of Object.entries(categoryGroups)) {
2118
+ const summary = {
2119
+ category,
2120
+ count: items.length,
2121
+ priorities: { high: 0, normal: 0, low: 0 },
2122
+ keys: items.map((i) => i.key),
2123
+ samples: items
2124
+ .slice(0, 3)
2125
+ .map((i) => ({ key: i.key, value: i.value.substring(0, 100) })),
2126
+ };
2127
+ for (const item of items) {
2128
+ const priority = (item.priority || 'normal');
2129
+ summary.priorities[priority]++;
2130
+ }
2131
+ compressed.push(summary);
2132
+ }
2133
+ // Calculate compression
2134
+ const originalSize = JSON.stringify(itemsToCompress).length;
2135
+ const compressedData = JSON.stringify(compressed);
2136
+ const compressedSize = compressedData.length;
2137
+ const compressionRatio = 1 - compressedSize / originalSize;
2138
+ // Store compressed data
2139
+ const compressedId = (0, uuid_1.v4)();
2140
+ const dateRange = itemsToCompress.reduce((acc, item) => {
2141
+ const date = new Date(item.created_at);
2142
+ if (!acc.start || date < acc.start)
2143
+ acc.start = date;
2144
+ if (!acc.end || date > acc.end)
2145
+ acc.end = date;
2146
+ return acc;
2147
+ }, { start: null, end: null });
2148
+ db.prepare(`
2149
+ INSERT INTO compressed_context (id, session_id, original_count, compressed_data, compression_ratio, date_range_start, date_range_end)
2150
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2151
+ `).run(compressedId, targetSessionId, itemsToCompress.length, compressedData, compressionRatio, dateRange.start?.toISOString(), dateRange.end?.toISOString());
2152
+ // Delete original items
2153
+ const deleteStmt = db.prepare('DELETE FROM context_items WHERE id = ?');
2154
+ for (const item of itemsToCompress) {
2155
+ deleteStmt.run(item.id);
2156
+ }
2157
+ return {
2158
+ content: [
2159
+ {
2160
+ type: 'text',
2161
+ text: `Compression completed!
2162
+ Items compressed: ${itemsToCompress.length}
2163
+ Original size: ${(originalSize / 1024).toFixed(2)} KB
2164
+ Compressed size: ${(compressedSize / 1024).toFixed(2)} KB
2165
+ Compression ratio: ${(compressionRatio * 100).toFixed(1)}%
2166
+ Date range: ${dateRange.start?.toISOString().substring(0, 10)} to ${dateRange.end?.toISOString().substring(0, 10)}
2167
+
2168
+ Categories compressed:
2169
+ ${Object.entries(categoryGroups)
2170
+ .map(([cat, items]) => `- ${cat}: ${items.length} items`)
2171
+ .join('\n')}
2172
+
2173
+ Compressed data ID: ${compressedId.substring(0, 8)}`,
2174
+ },
2175
+ ],
2176
+ };
2177
+ }
2178
+ catch (error) {
2179
+ return {
2180
+ content: [
2181
+ {
2182
+ type: 'text',
2183
+ text: `Compression failed: ${error.message}`,
2184
+ },
2185
+ ],
2186
+ };
2187
+ }
2188
+ }
2189
+ // Phase 4.4: Cross-Tool Integration
2190
+ case 'context_integrate_tool': {
2191
+ const { toolName, eventType, data } = args;
2192
+ const sessionId = ensureSession();
2193
+ try {
2194
+ const id = (0, uuid_1.v4)();
2195
+ db.prepare(`
2196
+ INSERT INTO tool_events (id, session_id, tool_name, event_type, data)
2197
+ VALUES (?, ?, ?, ?, ?)
2198
+ `).run(id, sessionId, toolName, eventType, JSON.stringify(data));
2199
+ // Optionally create a context item for important events
2200
+ if (data.important || eventType === 'error' || eventType === 'milestone') {
2201
+ db.prepare(`
2202
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
2203
+ VALUES (?, ?, ?, ?, ?, ?)
2204
+ `).run((0, uuid_1.v4)(), sessionId, `${toolName}_${eventType}_${Date.now()}`, `Tool event: ${toolName} - ${eventType}: ${JSON.stringify(data)}`, 'tool_event', data.important ? 'high' : 'normal');
2205
+ }
2206
+ return {
2207
+ content: [
2208
+ {
2209
+ type: 'text',
2210
+ text: `Tool event recorded!
2211
+ Tool: ${toolName}
2212
+ Event: ${eventType}
2213
+ Data recorded: ${JSON.stringify(data).length} bytes
2214
+ Event ID: ${id.substring(0, 8)}`,
2215
+ },
2216
+ ],
2217
+ };
2218
+ }
2219
+ catch (error) {
2220
+ return {
2221
+ content: [
2222
+ {
2223
+ type: 'text',
2224
+ text: `Tool integration failed: ${error.message}`,
2225
+ },
2226
+ ],
2227
+ };
2228
+ }
2229
+ }
2230
+ // Cross-Session Collaboration Tools
2231
+ // REMOVED: Sharing is now automatic (public by default)
2232
+ /*
2233
+ case 'context_share': {
2234
+ const { key, targetSessions, makePublic = false } = args;
2235
+ const sessionId = ensureSession();
2236
+
2237
+ try {
2238
+ // Get the item to share
2239
+ const item = repositories.contexts.getByKey(sessionId, key);
2240
+ if (!item) {
2241
+ return {
2242
+ content: [{
2243
+ type: 'text',
2244
+ text: `Item not found: ${key}`,
2245
+ }],
2246
+ };
2247
+ }
2248
+
2249
+ // Share with specific sessions or make public
2250
+ const targetSessionIds = makePublic ? [] : (targetSessions || []);
2251
+ repositories.contexts.shareByKey(sessionId, key, targetSessionIds);
2252
+
2253
+ return {
2254
+ content: [{
2255
+ type: 'text',
2256
+ text: `Shared "${key}" ${makePublic ? 'publicly' : `with ${targetSessionIds.length} session(s)`}`,
2257
+ }],
2258
+ };
2259
+ } catch (error: any) {
2260
+ return {
2261
+ content: [{
2262
+ type: 'text',
2263
+ text: `Failed to share context: ${error.message}`,
2264
+ }],
2265
+ };
2266
+ }
2267
+ }
2268
+ */
2269
+ // REMOVED: All accessible items are retrieved via context_get
2270
+ /*
2271
+ case 'context_get_shared': {
2272
+ const { includeAll = false } = args;
2273
+ const sessionId = ensureSession();
2274
+
2275
+ try {
2276
+ const items = includeAll
2277
+ ? repositories.contexts.getAllSharedItems()
2278
+ : repositories.contexts.getSharedItems(sessionId);
2279
+
2280
+ if (items.length === 0) {
2281
+ return {
2282
+ content: [{
2283
+ type: 'text',
2284
+ text: 'No shared context items found',
2285
+ }],
2286
+ };
2287
+ }
2288
+
2289
+ const itemsList = items.map((item: any) => {
2290
+ const sharedWith = item.shared_with_sessions
2291
+ ? JSON.parse(item.shared_with_sessions).length
2292
+ : 'all';
2293
+ return `• [${item.priority}] ${item.key} (from session: ${item.session_id.substring(0, 8)}, shared with: ${sharedWith})\n ${item.value.substring(0, 100)}${item.value.length > 100 ? '...' : ''}`;
2294
+ }).join('\n\n');
2295
+
2296
+ return {
2297
+ content: [{
2298
+ type: 'text',
2299
+ text: `Found ${items.length} shared items:\n\n${itemsList}`,
2300
+ }],
2301
+ };
2302
+ } catch (error: any) {
2303
+ return {
2304
+ content: [{
2305
+ type: 'text',
2306
+ text: `Failed to get shared context: ${error.message}`,
2307
+ }],
2308
+ };
2309
+ }
2310
+ }
2311
+ */
2312
+ case 'context_search_all': {
2313
+ const { query, sessions, includeShared = true, limit: rawLimit = 25, offset: rawOffset = 0, sort = 'created_desc', category, channel, channels, priorities, createdAfter, createdBefore, keyPattern, searchIn = ['key', 'value'], includeMetadata = false, } = args;
2314
+ // Enhanced pagination validation with proper error handling
2315
+ const paginationValidation = validatePaginationParams({ limit: rawLimit, offset: rawOffset });
2316
+ const { limit, offset, errors: paginationErrors } = paginationValidation;
2317
+ const currentSession = currentSessionId || ensureSession();
2318
+ // Log pagination validation errors for debugging
2319
+ if (paginationErrors.length > 0) {
2320
+ debugLog('Pagination validation errors:', paginationErrors);
2321
+ }
2322
+ try {
2323
+ // Use enhanced search across sessions with pagination
2324
+ const result = repositories.contexts.searchAcrossSessionsEnhanced({
2325
+ query,
2326
+ currentSessionId: currentSession,
2327
+ sessions,
2328
+ includeShared,
2329
+ searchIn,
2330
+ limit,
2331
+ offset,
2332
+ sort,
2333
+ category,
2334
+ channel,
2335
+ channels,
2336
+ priorities,
2337
+ createdAfter,
2338
+ createdBefore,
2339
+ keyPattern,
2340
+ includeMetadata,
2341
+ });
2342
+ // PAGINATION VALIDATION: Ensure pagination is working as expected
2343
+ if (result.items.length > limit && limit < result.totalCount) {
2344
+ debugLog(`Pagination warning: Expected max ${limit} items, got ${result.items.length}. This may indicate a pagination implementation issue.`);
2345
+ }
2346
+ if (result.items.length === 0) {
2347
+ return {
2348
+ content: [
2349
+ {
2350
+ type: 'text',
2351
+ text: `No results found for: "${query}"${result.totalCount > 0 ? ` (showing page ${result.pagination.currentPage} of ${result.pagination.totalPages})` : ''}`,
2352
+ },
2353
+ ],
2354
+ };
2355
+ }
2356
+ const resultsList = result.items
2357
+ .map((item) => `• [${item.session_id.substring(0, 8)}] ${item.key}: ${item.value.substring(0, 100)}${item.value.length > 100 ? '...' : ''}`)
2358
+ .join('\n');
2359
+ // Build pagination info
2360
+ const paginationInfo = result.pagination.totalPages > 1
2361
+ ? `\n\nPagination: Page ${result.pagination.currentPage} of ${result.pagination.totalPages} (${result.pagination.totalItems} total items)${result.pagination.hasNextPage
2362
+ ? `\nNext page: offset=${result.pagination.nextOffset}, limit=${result.pagination.itemsPerPage}`
2363
+ : ''}${result.pagination.hasPreviousPage
2364
+ ? `\nPrevious page: offset=${result.pagination.previousOffset}, limit=${result.pagination.itemsPerPage}`
2365
+ : ''}`
2366
+ : '';
2367
+ return {
2368
+ content: [
2369
+ {
2370
+ type: 'text',
2371
+ text: `Found ${result.items.length} results on this page (${result.totalCount} total across sessions):\n\n${resultsList}${paginationInfo}`,
2372
+ },
2373
+ ],
2374
+ };
2375
+ }
2376
+ catch (error) {
2377
+ // Enhanced error handling to distinguish pagination errors from search errors
2378
+ let errorMessage = 'Search failed';
2379
+ if (paginationErrors.length > 0) {
2380
+ errorMessage = `Search failed due to pagination validation errors: ${paginationErrors.join(', ')}. ${error.message}`;
2381
+ }
2382
+ else if (error.message.includes('pagination') ||
2383
+ error.message.includes('limit') ||
2384
+ error.message.includes('offset')) {
2385
+ errorMessage = `Search failed due to pagination parameter issue: ${error.message}`;
2386
+ }
2387
+ else {
2388
+ errorMessage = `Search failed: ${error.message}`;
2389
+ }
2390
+ debugLog('Search error:', { error: error.message, paginationErrors, limit, offset });
2391
+ return {
2392
+ content: [
2393
+ {
2394
+ type: 'text',
2395
+ text: errorMessage,
2396
+ },
2397
+ ],
2398
+ };
2399
+ }
2400
+ }
2401
+ // Context Diff - Track changes since a specific point in time
2402
+ case 'context_diff': {
2403
+ const { since, sessionId: specificSessionId, category, channel, channels, includeValues = true, limit, offset, } = args;
2404
+ const targetSessionId = specificSessionId || currentSessionId || ensureSession();
2405
+ try {
2406
+ // Parse the 'since' parameter
2407
+ let sinceTimestamp = null;
2408
+ let checkpointId = null;
2409
+ if (since) {
2410
+ // Check if it's a checkpoint name or ID
2411
+ const checkpointByName = db
2412
+ .prepare('SELECT * FROM checkpoints WHERE name = ? ORDER BY created_at DESC LIMIT 1')
2413
+ .get(since);
2414
+ const checkpointById = !checkpointByName
2415
+ ? db.prepare('SELECT * FROM checkpoints WHERE id = ?').get(since)
2416
+ : null;
2417
+ const checkpoint = checkpointByName || checkpointById;
2418
+ if (checkpoint) {
2419
+ checkpointId = checkpoint.id;
2420
+ sinceTimestamp = checkpoint.created_at;
2421
+ }
2422
+ else {
2423
+ // Try to parse as relative time
2424
+ const parsedTime = parseRelativeTime(since);
2425
+ if (parsedTime) {
2426
+ sinceTimestamp = parsedTime;
2427
+ }
2428
+ else {
2429
+ // Assume it's an ISO timestamp
2430
+ sinceTimestamp = since;
2431
+ }
2432
+ }
2433
+ }
2434
+ else {
2435
+ // Default to 1 hour ago if no 'since' provided
2436
+ sinceTimestamp = new Date(Date.now() - 60 * 60 * 1000).toISOString();
2437
+ }
2438
+ // Convert ISO timestamp to SQLite format for repository compatibility
2439
+ const sqliteTimestamp = (0, timestamps_js_1.ensureSQLiteFormat)(sinceTimestamp);
2440
+ // Use repository method to get diff data
2441
+ const diffData = repositories.contexts.getDiff({
2442
+ sessionId: targetSessionId,
2443
+ sinceTimestamp: sqliteTimestamp,
2444
+ category,
2445
+ channel,
2446
+ channels,
2447
+ limit,
2448
+ offset,
2449
+ includeValues,
2450
+ });
2451
+ // Handle deleted items if we have a checkpoint
2452
+ let deletedKeys = [];
2453
+ if (checkpointId) {
2454
+ deletedKeys = repositories.contexts.getDeletedKeysFromCheckpoint(targetSessionId, checkpointId);
2455
+ }
2456
+ // Format response
2457
+ const toDate = new Date().toISOString();
2458
+ const response = {
2459
+ added: includeValues
2460
+ ? diffData.added
2461
+ : diffData.added.map(i => ({ key: i.key, category: i.category })),
2462
+ modified: includeValues
2463
+ ? diffData.modified
2464
+ : diffData.modified.map(i => ({ key: i.key, category: i.category })),
2465
+ deleted: deletedKeys,
2466
+ summary: `${diffData.added.length} added, ${diffData.modified.length} modified, ${deletedKeys.length} deleted`,
2467
+ period: {
2468
+ from: sinceTimestamp,
2469
+ to: toDate,
2470
+ },
2471
+ };
2472
+ return {
2473
+ content: [
2474
+ {
2475
+ type: 'text',
2476
+ text: JSON.stringify(response, null, 2),
2477
+ },
2478
+ ],
2479
+ };
2480
+ }
2481
+ catch (error) {
2482
+ return {
2483
+ content: [
2484
+ {
2485
+ type: 'text',
2486
+ text: `Failed to get context diff: ${error.message}`,
2487
+ },
2488
+ ],
2489
+ };
2490
+ }
2491
+ }
2492
+ // Channel Management
2493
+ case 'context_list_channels': {
2494
+ const { sessionId, sessionIds, sort, includeEmpty } = args;
2495
+ try {
2496
+ const channels = repositories.contexts.listChannels({
2497
+ sessionId: sessionId || currentSessionId,
2498
+ sessionIds,
2499
+ sort,
2500
+ includeEmpty,
2501
+ });
2502
+ if (channels.length === 0) {
2503
+ return {
2504
+ content: [
2505
+ {
2506
+ type: 'text',
2507
+ text: 'No channels found.',
2508
+ },
2509
+ ],
2510
+ };
2511
+ }
2512
+ // Format the response
2513
+ const channelList = channels
2514
+ .map((ch) => `• ${ch.channel}: ${ch.total_count} items (${ch.public_count} public, ${ch.private_count} private)\n Last activity: ${new Date(ch.last_activity).toLocaleString()}\n Categories: ${ch.categories.join(', ') || 'none'}\n Sessions: ${ch.session_count}`)
2515
+ .join('\n\n');
2516
+ return {
2517
+ content: [
2518
+ {
2519
+ type: 'text',
2520
+ text: `Found ${channels.length} channels:\n\n${channelList}`,
2521
+ },
2522
+ ],
2523
+ };
2524
+ }
2525
+ catch (error) {
2526
+ return {
2527
+ content: [
2528
+ {
2529
+ type: 'text',
2530
+ text: `Failed to list channels: ${error.message}`,
2531
+ },
2532
+ ],
2533
+ };
2534
+ }
2535
+ }
2536
+ case 'context_channel_stats': {
2537
+ const { channel, sessionId, includeTimeSeries, includeInsights } = args;
2538
+ try {
2539
+ const stats = repositories.contexts.getChannelStats({
2540
+ channel,
2541
+ sessionId: sessionId || currentSessionId,
2542
+ includeTimeSeries,
2543
+ includeInsights,
2544
+ });
2545
+ return {
2546
+ content: [
2547
+ {
2548
+ type: 'text',
2549
+ text: JSON.stringify(stats, null, 2),
2550
+ },
2551
+ ],
2552
+ };
2553
+ }
2554
+ catch (error) {
2555
+ return {
2556
+ content: [
2557
+ {
2558
+ type: 'text',
2559
+ text: `Failed to get channel stats: ${error.message}`,
2560
+ },
2561
+ ],
2562
+ };
2563
+ }
2564
+ }
2565
+ // Context Watch functionality
2566
+ case 'context_watch': {
2567
+ return await (0, contextWatchHandlers_js_1.handleContextWatch)(args, repositories, ensureSession());
2568
+ }
2569
+ // Context Reassign Channel
2570
+ case 'context_reassign_channel': {
2571
+ const { keys, keyPattern, fromChannel, toChannel, sessionId, category, priorities, dryRun = false, } = args;
2572
+ try {
2573
+ // Validate input
2574
+ if (!toChannel || !toChannel.trim()) {
2575
+ throw new Error('Target channel name cannot be empty');
2576
+ }
2577
+ if (!keys && !keyPattern && !fromChannel) {
2578
+ throw new Error('Must provide either keys array, keyPattern, or fromChannel');
2579
+ }
2580
+ if (fromChannel && fromChannel === toChannel) {
2581
+ throw new Error('Source and destination channels cannot be the same');
2582
+ }
2583
+ const targetSessionId = sessionId || ensureSession();
2584
+ // Call repository method
2585
+ const result = await repositories.contexts.reassignChannel({
2586
+ keys,
2587
+ keyPattern,
2588
+ fromChannel,
2589
+ toChannel,
2590
+ sessionId: targetSessionId,
2591
+ category,
2592
+ priorities,
2593
+ dryRun,
2594
+ });
2595
+ return {
2596
+ content: [
2597
+ {
2598
+ type: 'text',
2599
+ text: JSON.stringify(result, null, 2),
2600
+ },
2601
+ ],
2602
+ };
2603
+ }
2604
+ catch (error) {
2605
+ return {
2606
+ content: [
2607
+ {
2608
+ type: 'text',
2609
+ text: `Failed to reassign channel: ${error.message}`,
2610
+ },
2611
+ ],
2612
+ };
2613
+ }
2614
+ }
2615
+ // Batch Operations
2616
+ case 'context_batch_save': {
2617
+ const { items, updateExisting = true } = args;
2618
+ const sessionId = ensureSession();
2619
+ // Validate items
2620
+ if (!items || !Array.isArray(items) || items.length === 0) {
2621
+ return {
2622
+ content: [
2623
+ {
2624
+ type: 'text',
2625
+ text: 'No items provided for batch save',
2626
+ },
2627
+ ],
2628
+ };
2629
+ }
2630
+ // Enforce batch size limit
2631
+ const maxBatchSize = 100;
2632
+ if (items.length > maxBatchSize) {
2633
+ return {
2634
+ content: [
2635
+ {
2636
+ type: 'text',
2637
+ text: `Batch size ${items.length} exceeds maximum allowed size of ${maxBatchSize}`,
2638
+ },
2639
+ ],
2640
+ };
2641
+ }
2642
+ // Validate items
2643
+ const validationErrors = [];
2644
+ items.forEach((item, index) => {
2645
+ try {
2646
+ // Validate item
2647
+ if (!item.key || !item.key.trim()) {
2648
+ throw new Error('Key is required and cannot be empty');
2649
+ }
2650
+ if (!item.value) {
2651
+ throw new Error('Value is required');
2652
+ }
2653
+ // Validate category
2654
+ if (item.category) {
2655
+ const validCategories = ['task', 'decision', 'progress', 'note', 'error', 'warning'];
2656
+ if (!validCategories.includes(item.category)) {
2657
+ throw new Error(`Invalid category: ${item.category}`);
2658
+ }
2659
+ }
2660
+ // Validate priority
2661
+ if (item.priority) {
2662
+ const validPriorities = ['high', 'normal', 'low'];
2663
+ if (!validPriorities.includes(item.priority)) {
2664
+ throw new Error(`Invalid priority: ${item.priority}`);
2665
+ }
2666
+ }
2667
+ }
2668
+ catch (error) {
2669
+ validationErrors.push({
2670
+ index,
2671
+ key: item.key || 'undefined',
2672
+ error: error.message,
2673
+ });
2674
+ }
2675
+ });
2676
+ // If all items have validation errors, return early
2677
+ if (validationErrors.length === items.length) {
2678
+ return {
2679
+ content: [
2680
+ {
2681
+ type: 'text',
2682
+ text: JSON.stringify({
2683
+ operation: 'batch_save',
2684
+ totalItems: items.length,
2685
+ succeeded: 0,
2686
+ failed: validationErrors.length,
2687
+ totalSize: 0,
2688
+ results: [],
2689
+ errors: validationErrors,
2690
+ timestamp: new Date().toISOString(),
2691
+ }, null, 2),
2692
+ },
2693
+ ],
2694
+ };
2695
+ }
2696
+ let results = [];
2697
+ let errors = [];
2698
+ let totalSize = 0;
2699
+ // Begin transaction
2700
+ db.prepare('BEGIN TRANSACTION').run();
2701
+ try {
2702
+ // Use repository method
2703
+ const batchResult = repositories.contexts.batchSave(sessionId, items, { updateExisting });
2704
+ totalSize = batchResult.totalSize;
2705
+ // Merge validation errors with operation results
2706
+ const allResults = batchResult.results.filter(r => r.success);
2707
+ const allErrors = [
2708
+ ...validationErrors,
2709
+ ...batchResult.results
2710
+ .filter(r => !r.success)
2711
+ .map(r => ({
2712
+ index: r.index,
2713
+ key: r.key,
2714
+ error: r.error,
2715
+ })),
2716
+ ];
2717
+ // Commit transaction
2718
+ db.prepare('COMMIT').run();
2719
+ // Create embeddings for successful saves (async, don't wait)
2720
+ allResults.forEach(async (result) => {
2721
+ if (result.success && result.action === 'created') {
2722
+ try {
2723
+ const item = items[result.index];
2724
+ const content = `${item.key}: ${item.value}`;
2725
+ const metadata = { key: item.key, category: item.category, priority: item.priority };
2726
+ await vectorStore.storeDocument(result.id, content, metadata);
2727
+ }
2728
+ catch (error) {
2729
+ // Log but don't fail
2730
+ console.error('Failed to create embedding:', error);
2731
+ }
2732
+ }
2733
+ });
2734
+ results = allResults;
2735
+ errors = allErrors;
2736
+ }
2737
+ catch (error) {
2738
+ // Rollback transaction
2739
+ db.prepare('ROLLBACK').run();
2740
+ return {
2741
+ content: [
2742
+ {
2743
+ type: 'text',
2744
+ text: `Batch save failed: ${error.message}`,
2745
+ },
2746
+ ],
2747
+ };
2748
+ }
2749
+ // Prepare response
2750
+ const response = {
2751
+ operation: 'batch_save',
2752
+ totalItems: items.length,
2753
+ succeeded: results.length,
2754
+ failed: errors.length,
2755
+ totalSize: totalSize,
2756
+ averageSize: results.length > 0 ? Math.round(totalSize / results.length) : 0,
2757
+ results: results,
2758
+ errors: errors,
2759
+ timestamp: new Date().toISOString(),
2760
+ };
2761
+ return {
2762
+ content: [
2763
+ {
2764
+ type: 'text',
2765
+ text: JSON.stringify(response, null, 2),
2766
+ },
2767
+ ],
2768
+ };
2769
+ }
2770
+ case 'context_batch_delete': {
2771
+ const { keys, keyPattern, sessionId: specificSessionId, dryRun = false } = args;
2772
+ const targetSessionId = specificSessionId || currentSessionId || ensureSession();
2773
+ // Validate input
2774
+ if (!keys && !keyPattern) {
2775
+ return {
2776
+ content: [
2777
+ {
2778
+ type: 'text',
2779
+ text: 'Either keys array or keyPattern must be provided',
2780
+ },
2781
+ ],
2782
+ };
2783
+ }
2784
+ if (keys && (!Array.isArray(keys) || keys.length === 0)) {
2785
+ return {
2786
+ content: [
2787
+ {
2788
+ type: 'text',
2789
+ text: 'Keys must be a non-empty array',
2790
+ },
2791
+ ],
2792
+ };
2793
+ }
2794
+ let results = [];
2795
+ let totalDeleted = 0;
2796
+ try {
2797
+ if (dryRun) {
2798
+ // Dry run - just show what would be deleted
2799
+ const itemsToDelete = repositories.contexts.getDryRunItems(targetSessionId, {
2800
+ keys,
2801
+ keyPattern,
2802
+ });
2803
+ return {
2804
+ content: [
2805
+ {
2806
+ type: 'text',
2807
+ text: JSON.stringify({
2808
+ operation: 'batch_delete',
2809
+ dryRun: true,
2810
+ keys: keys,
2811
+ pattern: keyPattern,
2812
+ itemsToDelete: itemsToDelete,
2813
+ totalItems: itemsToDelete.length,
2814
+ }, null, 2),
2815
+ },
2816
+ ],
2817
+ };
2818
+ }
2819
+ // Actual deletion
2820
+ db.prepare('BEGIN TRANSACTION').run();
2821
+ const deleteResult = repositories.contexts.batchDelete(targetSessionId, {
2822
+ keys,
2823
+ keyPattern,
2824
+ });
2825
+ results = deleteResult.results || [];
2826
+ totalDeleted = deleteResult.totalDeleted;
2827
+ db.prepare('COMMIT').run();
2828
+ }
2829
+ catch (error) {
2830
+ db.prepare('ROLLBACK').run();
2831
+ return {
2832
+ content: [
2833
+ {
2834
+ type: 'text',
2835
+ text: `Batch delete failed: ${error.message}`,
2836
+ },
2837
+ ],
2838
+ };
2839
+ }
2840
+ // Prepare response
2841
+ const response = keys
2842
+ ? {
2843
+ operation: 'batch_delete',
2844
+ keys: keys,
2845
+ totalRequested: keys.length,
2846
+ totalDeleted: totalDeleted,
2847
+ notFound: results.filter(r => !r.deleted).map(r => r.key),
2848
+ results: results,
2849
+ }
2850
+ : {
2851
+ operation: 'batch_delete',
2852
+ pattern: keyPattern,
2853
+ totalDeleted: totalDeleted,
2854
+ };
2855
+ return {
2856
+ content: [
2857
+ {
2858
+ type: 'text',
2859
+ text: JSON.stringify(response, null, 2),
2860
+ },
2861
+ ],
2862
+ };
2863
+ }
2864
+ case 'context_batch_update': {
2865
+ const { updates, sessionId: specificSessionId } = args;
2866
+ const targetSessionId = specificSessionId || currentSessionId || ensureSession();
2867
+ // Validate input
2868
+ if (!updates || !Array.isArray(updates) || updates.length === 0) {
2869
+ return {
2870
+ content: [
2871
+ {
2872
+ type: 'text',
2873
+ text: 'Updates array must be provided and non-empty',
2874
+ },
2875
+ ],
2876
+ };
2877
+ }
2878
+ // Validate updates
2879
+ const validationErrors = [];
2880
+ updates.forEach((update, index) => {
2881
+ try {
2882
+ // Validate update
2883
+ if (!update.key || !update.key.trim()) {
2884
+ throw new Error('Key is required and cannot be empty');
2885
+ }
2886
+ // Check if any updates are provided
2887
+ const hasUpdates = update.value !== undefined ||
2888
+ update.category !== undefined ||
2889
+ update.priority !== undefined ||
2890
+ update.channel !== undefined;
2891
+ if (!hasUpdates) {
2892
+ throw new Error('No updates provided');
2893
+ }
2894
+ // Validate fields if provided
2895
+ if (update.category !== undefined) {
2896
+ const validCategories = ['task', 'decision', 'progress', 'note', 'error', 'warning'];
2897
+ if (!validCategories.includes(update.category)) {
2898
+ throw new Error(`Invalid category: ${update.category}`);
2899
+ }
2900
+ }
2901
+ if (update.priority !== undefined) {
2902
+ const validPriorities = ['high', 'normal', 'low'];
2903
+ if (!validPriorities.includes(update.priority)) {
2904
+ throw new Error(`Invalid priority: ${update.priority}`);
2905
+ }
2906
+ }
2907
+ if (update.value !== undefined && update.value === '') {
2908
+ throw new Error('Value cannot be empty');
2909
+ }
2910
+ }
2911
+ catch (error) {
2912
+ validationErrors.push({
2913
+ index,
2914
+ key: update.key || 'undefined',
2915
+ error: error.message,
2916
+ });
2917
+ }
2918
+ });
2919
+ // If all updates have validation errors, return early
2920
+ if (validationErrors.length === updates.length) {
2921
+ return {
2922
+ content: [
2923
+ {
2924
+ type: 'text',
2925
+ text: JSON.stringify({
2926
+ operation: 'batch_update',
2927
+ totalItems: updates.length,
2928
+ succeeded: 0,
2929
+ failed: validationErrors.length,
2930
+ results: [],
2931
+ errors: validationErrors,
2932
+ }, null, 2),
2933
+ },
2934
+ ],
2935
+ };
2936
+ }
2937
+ let results = [];
2938
+ let errors = [];
2939
+ // Begin transaction
2940
+ db.prepare('BEGIN TRANSACTION').run();
2941
+ try {
2942
+ // Use repository method
2943
+ const updateResult = repositories.contexts.batchUpdate(targetSessionId, updates);
2944
+ // Merge validation errors with operation results
2945
+ results = updateResult.results.filter(r => r.updated);
2946
+ errors = [
2947
+ ...validationErrors,
2948
+ ...updateResult.results
2949
+ .filter(r => !r.updated)
2950
+ .map(r => ({
2951
+ index: r.index,
2952
+ key: r.key,
2953
+ error: r.error,
2954
+ })),
2955
+ ];
2956
+ // Commit transaction
2957
+ db.prepare('COMMIT').run();
2958
+ }
2959
+ catch (error) {
2960
+ // Rollback transaction
2961
+ db.prepare('ROLLBACK').run();
2962
+ return {
2963
+ content: [
2964
+ {
2965
+ type: 'text',
2966
+ text: `Batch update failed: ${error.message}`,
2967
+ },
2968
+ ],
2969
+ };
2970
+ }
2971
+ // Prepare response
2972
+ const response = {
2973
+ operation: 'batch_update',
2974
+ totalItems: updates.length,
2975
+ succeeded: results.length,
2976
+ failed: errors.length,
2977
+ results: results,
2978
+ errors: errors,
2979
+ };
2980
+ return {
2981
+ content: [
2982
+ {
2983
+ type: 'text',
2984
+ text: JSON.stringify(response, null, 2),
2985
+ },
2986
+ ],
2987
+ };
2988
+ }
2989
+ // Context Relationships
2990
+ case 'context_link': {
2991
+ const { sourceKey, targetKey, relationship, metadata } = args;
2992
+ const sessionId = currentSessionId || ensureSession();
2993
+ // Validate inputs
2994
+ if (!sourceKey || !sourceKey.trim()) {
2995
+ return {
2996
+ content: [
2997
+ {
2998
+ type: 'text',
2999
+ text: 'Error: sourceKey cannot be empty',
3000
+ },
3001
+ ],
3002
+ };
3003
+ }
3004
+ if (!targetKey || !targetKey.trim()) {
3005
+ return {
3006
+ content: [
3007
+ {
3008
+ type: 'text',
3009
+ text: 'Error: targetKey cannot be empty',
3010
+ },
3011
+ ],
3012
+ };
3013
+ }
3014
+ if (!relationship || !relationship.trim()) {
3015
+ return {
3016
+ content: [
3017
+ {
3018
+ type: 'text',
3019
+ text: 'Error: relationship cannot be empty',
3020
+ },
3021
+ ],
3022
+ };
3023
+ }
3024
+ // Create relationship
3025
+ const result = repositories.contexts.createRelationship({
3026
+ sessionId,
3027
+ sourceKey,
3028
+ targetKey,
3029
+ relationship,
3030
+ metadata,
3031
+ });
3032
+ if (!result.created) {
3033
+ return {
3034
+ content: [
3035
+ {
3036
+ type: 'text',
3037
+ text: `Error: ${result.error}`,
3038
+ },
3039
+ ],
3040
+ };
3041
+ }
3042
+ return {
3043
+ content: [
3044
+ {
3045
+ type: 'text',
3046
+ text: JSON.stringify({
3047
+ operation: 'context_link',
3048
+ relationshipId: result.id,
3049
+ sourceKey,
3050
+ targetKey,
3051
+ relationship,
3052
+ metadata,
3053
+ created: true,
3054
+ timestamp: new Date().toISOString(),
3055
+ }, null, 2),
3056
+ },
3057
+ ],
3058
+ };
3059
+ }
3060
+ case 'context_get_related': {
3061
+ const { key, relationship, depth = 1, direction = 'both' } = args;
3062
+ const sessionId = currentSessionId || ensureSession();
3063
+ if (!key || !key.trim()) {
3064
+ return {
3065
+ content: [
3066
+ {
3067
+ type: 'text',
3068
+ text: 'Error: key cannot be empty',
3069
+ },
3070
+ ],
3071
+ };
3072
+ }
3073
+ // Get related items
3074
+ const result = repositories.contexts.getRelatedItems({
3075
+ sessionId,
3076
+ key,
3077
+ relationship,
3078
+ depth,
3079
+ direction,
3080
+ });
3081
+ const totalRelated = result.outgoing.length + result.incoming.length;
3082
+ // Prepare response
3083
+ let response = {
3084
+ operation: 'context_get_related',
3085
+ key,
3086
+ related: {
3087
+ outgoing: result.outgoing,
3088
+ incoming: result.incoming,
3089
+ },
3090
+ totalRelated,
3091
+ };
3092
+ // Add graph data if depth > 1
3093
+ if (depth > 1 && result.graph) {
3094
+ response.visualization = {
3095
+ format: 'graph',
3096
+ nodes: result.graph.nodes,
3097
+ edges: result.graph.edges,
3098
+ };
3099
+ response.summary = {
3100
+ totalNodes: result.graph.nodes.length,
3101
+ totalEdges: result.graph.edges.length,
3102
+ relationshipTypes: [...new Set(result.graph.edges.map((e) => e.type))],
3103
+ };
3104
+ }
3105
+ // Add message if no relationships found
3106
+ if (totalRelated === 0) {
3107
+ response.message = 'No relationships found for this item';
3108
+ }
3109
+ return {
3110
+ content: [
3111
+ {
3112
+ type: 'text',
3113
+ text: JSON.stringify(response, null, 2),
3114
+ },
3115
+ ],
3116
+ };
3117
+ }
3118
+ default:
3119
+ throw new Error(`Unknown tool: ${toolName}`);
3120
+ }
3121
+ });
3122
+ // List available tools
3123
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
3124
+ return {
3125
+ tools: [
3126
+ // Session Management
3127
+ {
3128
+ name: 'context_session_start',
3129
+ description: 'Start a new context session with optional project directory for git tracking',
3130
+ inputSchema: {
3131
+ type: 'object',
3132
+ properties: {
3133
+ name: { type: 'string', description: 'Session name' },
3134
+ description: { type: 'string', description: 'Session description' },
3135
+ continueFrom: { type: 'string', description: 'Session ID to continue from' },
3136
+ projectDir: {
3137
+ type: 'string',
3138
+ description: 'Project directory path for git tracking (e.g., "/path/to/your/project")',
3139
+ },
3140
+ defaultChannel: {
3141
+ type: 'string',
3142
+ description: 'Default channel for context items (auto-derived from git branch if not provided)',
3143
+ },
3144
+ },
3145
+ },
3146
+ },
3147
+ {
3148
+ name: 'context_session_list',
3149
+ description: 'List recent sessions',
3150
+ inputSchema: {
3151
+ type: 'object',
3152
+ properties: {
3153
+ limit: {
3154
+ type: 'number',
3155
+ description: 'Maximum number of sessions to return',
3156
+ default: 10,
3157
+ },
3158
+ },
3159
+ },
3160
+ },
3161
+ {
3162
+ name: 'context_set_project_dir',
3163
+ description: 'Set the project directory for git tracking in the current session',
3164
+ inputSchema: {
3165
+ type: 'object',
3166
+ properties: {
3167
+ projectDir: {
3168
+ type: 'string',
3169
+ description: 'Project directory path for git tracking (e.g., "/path/to/your/project")',
3170
+ },
3171
+ },
3172
+ required: ['projectDir'],
3173
+ },
3174
+ },
3175
+ // Enhanced Context Storage
3176
+ {
3177
+ name: 'context_save',
3178
+ description: 'Save a context item with optional category, priority, and privacy setting',
3179
+ inputSchema: {
3180
+ type: 'object',
3181
+ properties: {
3182
+ key: { type: 'string', description: 'Unique key for the context item' },
3183
+ value: { type: 'string', description: 'Context value to save' },
3184
+ category: {
3185
+ type: 'string',
3186
+ description: 'Category (e.g., task, decision, progress)',
3187
+ enum: ['task', 'decision', 'progress', 'note', 'error', 'warning'],
3188
+ },
3189
+ priority: {
3190
+ type: 'string',
3191
+ description: 'Priority level',
3192
+ enum: ['high', 'normal', 'low'],
3193
+ default: 'normal',
3194
+ },
3195
+ private: {
3196
+ type: 'boolean',
3197
+ description: 'If true, item is only accessible from the current session. Default: false (accessible from all sessions)',
3198
+ default: false,
3199
+ },
3200
+ channel: {
3201
+ type: 'string',
3202
+ description: 'Channel to organize this item (uses session default if not provided)',
3203
+ },
3204
+ },
3205
+ required: ['key', 'value'],
3206
+ },
3207
+ },
3208
+ {
3209
+ name: 'context_get',
3210
+ description: 'Retrieve saved context by key, category, or session with enhanced filtering. Returns all accessible items (public items + own private items)',
3211
+ inputSchema: {
3212
+ type: 'object',
3213
+ properties: {
3214
+ key: { type: 'string', description: 'Specific key to retrieve' },
3215
+ category: { type: 'string', description: 'Filter by category' },
3216
+ sessionId: { type: 'string', description: 'Specific session ID (defaults to current)' },
3217
+ channel: { type: 'string', description: 'Filter by single channel' },
3218
+ channels: {
3219
+ type: 'array',
3220
+ items: { type: 'string' },
3221
+ description: 'Filter by multiple channels',
3222
+ },
3223
+ includeMetadata: {
3224
+ type: 'boolean',
3225
+ description: 'Include timestamps and size info',
3226
+ },
3227
+ sort: {
3228
+ type: 'string',
3229
+ enum: ['created_desc', 'created_asc', 'updated_desc', 'key_asc', 'key_desc'],
3230
+ description: 'Sort order for results',
3231
+ },
3232
+ limit: {
3233
+ type: 'number',
3234
+ description: 'Maximum items to return. Must be a positive integer. Invalid values will cause validation error. (default: auto-derived)',
3235
+ },
3236
+ offset: {
3237
+ type: 'number',
3238
+ description: 'Pagination offset. Must be a non-negative integer. Invalid values will cause validation error. (default: 0)',
3239
+ },
3240
+ createdAfter: {
3241
+ type: 'string',
3242
+ description: 'ISO date - items created after this time',
3243
+ },
3244
+ createdBefore: {
3245
+ type: 'string',
3246
+ description: 'ISO date - items created before this time',
3247
+ },
3248
+ keyPattern: {
3249
+ type: 'string',
3250
+ description: 'Regex pattern for key matching',
3251
+ },
3252
+ priorities: {
3253
+ type: 'array',
3254
+ items: { type: 'string', enum: ['high', 'normal', 'low'] },
3255
+ description: 'Filter by priority levels',
3256
+ },
3257
+ },
3258
+ },
3259
+ },
3260
+ // File Caching
3261
+ {
3262
+ name: 'context_cache_file',
3263
+ description: 'Cache file content with hash for change detection',
3264
+ inputSchema: {
3265
+ type: 'object',
3266
+ properties: {
3267
+ filePath: { type: 'string', description: 'Path to the file' },
3268
+ content: { type: 'string', description: 'File content to cache' },
3269
+ },
3270
+ required: ['filePath', 'content'],
3271
+ },
3272
+ },
3273
+ {
3274
+ name: 'context_file_changed',
3275
+ description: 'Check if a file has changed since it was cached',
3276
+ inputSchema: {
3277
+ type: 'object',
3278
+ properties: {
3279
+ filePath: { type: 'string', description: 'Path to the file' },
3280
+ currentContent: { type: 'string', description: 'Current file content to compare' },
3281
+ },
3282
+ required: ['filePath'],
3283
+ },
3284
+ },
3285
+ // Status
3286
+ {
3287
+ name: 'context_status',
3288
+ description: 'Get current context status and statistics',
3289
+ inputSchema: {
3290
+ type: 'object',
3291
+ properties: {},
3292
+ },
3293
+ },
3294
+ // Phase 2: Checkpoint System
3295
+ {
3296
+ name: 'context_checkpoint',
3297
+ description: 'Create a named checkpoint of current context',
3298
+ inputSchema: {
3299
+ type: 'object',
3300
+ properties: {
3301
+ name: { type: 'string', description: 'Checkpoint name' },
3302
+ description: { type: 'string', description: 'Checkpoint description' },
3303
+ includeFiles: {
3304
+ type: 'boolean',
3305
+ description: 'Include cached files in checkpoint',
3306
+ default: true,
3307
+ },
3308
+ includeGitStatus: {
3309
+ type: 'boolean',
3310
+ description: 'Capture current git status',
3311
+ default: true,
3312
+ },
3313
+ },
3314
+ required: ['name'],
3315
+ },
3316
+ },
3317
+ {
3318
+ name: 'context_restore_checkpoint',
3319
+ description: 'Restore context from a checkpoint',
3320
+ inputSchema: {
3321
+ type: 'object',
3322
+ properties: {
3323
+ name: { type: 'string', description: 'Checkpoint name to restore' },
3324
+ checkpointId: { type: 'string', description: 'Specific checkpoint ID' },
3325
+ restoreFiles: {
3326
+ type: 'boolean',
3327
+ description: 'Restore cached files',
3328
+ default: true,
3329
+ },
3330
+ },
3331
+ },
3332
+ },
3333
+ // Phase 2: Summarization
3334
+ {
3335
+ name: 'context_summarize',
3336
+ description: 'Get AI-friendly summary of session context',
3337
+ inputSchema: {
3338
+ type: 'object',
3339
+ properties: {
3340
+ sessionId: {
3341
+ type: 'string',
3342
+ description: 'Session to summarize (defaults to current)',
3343
+ },
3344
+ categories: {
3345
+ type: 'array',
3346
+ items: { type: 'string' },
3347
+ description: 'Filter by specific categories',
3348
+ },
3349
+ maxLength: {
3350
+ type: 'number',
3351
+ description: 'Maximum summary length',
3352
+ default: 1000,
3353
+ },
3354
+ },
3355
+ },
3356
+ },
3357
+ // Phase 3: Smart Compaction
3358
+ {
3359
+ name: 'context_prepare_compaction',
3360
+ description: 'Automatically save critical context before compaction',
3361
+ inputSchema: {
3362
+ type: 'object',
3363
+ properties: {},
3364
+ },
3365
+ },
3366
+ // Phase 3: Git Integration
3367
+ {
3368
+ name: 'context_git_commit',
3369
+ description: 'Create git commit with automatic context save',
3370
+ inputSchema: {
3371
+ type: 'object',
3372
+ properties: {
3373
+ message: { type: 'string', description: 'Commit message' },
3374
+ autoSave: {
3375
+ type: 'boolean',
3376
+ description: 'Automatically save context state',
3377
+ default: true,
3378
+ },
3379
+ },
3380
+ required: ['message'],
3381
+ },
3382
+ },
3383
+ // Phase 3: Search
3384
+ {
3385
+ name: 'context_search',
3386
+ description: 'Search through saved context items with advanced filtering',
3387
+ inputSchema: {
3388
+ type: 'object',
3389
+ properties: {
3390
+ query: { type: 'string', description: 'Search query' },
3391
+ searchIn: {
3392
+ type: 'array',
3393
+ items: { type: 'string', enum: ['key', 'value'] },
3394
+ description: 'Fields to search in',
3395
+ default: ['key', 'value'],
3396
+ },
3397
+ sessionId: { type: 'string', description: 'Session to search (defaults to current)' },
3398
+ category: { type: 'string', description: 'Filter by category' },
3399
+ channel: { type: 'string', description: 'Filter by single channel' },
3400
+ channels: {
3401
+ type: 'array',
3402
+ items: { type: 'string' },
3403
+ description: 'Filter by multiple channels',
3404
+ },
3405
+ createdAfter: {
3406
+ type: 'string',
3407
+ description: 'ISO date - items created after this time',
3408
+ },
3409
+ createdBefore: {
3410
+ type: 'string',
3411
+ description: 'ISO date - items created before this time',
3412
+ },
3413
+ relativeTime: {
3414
+ type: 'string',
3415
+ description: 'Natural language time (e.g., "2 hours ago", "yesterday")',
3416
+ },
3417
+ keyPattern: {
3418
+ type: 'string',
3419
+ description: 'Pattern for key matching (uses GLOB syntax)',
3420
+ },
3421
+ priorities: {
3422
+ type: 'array',
3423
+ items: { type: 'string', enum: ['high', 'normal', 'low'] },
3424
+ description: 'Filter by priority levels',
3425
+ },
3426
+ sort: {
3427
+ type: 'string',
3428
+ enum: ['created_desc', 'created_asc', 'updated_desc', 'key_asc', 'key_desc'],
3429
+ description: 'Sort order for results',
3430
+ },
3431
+ limit: {
3432
+ type: 'number',
3433
+ description: 'Maximum items to return. Must be a positive integer. Invalid values will cause validation error. (default: auto-derived)',
3434
+ },
3435
+ offset: {
3436
+ type: 'number',
3437
+ description: 'Pagination offset. Must be a non-negative integer. Invalid values will cause validation error. (default: 0)',
3438
+ },
3439
+ includeMetadata: {
3440
+ type: 'boolean',
3441
+ description: 'Include timestamps and size info',
3442
+ },
3443
+ },
3444
+ required: ['query'],
3445
+ },
3446
+ },
3447
+ // Cross-Session Collaboration
3448
+ // REMOVED: Sharing is now automatic (public by default)
3449
+ /*
3450
+ {
3451
+ name: 'context_share',
3452
+ description: 'Share a context item with other sessions for cross-session collaboration',
3453
+ inputSchema: {
3454
+ type: 'object',
3455
+ properties: {
3456
+ key: { type: 'string', description: 'Key of the item to share' },
3457
+ targetSessions: {
3458
+ type: 'array',
3459
+ items: { type: 'string' },
3460
+ description: 'Session IDs to share with (empty for public sharing)'
3461
+ },
3462
+ makePublic: {
3463
+ type: 'boolean',
3464
+ description: 'Share with all sessions',
3465
+ default: false
3466
+ },
3467
+ },
3468
+ required: ['key'],
3469
+ },
3470
+ },
3471
+ */
3472
+ // REMOVED: All accessible items are retrieved via context_get
3473
+ /*
3474
+ {
3475
+ name: 'context_get_shared',
3476
+ description: 'Get shared context items from other sessions',
3477
+ inputSchema: {
3478
+ type: 'object',
3479
+ properties: {
3480
+ includeAll: {
3481
+ type: 'boolean',
3482
+ description: 'Include all shared items from all sessions',
3483
+ default: false
3484
+ },
3485
+ },
3486
+ },
3487
+ },
3488
+ */
3489
+ {
3490
+ name: 'context_search_all',
3491
+ description: 'Search across multiple or all sessions with pagination support',
3492
+ inputSchema: {
3493
+ type: 'object',
3494
+ properties: {
3495
+ query: { type: 'string', description: 'Search query' },
3496
+ sessions: {
3497
+ type: 'array',
3498
+ items: { type: 'string' },
3499
+ description: 'Session IDs to search (empty for all sessions)',
3500
+ },
3501
+ includeShared: {
3502
+ type: 'boolean',
3503
+ description: 'Include shared items in search',
3504
+ default: true,
3505
+ },
3506
+ limit: {
3507
+ type: 'number',
3508
+ description: 'Maximum number of items to return. Must be a positive integer between 1-100. Non-integer values will be rejected with validation error. (default: 25)',
3509
+ minimum: 1,
3510
+ maximum: 100,
3511
+ default: 25,
3512
+ },
3513
+ offset: {
3514
+ type: 'number',
3515
+ description: 'Number of items to skip for pagination. Must be a non-negative integer (0 or higher). Non-integer values will be rejected with validation error. (default: 0)',
3516
+ minimum: 0,
3517
+ default: 0,
3518
+ },
3519
+ sort: {
3520
+ type: 'string',
3521
+ description: 'Sort order for results',
3522
+ enum: ['created_desc', 'created_asc', 'updated_desc', 'key_asc', 'key_desc'],
3523
+ default: 'created_desc',
3524
+ },
3525
+ category: {
3526
+ type: 'string',
3527
+ description: 'Filter by category',
3528
+ enum: ['task', 'decision', 'progress', 'note', 'error', 'warning'],
3529
+ },
3530
+ channel: {
3531
+ type: 'string',
3532
+ description: 'Filter by single channel',
3533
+ },
3534
+ channels: {
3535
+ type: 'array',
3536
+ items: { type: 'string' },
3537
+ description: 'Filter by multiple channels',
3538
+ },
3539
+ priorities: {
3540
+ type: 'array',
3541
+ items: { type: 'string', enum: ['high', 'normal', 'low'] },
3542
+ description: 'Filter by priority levels',
3543
+ },
3544
+ createdAfter: {
3545
+ type: 'string',
3546
+ description: 'Filter items created after this date (ISO format or relative time)',
3547
+ },
3548
+ createdBefore: {
3549
+ type: 'string',
3550
+ description: 'Filter items created before this date (ISO format or relative time)',
3551
+ },
3552
+ keyPattern: {
3553
+ type: 'string',
3554
+ description: 'Pattern to match keys (supports wildcards: *, ?)',
3555
+ },
3556
+ searchIn: {
3557
+ type: 'array',
3558
+ items: { type: 'string', enum: ['key', 'value'] },
3559
+ description: 'Fields to search in',
3560
+ default: ['key', 'value'],
3561
+ },
3562
+ includeMetadata: {
3563
+ type: 'boolean',
3564
+ description: 'Include timestamps and size info',
3565
+ default: false,
3566
+ },
3567
+ },
3568
+ required: ['query'],
3569
+ },
3570
+ },
3571
+ // Phase 3: Export/Import
3572
+ {
3573
+ name: 'context_export',
3574
+ description: 'Export session data for backup or sharing',
3575
+ inputSchema: {
3576
+ type: 'object',
3577
+ properties: {
3578
+ sessionId: { type: 'string', description: 'Session to export (defaults to current)' },
3579
+ format: {
3580
+ type: 'string',
3581
+ enum: ['json', 'inline'],
3582
+ description: 'Export format',
3583
+ default: 'json',
3584
+ },
3585
+ },
3586
+ },
3587
+ },
3588
+ {
3589
+ name: 'context_import',
3590
+ description: 'Import previously exported session data',
3591
+ inputSchema: {
3592
+ type: 'object',
3593
+ properties: {
3594
+ filePath: { type: 'string', description: 'Path to import file' },
3595
+ merge: {
3596
+ type: 'boolean',
3597
+ description: 'Merge with current session instead of creating new',
3598
+ default: false,
3599
+ },
3600
+ },
3601
+ required: ['filePath'],
3602
+ },
3603
+ },
3604
+ // Phase 4.1: Knowledge Graph
3605
+ {
3606
+ name: 'context_analyze',
3607
+ description: 'Analyze context to extract entities and relationships',
3608
+ inputSchema: {
3609
+ type: 'object',
3610
+ properties: {
3611
+ sessionId: {
3612
+ type: 'string',
3613
+ description: 'Session ID to analyze (defaults to current)',
3614
+ },
3615
+ categories: {
3616
+ type: 'array',
3617
+ items: { type: 'string' },
3618
+ description: 'Categories to analyze',
3619
+ },
3620
+ },
3621
+ },
3622
+ },
3623
+ {
3624
+ name: 'context_find_related',
3625
+ description: 'Find entities related to a key or entity',
3626
+ inputSchema: {
3627
+ type: 'object',
3628
+ properties: {
3629
+ key: { type: 'string', description: 'Context key or entity name' },
3630
+ relationTypes: {
3631
+ type: 'array',
3632
+ items: { type: 'string' },
3633
+ description: 'Types of relations to include',
3634
+ },
3635
+ maxDepth: {
3636
+ type: 'number',
3637
+ description: 'Maximum graph traversal depth',
3638
+ default: 2,
3639
+ },
3640
+ },
3641
+ required: ['key'],
3642
+ },
3643
+ },
3644
+ {
3645
+ name: 'context_visualize',
3646
+ description: 'Generate visualization data for the knowledge graph',
3647
+ inputSchema: {
3648
+ type: 'object',
3649
+ properties: {
3650
+ type: {
3651
+ type: 'string',
3652
+ enum: ['graph', 'timeline', 'heatmap'],
3653
+ description: 'Visualization type',
3654
+ default: 'graph',
3655
+ },
3656
+ entityTypes: {
3657
+ type: 'array',
3658
+ items: { type: 'string' },
3659
+ description: 'Entity types to include',
3660
+ },
3661
+ sessionId: {
3662
+ type: 'string',
3663
+ description: 'Session to visualize (defaults to current)',
3664
+ },
3665
+ },
3666
+ },
3667
+ },
3668
+ // Phase 4.2: Semantic Search
3669
+ {
3670
+ name: 'context_semantic_search',
3671
+ description: 'Search context using natural language queries',
3672
+ inputSchema: {
3673
+ type: 'object',
3674
+ properties: {
3675
+ query: { type: 'string', description: 'Natural language search query' },
3676
+ topK: {
3677
+ type: 'number',
3678
+ description: 'Number of results to return',
3679
+ default: 10,
3680
+ },
3681
+ minSimilarity: {
3682
+ type: 'number',
3683
+ description: 'Minimum similarity score (0-1)',
3684
+ default: 0.3,
3685
+ },
3686
+ sessionId: {
3687
+ type: 'string',
3688
+ description: 'Search within specific session (defaults to current)',
3689
+ },
3690
+ },
3691
+ required: ['query'],
3692
+ },
3693
+ },
3694
+ // Phase 4.3: Multi-Agent System
3695
+ {
3696
+ name: 'context_delegate',
3697
+ description: 'Delegate complex analysis tasks to specialized agents',
3698
+ inputSchema: {
3699
+ type: 'object',
3700
+ properties: {
3701
+ taskType: {
3702
+ type: 'string',
3703
+ enum: ['analyze', 'synthesize'],
3704
+ description: 'Type of task to delegate',
3705
+ },
3706
+ input: {
3707
+ type: 'object',
3708
+ properties: {
3709
+ analysisType: {
3710
+ type: 'string',
3711
+ enum: ['patterns', 'relationships', 'trends', 'comprehensive'],
3712
+ description: 'For analyze tasks: type of analysis',
3713
+ },
3714
+ synthesisType: {
3715
+ type: 'string',
3716
+ enum: ['summary', 'merge', 'recommendations'],
3717
+ description: 'For synthesize tasks: type of synthesis',
3718
+ },
3719
+ categories: {
3720
+ type: 'array',
3721
+ items: { type: 'string' },
3722
+ description: 'Categories to include in analysis',
3723
+ },
3724
+ timeframe: {
3725
+ type: 'string',
3726
+ description: 'Time period for analysis (e.g., "-7 days")',
3727
+ },
3728
+ maxLength: {
3729
+ type: 'number',
3730
+ description: 'Maximum length for summaries',
3731
+ },
3732
+ insights: {
3733
+ type: 'array',
3734
+ description: 'For merge synthesis: array of insights to merge',
3735
+ },
3736
+ },
3737
+ },
3738
+ chain: {
3739
+ type: 'boolean',
3740
+ description: 'Process multiple tasks in sequence',
3741
+ default: false,
3742
+ },
3743
+ sessionId: { type: 'string', description: 'Session to analyze (defaults to current)' },
3744
+ },
3745
+ required: ['taskType', 'input'],
3746
+ },
3747
+ },
3748
+ // Phase 4.4: Advanced Features
3749
+ {
3750
+ name: 'context_branch_session',
3751
+ description: 'Create a branch from current session for exploring alternatives',
3752
+ inputSchema: {
3753
+ type: 'object',
3754
+ properties: {
3755
+ branchName: {
3756
+ type: 'string',
3757
+ description: 'Name for the new branch',
3758
+ },
3759
+ copyDepth: {
3760
+ type: 'string',
3761
+ enum: ['shallow', 'deep'],
3762
+ description: 'How much to copy: shallow (high priority only) or deep (everything)',
3763
+ default: 'shallow',
3764
+ },
3765
+ },
3766
+ required: ['branchName'],
3767
+ },
3768
+ },
3769
+ {
3770
+ name: 'context_merge_sessions',
3771
+ description: 'Merge another session into the current one',
3772
+ inputSchema: {
3773
+ type: 'object',
3774
+ properties: {
3775
+ sourceSessionId: {
3776
+ type: 'string',
3777
+ description: 'ID of the session to merge from',
3778
+ },
3779
+ conflictResolution: {
3780
+ type: 'string',
3781
+ enum: ['keep_current', 'keep_source', 'keep_newest'],
3782
+ description: 'How to resolve conflicts',
3783
+ default: 'keep_current',
3784
+ },
3785
+ },
3786
+ required: ['sourceSessionId'],
3787
+ },
3788
+ },
3789
+ {
3790
+ name: 'context_journal_entry',
3791
+ description: 'Add a timestamped journal entry with optional tags and mood',
3792
+ inputSchema: {
3793
+ type: 'object',
3794
+ properties: {
3795
+ entry: {
3796
+ type: 'string',
3797
+ description: 'Journal entry text',
3798
+ },
3799
+ tags: {
3800
+ type: 'array',
3801
+ items: { type: 'string' },
3802
+ description: 'Tags for categorization',
3803
+ },
3804
+ mood: {
3805
+ type: 'string',
3806
+ description: 'Current mood/feeling',
3807
+ },
3808
+ },
3809
+ required: ['entry'],
3810
+ },
3811
+ },
3812
+ {
3813
+ name: 'context_timeline',
3814
+ description: 'Get timeline of activities with optional grouping',
3815
+ inputSchema: {
3816
+ type: 'object',
3817
+ properties: {
3818
+ startDate: {
3819
+ type: 'string',
3820
+ description: 'Start date (ISO format)',
3821
+ },
3822
+ endDate: {
3823
+ type: 'string',
3824
+ description: 'End date (ISO format)',
3825
+ },
3826
+ groupBy: {
3827
+ type: 'string',
3828
+ enum: ['hour', 'day', 'week'],
3829
+ description: 'How to group timeline data',
3830
+ default: 'day',
3831
+ },
3832
+ sessionId: {
3833
+ type: 'string',
3834
+ description: 'Session to analyze (defaults to current)',
3835
+ },
3836
+ includeItems: {
3837
+ type: 'boolean',
3838
+ description: 'Include item details in timeline',
3839
+ },
3840
+ categories: {
3841
+ type: 'array',
3842
+ items: { type: 'string' },
3843
+ description: 'Filter by categories',
3844
+ },
3845
+ relativeTime: {
3846
+ type: 'string',
3847
+ description: 'Natural language time (e.g., "2 hours ago", "today")',
3848
+ },
3849
+ itemsPerPeriod: {
3850
+ type: 'number',
3851
+ description: 'Max items per time period',
3852
+ },
3853
+ minItemsPerPeriod: {
3854
+ type: 'number',
3855
+ description: 'Only include periods with at least N items',
3856
+ },
3857
+ showEmpty: {
3858
+ type: 'boolean',
3859
+ description: 'Include periods with 0 items (default: false)',
3860
+ },
3861
+ },
3862
+ },
3863
+ },
3864
+ {
3865
+ name: 'context_compress',
3866
+ description: 'Intelligently compress old context to save space',
3867
+ inputSchema: {
3868
+ type: 'object',
3869
+ properties: {
3870
+ olderThan: {
3871
+ type: 'string',
3872
+ description: 'Compress items older than this date (ISO format)',
3873
+ },
3874
+ preserveCategories: {
3875
+ type: 'array',
3876
+ items: { type: 'string' },
3877
+ description: 'Categories to preserve (not compress)',
3878
+ },
3879
+ targetSize: {
3880
+ type: 'number',
3881
+ description: 'Target size in KB (optional)',
3882
+ },
3883
+ sessionId: {
3884
+ type: 'string',
3885
+ description: 'Session to compress (defaults to current)',
3886
+ },
3887
+ },
3888
+ },
3889
+ },
3890
+ {
3891
+ name: 'context_integrate_tool',
3892
+ description: 'Track events from other MCP tools',
3893
+ inputSchema: {
3894
+ type: 'object',
3895
+ properties: {
3896
+ toolName: {
3897
+ type: 'string',
3898
+ description: 'Name of the tool',
3899
+ },
3900
+ eventType: {
3901
+ type: 'string',
3902
+ description: 'Type of event',
3903
+ },
3904
+ data: {
3905
+ type: 'object',
3906
+ description: 'Event data',
3907
+ properties: {
3908
+ important: {
3909
+ type: 'boolean',
3910
+ description: 'Mark as important to save as context item',
3911
+ },
3912
+ },
3913
+ },
3914
+ },
3915
+ required: ['toolName', 'eventType', 'data'],
3916
+ },
3917
+ },
3918
+ {
3919
+ name: 'context_diff',
3920
+ description: 'Get changes to context items since a specific point in time (timestamp, checkpoint, or relative time)',
3921
+ inputSchema: {
3922
+ type: 'object',
3923
+ properties: {
3924
+ since: {
3925
+ type: 'string',
3926
+ description: 'Point in time to compare against (ISO timestamp, checkpoint name/ID, or relative time like "2 hours ago")',
3927
+ },
3928
+ sessionId: {
3929
+ type: 'string',
3930
+ description: 'Session ID to analyze (defaults to current)',
3931
+ },
3932
+ category: {
3933
+ type: 'string',
3934
+ description: 'Filter by category',
3935
+ },
3936
+ channel: {
3937
+ type: 'string',
3938
+ description: 'Filter by single channel',
3939
+ },
3940
+ channels: {
3941
+ type: 'array',
3942
+ items: { type: 'string' },
3943
+ description: 'Filter by multiple channels',
3944
+ },
3945
+ includeValues: {
3946
+ type: 'boolean',
3947
+ description: 'Include full item values in response',
3948
+ default: true,
3949
+ },
3950
+ limit: {
3951
+ type: 'number',
3952
+ description: 'Maximum items per category (added/modified)',
3953
+ },
3954
+ offset: {
3955
+ type: 'number',
3956
+ description: 'Pagination offset',
3957
+ },
3958
+ },
3959
+ },
3960
+ },
3961
+ {
3962
+ name: 'context_list_channels',
3963
+ description: 'List all channels with metadata (counts, activity, categories)',
3964
+ inputSchema: {
3965
+ type: 'object',
3966
+ properties: {
3967
+ sessionId: {
3968
+ type: 'string',
3969
+ description: 'Filter by specific session (shows accessible items)',
3970
+ },
3971
+ sessionIds: {
3972
+ type: 'array',
3973
+ items: { type: 'string' },
3974
+ description: 'Filter by multiple sessions',
3975
+ },
3976
+ sort: {
3977
+ type: 'string',
3978
+ enum: ['name', 'count', 'activity'],
3979
+ description: 'Sort order for results',
3980
+ default: 'name',
3981
+ },
3982
+ includeEmpty: {
3983
+ type: 'boolean',
3984
+ description: 'Include channels with no items',
3985
+ default: false,
3986
+ },
3987
+ },
3988
+ },
3989
+ },
3990
+ {
3991
+ name: 'context_channel_stats',
3992
+ description: 'Get detailed statistics for a specific channel or all channels',
3993
+ inputSchema: {
3994
+ type: 'object',
3995
+ properties: {
3996
+ channel: {
3997
+ type: 'string',
3998
+ description: 'Specific channel name (omit for all channels overview)',
3999
+ },
4000
+ sessionId: {
4001
+ type: 'string',
4002
+ description: 'Session context for privacy filtering',
4003
+ },
4004
+ includeTimeSeries: {
4005
+ type: 'boolean',
4006
+ description: 'Include hourly/daily activity data',
4007
+ default: false,
4008
+ },
4009
+ includeInsights: {
4010
+ type: 'boolean',
4011
+ description: 'Include AI-generated insights',
4012
+ default: false,
4013
+ },
4014
+ },
4015
+ },
4016
+ },
4017
+ // Context Watch - Real-time monitoring
4018
+ {
4019
+ name: 'context_watch',
4020
+ description: 'Create and manage watchers for real-time context change monitoring',
4021
+ inputSchema: {
4022
+ type: 'object',
4023
+ properties: {
4024
+ action: {
4025
+ type: 'string',
4026
+ enum: ['create', 'poll', 'stop', 'list'],
4027
+ description: 'Action to perform',
4028
+ },
4029
+ watcherId: {
4030
+ type: 'string',
4031
+ description: 'Watcher ID (required for poll/stop actions)',
4032
+ },
4033
+ filters: {
4034
+ type: 'object',
4035
+ description: 'Filters for watching specific changes (for create action)',
4036
+ properties: {
4037
+ keys: {
4038
+ type: 'array',
4039
+ items: { type: 'string' },
4040
+ description: 'Key patterns to watch (supports wildcards: *, ?)',
4041
+ },
4042
+ categories: {
4043
+ type: 'array',
4044
+ items: {
4045
+ type: 'string',
4046
+ enum: ['task', 'decision', 'progress', 'note', 'error', 'warning'],
4047
+ },
4048
+ description: 'Categories to watch',
4049
+ },
4050
+ channels: {
4051
+ type: 'array',
4052
+ items: { type: 'string' },
4053
+ description: 'Channels to watch',
4054
+ },
4055
+ priorities: {
4056
+ type: 'array',
4057
+ items: {
4058
+ type: 'string',
4059
+ enum: ['high', 'normal', 'low'],
4060
+ },
4061
+ description: 'Priority levels to watch',
4062
+ },
4063
+ },
4064
+ },
4065
+ pollTimeout: {
4066
+ type: 'number',
4067
+ description: 'Polling timeout in seconds (default: 30)',
4068
+ default: 30,
4069
+ },
4070
+ },
4071
+ required: ['action'],
4072
+ },
4073
+ },
4074
+ // Context Reassign Channel
4075
+ {
4076
+ name: 'context_reassign_channel',
4077
+ description: 'Move context items between channels based on keys, patterns, or entire channel',
4078
+ inputSchema: {
4079
+ type: 'object',
4080
+ properties: {
4081
+ keys: {
4082
+ type: 'array',
4083
+ items: { type: 'string' },
4084
+ description: 'Specific keys to reassign',
4085
+ },
4086
+ keyPattern: {
4087
+ type: 'string',
4088
+ description: 'Pattern to match keys (supports wildcards: *, ?)',
4089
+ },
4090
+ fromChannel: {
4091
+ type: 'string',
4092
+ description: 'Source channel to move all items from',
4093
+ },
4094
+ toChannel: {
4095
+ type: 'string',
4096
+ description: 'Target channel to move items to',
4097
+ },
4098
+ sessionId: {
4099
+ type: 'string',
4100
+ description: 'Session ID (defaults to current)',
4101
+ },
4102
+ category: {
4103
+ type: 'string',
4104
+ enum: ['task', 'decision', 'progress', 'note', 'error', 'warning'],
4105
+ description: 'Filter by category',
4106
+ },
4107
+ priorities: {
4108
+ type: 'array',
4109
+ items: {
4110
+ type: 'string',
4111
+ enum: ['high', 'normal', 'low'],
4112
+ },
4113
+ description: 'Filter by priority levels',
4114
+ },
4115
+ dryRun: {
4116
+ type: 'boolean',
4117
+ description: 'Preview changes without applying them',
4118
+ default: false,
4119
+ },
4120
+ },
4121
+ required: ['toChannel'],
4122
+ },
4123
+ },
4124
+ // Batch Operations
4125
+ {
4126
+ name: 'context_batch_save',
4127
+ description: 'Save multiple context items in a single atomic operation',
4128
+ inputSchema: {
4129
+ type: 'object',
4130
+ properties: {
4131
+ items: {
4132
+ type: 'array',
4133
+ description: 'Array of items to save',
4134
+ items: {
4135
+ type: 'object',
4136
+ properties: {
4137
+ key: { type: 'string', description: 'Unique key for the context item' },
4138
+ value: { type: 'string', description: 'Context value to save' },
4139
+ category: {
4140
+ type: 'string',
4141
+ description: 'Category (e.g., task, decision, progress)',
4142
+ enum: ['task', 'decision', 'progress', 'note', 'error', 'warning'],
4143
+ },
4144
+ priority: {
4145
+ type: 'string',
4146
+ description: 'Priority level',
4147
+ enum: ['high', 'normal', 'low'],
4148
+ },
4149
+ channel: {
4150
+ type: 'string',
4151
+ description: 'Channel to organize this item',
4152
+ },
4153
+ },
4154
+ required: ['key', 'value'],
4155
+ },
4156
+ },
4157
+ updateExisting: {
4158
+ type: 'boolean',
4159
+ description: 'Update existing items with same key (default: true)',
4160
+ default: true,
4161
+ },
4162
+ },
4163
+ required: ['items'],
4164
+ },
4165
+ },
4166
+ {
4167
+ name: 'context_batch_delete',
4168
+ description: 'Delete multiple context items by keys or pattern in a single atomic operation',
4169
+ inputSchema: {
4170
+ type: 'object',
4171
+ properties: {
4172
+ keys: {
4173
+ type: 'array',
4174
+ items: { type: 'string' },
4175
+ description: 'Array of specific keys to delete',
4176
+ },
4177
+ keyPattern: {
4178
+ type: 'string',
4179
+ description: 'Pattern to match keys for deletion (supports wildcards: *, ?)',
4180
+ },
4181
+ sessionId: {
4182
+ type: 'string',
4183
+ description: 'Session ID (defaults to current)',
4184
+ },
4185
+ dryRun: {
4186
+ type: 'boolean',
4187
+ description: 'Preview items to be deleted without actually deleting',
4188
+ default: false,
4189
+ },
4190
+ },
4191
+ },
4192
+ },
4193
+ {
4194
+ name: 'context_batch_update',
4195
+ description: 'Update multiple context items with partial updates in a single atomic operation',
4196
+ inputSchema: {
4197
+ type: 'object',
4198
+ properties: {
4199
+ updates: {
4200
+ type: 'array',
4201
+ description: 'Array of updates to apply',
4202
+ items: {
4203
+ type: 'object',
4204
+ properties: {
4205
+ key: { type: 'string', description: 'Key of the item to update' },
4206
+ value: { type: 'string', description: 'New value (optional)' },
4207
+ category: {
4208
+ type: 'string',
4209
+ description: 'New category (optional)',
4210
+ enum: ['task', 'decision', 'progress', 'note', 'error', 'warning'],
4211
+ },
4212
+ priority: {
4213
+ type: 'string',
4214
+ description: 'New priority (optional)',
4215
+ enum: ['high', 'normal', 'low'],
4216
+ },
4217
+ channel: {
4218
+ type: 'string',
4219
+ description: 'New channel (optional)',
4220
+ },
4221
+ },
4222
+ required: ['key'],
4223
+ },
4224
+ },
4225
+ sessionId: {
4226
+ type: 'string',
4227
+ description: 'Session ID (defaults to current)',
4228
+ },
4229
+ },
4230
+ required: ['updates'],
4231
+ },
4232
+ },
4233
+ // Context Relationships
4234
+ {
4235
+ name: 'context_link',
4236
+ description: 'Create a relationship between two context items',
4237
+ inputSchema: {
4238
+ type: 'object',
4239
+ properties: {
4240
+ sourceKey: {
4241
+ type: 'string',
4242
+ description: 'Key of the source context item',
4243
+ },
4244
+ targetKey: {
4245
+ type: 'string',
4246
+ description: 'Key of the target context item',
4247
+ },
4248
+ relationship: {
4249
+ type: 'string',
4250
+ description: 'Type of relationship',
4251
+ enum: [
4252
+ 'contains',
4253
+ 'depends_on',
4254
+ 'references',
4255
+ 'implements',
4256
+ 'extends',
4257
+ 'related_to',
4258
+ 'blocks',
4259
+ 'blocked_by',
4260
+ 'parent_of',
4261
+ 'child_of',
4262
+ 'has_task',
4263
+ 'documented_in',
4264
+ 'serves',
4265
+ 'leads_to',
4266
+ ],
4267
+ },
4268
+ metadata: {
4269
+ type: 'object',
4270
+ description: 'Optional metadata for the relationship',
4271
+ },
4272
+ },
4273
+ required: ['sourceKey', 'targetKey', 'relationship'],
4274
+ },
4275
+ },
4276
+ {
4277
+ name: 'context_get_related',
4278
+ description: 'Get items related to a given context item',
4279
+ inputSchema: {
4280
+ type: 'object',
4281
+ properties: {
4282
+ key: {
4283
+ type: 'string',
4284
+ description: 'Key of the context item to find relationships for',
4285
+ },
4286
+ relationship: {
4287
+ type: 'string',
4288
+ description: 'Filter by specific relationship type',
4289
+ },
4290
+ depth: {
4291
+ type: 'number',
4292
+ description: 'Traversal depth for multi-level relationships (default: 1)',
4293
+ default: 1,
4294
+ },
4295
+ direction: {
4296
+ type: 'string',
4297
+ description: 'Direction of relationships to retrieve',
4298
+ enum: ['outgoing', 'incoming', 'both'],
4299
+ default: 'both',
4300
+ },
4301
+ },
4302
+ required: ['key'],
4303
+ },
4304
+ },
4305
+ ],
4306
+ };
4307
+ });
4308
+ // Start server
4309
+ const transport = new stdio_js_1.StdioServerTransport();
4310
+ server.connect(transport);