claude-self-reflect 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ErrorCode,
7
+ ListToolsRequestSchema,
8
+ McpError,
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import { QdrantClient } from '@qdrant/js-client-rest';
11
+ import { createEmbeddingService, detectEmbeddingModel, EmbeddingService } from './embeddings.js';
12
+ import { ProjectIsolationManager, ProjectIsolationConfig, DEFAULT_ISOLATION_CONFIG } from './project-isolation.js';
13
+
14
+ const QDRANT_URL = process.env.QDRANT_URL || 'http://localhost:6333';
15
+ const COLLECTION_NAME = process.env.COLLECTION_NAME || 'conversations';
16
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
17
+ const VOYAGE_API_KEY = process.env.VOYAGE_KEY || process.env['VOYAGE_KEY-2'];
18
+ const PREFER_LOCAL_EMBEDDINGS = process.env.PREFER_LOCAL_EMBEDDINGS === 'true';
19
+ const ISOLATION_MODE = process.env.ISOLATION_MODE || 'hybrid';
20
+ const ALLOW_CROSS_PROJECT = process.env.ALLOW_CROSS_PROJECT === 'true';
21
+
22
+ interface SearchResult {
23
+ id: string;
24
+ score: number;
25
+ timestamp: string;
26
+ role: string;
27
+ excerpt: string;
28
+ projectName?: string;
29
+ conversationId?: string;
30
+ }
31
+
32
+ class SelfReflectionServer {
33
+ private server: Server;
34
+ private qdrantClient: QdrantClient;
35
+ private embeddingService?: EmbeddingService;
36
+ private collectionInfo?: { model: string; dimensions: number };
37
+ private isolationManager: ProjectIsolationManager;
38
+ private currentProject?: string;
39
+
40
+ constructor() {
41
+ this.server = new Server(
42
+ {
43
+ name: 'claude-self-reflection',
44
+ version: '0.1.0',
45
+ },
46
+ {
47
+ capabilities: {
48
+ tools: {},
49
+ },
50
+ }
51
+ );
52
+
53
+ this.qdrantClient = new QdrantClient({ url: QDRANT_URL });
54
+
55
+ // Initialize project isolation
56
+ const isolationConfig: ProjectIsolationConfig = {
57
+ mode: ISOLATION_MODE as 'isolated' | 'shared' | 'hybrid',
58
+ allowCrossProject: ALLOW_CROSS_PROJECT,
59
+ projectIdentifier: ProjectIsolationManager.detectCurrentProject()
60
+ };
61
+ this.isolationManager = new ProjectIsolationManager(this.qdrantClient, isolationConfig);
62
+ this.currentProject = isolationConfig.projectIdentifier;
63
+
64
+ this.setupToolHandlers();
65
+ }
66
+
67
+ private async initialize() {
68
+ try {
69
+ // Create embedding service with Voyage AI if available
70
+ this.embeddingService = await createEmbeddingService({
71
+ openaiApiKey: OPENAI_API_KEY,
72
+ voyageApiKey: VOYAGE_API_KEY,
73
+ preferLocal: PREFER_LOCAL_EMBEDDINGS,
74
+ });
75
+
76
+ // For Voyage collections, we don't need to check dimensions as they're consistent
77
+ if (this.embeddingService.getModelName().includes('voyage')) {
78
+ console.error('Using Voyage AI embeddings for search across project collections');
79
+ }
80
+ } catch (error) {
81
+ console.error('Failed to initialize embedding service:', error);
82
+ console.error('Server will run in degraded mode with text-based search');
83
+ }
84
+ }
85
+
86
+ private setupToolHandlers() {
87
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
88
+ tools: [
89
+ {
90
+ name: 'reflect_on_past',
91
+ description: 'Search for relevant past conversations using semantic search',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ query: {
96
+ type: 'string',
97
+ description: 'The search query to find semantically similar conversations',
98
+ },
99
+ limit: {
100
+ type: 'number',
101
+ description: 'Maximum number of results to return (default: 5)',
102
+ default: 5,
103
+ },
104
+ project: {
105
+ type: 'string',
106
+ description: 'Filter by project name (optional)',
107
+ },
108
+ crossProject: {
109
+ type: 'boolean',
110
+ description: 'Search across all projects (default: false, requires permission)',
111
+ default: false,
112
+ },
113
+ minScore: {
114
+ type: 'number',
115
+ description: 'Minimum similarity score (0-1, default: 0.7)',
116
+ default: 0.7,
117
+ },
118
+ },
119
+ required: ['query'],
120
+ },
121
+ },
122
+ {
123
+ name: 'store_reflection',
124
+ description: 'Store an important insight or reflection for future reference',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ content: {
129
+ type: 'string',
130
+ description: 'The insight or reflection to store',
131
+ },
132
+ tags: {
133
+ type: 'array',
134
+ items: { type: 'string' },
135
+ description: 'Tags to categorize this reflection',
136
+ },
137
+ },
138
+ required: ['content'],
139
+ },
140
+ },
141
+ ],
142
+ }));
143
+
144
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
145
+ if (request.params.name === 'reflect_on_past') {
146
+ return this.handleReflectOnPast(request.params.arguments);
147
+ } else if (request.params.name === 'store_reflection') {
148
+ return this.handleStoreReflection(request.params.arguments);
149
+ }
150
+
151
+ throw new McpError(
152
+ ErrorCode.MethodNotFound,
153
+ `Unknown tool: ${request.params.name}`
154
+ );
155
+ });
156
+ }
157
+
158
+ private async getVoyageCollections(): Promise<string[]> {
159
+ try {
160
+ const collections = await this.qdrantClient.getCollections();
161
+ return collections.collections
162
+ .map(c => c.name)
163
+ .filter(name => name.endsWith('_voyage'));
164
+ } catch (error) {
165
+ console.error('Failed to get Voyage collections:', error);
166
+ return [];
167
+ }
168
+ }
169
+
170
+ private async handleReflectOnPast(args: any) {
171
+ const { query, limit = 5, project, minScore = 0.7, crossProject = false } = args;
172
+
173
+ try {
174
+ // Initialize if not already done
175
+ if (!this.embeddingService) {
176
+ await this.initialize();
177
+ }
178
+
179
+ let results: SearchResult[] = [];
180
+
181
+ if (this.embeddingService) {
182
+ // Use vector search with embeddings
183
+ console.error(`Generating embedding for query: "${query}"`);
184
+ const queryEmbedding = await this.embeddingService.generateEmbedding(query);
185
+
186
+ // Get all Voyage collections
187
+ const voyageCollections = await this.getVoyageCollections();
188
+
189
+ if (voyageCollections.length === 0) {
190
+ console.error('No Voyage collections found');
191
+ return {
192
+ content: [
193
+ {
194
+ type: 'text',
195
+ text: `No Voyage collections found. Please run the import process first.`,
196
+ },
197
+ ],
198
+ };
199
+ }
200
+
201
+ console.error(`Searching across ${voyageCollections.length} Voyage collections: ${voyageCollections.slice(0, 3).join(', ')}...`);
202
+
203
+ // Search across multiple collections
204
+ const searchPromises = voyageCollections.map(async (collectionName) => {
205
+ try {
206
+ const searchResponse = await this.qdrantClient.search(collectionName, {
207
+ vector: queryEmbedding,
208
+ limit: Math.ceil(limit * 1.5), // Get extra results per collection
209
+ score_threshold: minScore,
210
+ with_payload: true,
211
+ });
212
+
213
+ if (searchResponse.length > 0) {
214
+ console.error(`Collection ${collectionName}: Found ${searchResponse.length} results`);
215
+ }
216
+
217
+ return searchResponse.map(point => ({
218
+ id: point.id as string,
219
+ score: point.score,
220
+ timestamp: point.payload?.timestamp as string || new Date().toISOString(),
221
+ role: (point.payload?.start_role as string) || (point.payload?.role as string) || 'unknown',
222
+ excerpt: ((point.payload?.text as string) || '').substring(0, 500) + '...',
223
+ projectName: (point.payload?.project as string) || (point.payload?.project_name as string) || (point.payload?.project_id as string) || collectionName.replace('conv_', '').replace('_voyage', ''),
224
+ conversationId: point.payload?.conversation_id as string,
225
+ collectionName,
226
+ }));
227
+ } catch (error) {
228
+ console.error(`Failed to search collection ${collectionName}:`, error);
229
+ return [];
230
+ }
231
+ });
232
+
233
+ // Wait for all searches to complete
234
+ const allResults = await Promise.all(searchPromises);
235
+
236
+ // Flatten and sort by score
237
+ const flatResults = allResults.flat();
238
+ console.error(`Found ${flatResults.length} total results across all collections`);
239
+
240
+ results = flatResults
241
+ .sort((a, b) => b.score - a.score)
242
+ .slice(0, limit);
243
+
244
+ } else {
245
+ // Fallback to text search
246
+ console.error('Using fallback text search (no embeddings available)');
247
+
248
+ const voyageCollections = await this.getVoyageCollections();
249
+
250
+ // Search across all collections with text matching
251
+ const searchPromises = voyageCollections.map(async (collectionName) => {
252
+ try {
253
+ const scrollResponse = await this.qdrantClient.scroll(collectionName, {
254
+ limit: 100,
255
+ with_payload: true,
256
+ with_vector: false,
257
+ });
258
+
259
+ const queryWords = query.toLowerCase().split(/\s+/);
260
+ return scrollResponse.points
261
+ .filter(point => {
262
+ const text = (point.payload?.text as string || '').toLowerCase();
263
+ return queryWords.some((word: string) => text.includes(word));
264
+ })
265
+ .map(point => ({
266
+ id: point.id as string,
267
+ score: 0.5,
268
+ timestamp: point.payload?.timestamp as string || new Date().toISOString(),
269
+ role: (point.payload?.start_role as string) || (point.payload?.role as string) || 'unknown',
270
+ excerpt: ((point.payload?.text as string) || '').substring(0, 500) + '...',
271
+ projectName: (point.payload?.project as string) || (point.payload?.project_name as string) || (point.payload?.project_id as string) || collectionName.replace('conv_', '').replace('_voyage', ''),
272
+ conversationId: point.payload?.conversation_id as string,
273
+ collectionName,
274
+ }));
275
+ } catch (error) {
276
+ console.error(`Failed to search collection ${collectionName}:`, error);
277
+ return [];
278
+ }
279
+ });
280
+
281
+ const allResults = await Promise.all(searchPromises);
282
+ results = allResults
283
+ .flat()
284
+ .slice(0, limit);
285
+ }
286
+
287
+ if (results.length === 0) {
288
+ return {
289
+ content: [
290
+ {
291
+ type: 'text',
292
+ text: `No conversations found matching "${query}". Try different keywords or check if conversations have been imported.`,
293
+ },
294
+ ],
295
+ };
296
+ }
297
+
298
+ const resultText = results
299
+ .map((result, i) =>
300
+ `**Result ${i + 1}** (Score: ${result.score.toFixed(3)})\n` +
301
+ `Time: ${new Date(result.timestamp).toLocaleString()}\n` +
302
+ `Project: ${result.projectName || 'unknown'}\n` +
303
+ `Role: ${result.role}\n` +
304
+ `Excerpt: ${result.excerpt}\n` +
305
+ `---`
306
+ )
307
+ .join('\n\n');
308
+
309
+ return {
310
+ content: [
311
+ {
312
+ type: 'text',
313
+ text: `Found ${results.length} relevant conversation(s) for "${query}":\n\n${resultText}`,
314
+ },
315
+ ],
316
+ };
317
+ } catch (error) {
318
+ console.error('Error searching conversations:', error);
319
+ throw new McpError(
320
+ ErrorCode.InternalError,
321
+ `Failed to search conversations: ${error instanceof Error ? error.message : String(error)}`
322
+ );
323
+ }
324
+ }
325
+
326
+ private async handleStoreReflection(args: any) {
327
+ const { content, tags = [] } = args;
328
+
329
+ try {
330
+ // This is a placeholder for now
331
+ // In production, we'd store this as a special type of conversation chunk
332
+ return {
333
+ content: [
334
+ {
335
+ type: 'text',
336
+ text: `Reflection stored successfully with tags: ${tags.join(', ') || 'none'}`,
337
+ },
338
+ ],
339
+ };
340
+ } catch (error) {
341
+ throw new McpError(
342
+ ErrorCode.InternalError,
343
+ `Failed to store reflection: ${error instanceof Error ? error.message : String(error)}`
344
+ );
345
+ }
346
+ }
347
+
348
+ async run() {
349
+ // Initialize embedding service early
350
+ await this.initialize();
351
+
352
+ const transport = new StdioServerTransport();
353
+ await this.server.connect(transport);
354
+ console.error('Claude Self-Reflection MCP server running');
355
+ console.error(`Connected to Qdrant at ${QDRANT_URL}`);
356
+ console.error(`Embedding service: ${this.embeddingService?.getModelName() || 'none (text search only)'}`);
357
+ console.error(`Voyage API Key: ${VOYAGE_API_KEY ? 'Set' : 'Not set'}`);
358
+
359
+ // Check for Voyage collections
360
+ const voyageCollections = await this.getVoyageCollections();
361
+ console.error(`Found ${voyageCollections.length} Voyage collections ready for search`);
362
+ }
363
+ }
364
+
365
+ const server = new SelfReflectionServer();
366
+ server.run().catch(console.error);
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'crypto';
2
+ import { QdrantClient } from '@qdrant/js-client-rest';
3
+
4
+ export interface ProjectIsolationConfig {
5
+ mode: 'isolated' | 'shared' | 'hybrid';
6
+ allowCrossProject: boolean;
7
+ projectIdentifier?: string;
8
+ }
9
+
10
+ export class ProjectIsolationManager {
11
+ private client: QdrantClient;
12
+ private config: ProjectIsolationConfig;
13
+
14
+ constructor(client: QdrantClient, config: ProjectIsolationConfig) {
15
+ this.client = client;
16
+ this.config = config;
17
+ }
18
+
19
+ /**
20
+ * Get collection name based on isolation mode
21
+ */
22
+ getCollectionName(projectName?: string): string {
23
+ if (this.config.mode === 'isolated' && projectName) {
24
+ // Create project-specific collection name
25
+ const projectHash = createHash('md5')
26
+ .update(projectName)
27
+ .digest('hex')
28
+ .substring(0, 8);
29
+ return `conv_${projectHash}`;
30
+ }
31
+
32
+ // Default to shared collection
33
+ return 'conversations';
34
+ }
35
+
36
+ /**
37
+ * Get project filter for queries
38
+ */
39
+ getProjectFilter(projectName?: string): any {
40
+ if (!projectName || this.config.mode === 'shared') {
41
+ return {};
42
+ }
43
+
44
+ return {
45
+ filter: {
46
+ must: [{
47
+ key: 'project_id',
48
+ match: { value: projectName }
49
+ }]
50
+ }
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Detect current project from environment
56
+ */
57
+ static detectCurrentProject(): string | undefined {
58
+ // Check environment variables
59
+ const fromEnv = process.env.CLAUDE_PROJECT_NAME;
60
+ if (fromEnv) return fromEnv;
61
+
62
+ // Try to detect from working directory
63
+ const cwd = process.cwd();
64
+ const projectMatch = cwd.match(/\/([^\/]+)$/);
65
+ return projectMatch ? projectMatch[1] : undefined;
66
+ }
67
+
68
+ /**
69
+ * Ensure collection exists for project
70
+ */
71
+ async ensureProjectCollection(projectName: string, vectorSize: number): Promise<void> {
72
+ const collectionName = this.getCollectionName(projectName);
73
+
74
+ try {
75
+ await this.client.getCollection(collectionName);
76
+ } catch (error) {
77
+ // Collection doesn't exist, create it
78
+ await this.client.createCollection(collectionName, {
79
+ vectors: {
80
+ size: vectorSize,
81
+ distance: 'Cosine'
82
+ }
83
+ });
84
+ console.error(`Created project-specific collection: ${collectionName}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ export const DEFAULT_ISOLATION_CONFIG: ProjectIsolationConfig = {
90
+ mode: 'hybrid',
91
+ allowCrossProject: false,
92
+ projectIdentifier: ProjectIsolationManager.detectCurrentProject()
93
+ };