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/config/claude-desktop-config.json +12 -0
- package/dist/embeddings-gemini.d.ts +76 -0
- package/dist/embeddings-gemini.d.ts.map +1 -0
- package/dist/embeddings-gemini.js +158 -0
- package/dist/embeddings-gemini.js.map +1 -0
- package/dist/embeddings.d.ts +67 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +252 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +307 -0
- package/dist/index.js.map +1 -0
- package/dist/project-isolation.d.ts +29 -0
- package/dist/project-isolation.d.ts.map +1 -0
- package/dist/project-isolation.js +78 -0
- package/dist/project-isolation.js.map +1 -0
- package/package.json +66 -0
- package/src/embeddings-gemini.ts +176 -0
- package/src/embeddings.ts +296 -0
- package/src/index.ts +366 -0
- package/src/project-isolation.ts +93 -0
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
|
+
};
|