delegate-sf-mcp 0.2.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 (44) hide show
  1. package/.eslintrc.json +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +76 -0
  4. package/auth.js +148 -0
  5. package/bin/config-helper.js +51 -0
  6. package/bin/mcp-salesforce.js +12 -0
  7. package/bin/setup.js +266 -0
  8. package/bin/status.js +134 -0
  9. package/docs/README.md +52 -0
  10. package/docs/step1.png +0 -0
  11. package/docs/step2.png +0 -0
  12. package/docs/step3.png +0 -0
  13. package/docs/step4.png +0 -0
  14. package/examples/README.md +35 -0
  15. package/package.json +16 -0
  16. package/scripts/README.md +30 -0
  17. package/src/auth/file-storage.js +447 -0
  18. package/src/auth/oauth.js +417 -0
  19. package/src/auth/token-manager.js +207 -0
  20. package/src/backup/manager.js +949 -0
  21. package/src/index.js +168 -0
  22. package/src/salesforce/client.js +388 -0
  23. package/src/sf-client.js +79 -0
  24. package/src/tools/auth.js +190 -0
  25. package/src/tools/backup.js +486 -0
  26. package/src/tools/create.js +109 -0
  27. package/src/tools/delegate-hygiene.js +268 -0
  28. package/src/tools/delegate-validate.js +212 -0
  29. package/src/tools/delegate-verify.js +143 -0
  30. package/src/tools/delete.js +72 -0
  31. package/src/tools/describe.js +132 -0
  32. package/src/tools/installation-info.js +656 -0
  33. package/src/tools/learn-context.js +1077 -0
  34. package/src/tools/learn.js +351 -0
  35. package/src/tools/query.js +82 -0
  36. package/src/tools/repair-credentials.js +77 -0
  37. package/src/tools/setup.js +120 -0
  38. package/src/tools/time_machine.js +347 -0
  39. package/src/tools/update.js +138 -0
  40. package/src/tools.js +214 -0
  41. package/src/utils/cache.js +120 -0
  42. package/src/utils/debug.js +52 -0
  43. package/src/utils/logger.js +19 -0
  44. package/tokens.json +8 -0
@@ -0,0 +1,1077 @@
1
+ /**
2
+ * Salesforce Context Learning Tool
3
+ *
4
+ * This tool learns personal and business context about the user and their
5
+ * Salesforce data model relationships. It stores this information persistently
6
+ * to provide better context-aware assistance across sessions.
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { getInstallationDocumentation, hasInstallationDocumentation } from './learn.js';
13
+ import { debug } from '../utils/debug.js';
14
+ import { getCacheFilePath, ensureCacheDirectory } from '../utils/cache.js';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const CONTEXT_FILE = getCacheFilePath('salesforce-context.json');
19
+
20
+ export const salesforceLearnContextTool = {
21
+ name: "salesforce_learn_context",
22
+ description: "Learn and store personal/business context about the user and their Salesforce data model relationships. This helps provide better context-aware assistance across sessions. PROACTIVELY CAPTURE AHA MOMENTS: Whenever you discover something important about the user's workflow, business processes, preferences, challenges, or breakthrough insights during conversations, automatically use store_learning to preserve this knowledge. Look for moments when the user reveals key information, expresses frustration, shares successful strategies, or has realizations - these are valuable learnings that should be stored immediately.",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ action: {
27
+ type: "string",
28
+ enum: ["start_interview", "answer_question", "show_context", "reset_context", "suggest_questions", "quick_setup", "store_learning"],
29
+ description: "Action to perform: start_interview (begin learning), answer_question (provide answers), show_context (display stored context), reset_context (clear all), suggest_questions (get intelligent questions based on data model), quick_setup (explain everything in one go), store_learning (AUTOMATICALLY capture breakthrough insights, aha moments, user preferences, workflow patterns, pain points, or any valuable context discovered during conversation)"
30
+ },
31
+ question_id: {
32
+ type: "string",
33
+ description: "ID of the question being answered (when action is 'answer_question')"
34
+ },
35
+ answer: {
36
+ type: "string",
37
+ description: "Answer to the question (when action is 'answer_question')"
38
+ },
39
+ context_type: {
40
+ type: "string",
41
+ description: "Type of context to focus on (for show_context and suggest_questions). Can be any section name like 'personal', 'business', 'data_model', 'technical_preferences', etc., or 'all' for everything",
42
+ default: "all"
43
+ },
44
+ // Quick setup parameters
45
+ full_name: {
46
+ type: "string",
47
+ description: "Your full name (for quick_setup)"
48
+ },
49
+ email: {
50
+ type: "string",
51
+ description: "Your email address (for quick_setup)"
52
+ },
53
+ role: {
54
+ type: "string",
55
+ description: "Your professional role/position (for quick_setup)"
56
+ },
57
+ company_name: {
58
+ type: "string",
59
+ description: "Your company name (for quick_setup)"
60
+ },
61
+ industry: {
62
+ type: "string",
63
+ description: "Your company's industry (for quick_setup)"
64
+ },
65
+ business_process_description: {
66
+ type: "string",
67
+ description: "Complete description of your business processes, how you use Salesforce, what you do, etc. (for quick_setup)"
68
+ },
69
+ // Dynamic learning parameters
70
+ section: {
71
+ type: "string",
72
+ description: "Context section to store the learning in (for store_learning). Use descriptive names that capture the nature of the insight: 'aha_moments' for breakthrough realizations, 'pain_points' for challenges discovered, 'workflow_insights' for process discoveries, 'preferences' for user likes/dislikes, 'success_patterns' for what works well, 'technical_discoveries' for system insights, etc. Will be created dynamically if it doesn't exist."
73
+ },
74
+ key: {
75
+ type: "string",
76
+ description: "Key name for the learning (for store_learning). Use specific, descriptive names that capture the insight: 'critical_realization_about_X', 'main_frustration_with_Y', 'breakthrough_solution_for_Z', 'preferred_approach_to_A', 'discovered_workflow_pattern_B', etc. Be specific about what was learned."
77
+ },
78
+ value: {
79
+ type: "string",
80
+ description: "Value/content of the learning (for store_learning)"
81
+ },
82
+ overwrite: {
83
+ type: "boolean",
84
+ description: "Whether to overwrite existing values for the same key (for store_learning). Default: false",
85
+ default: false
86
+ }
87
+ },
88
+ required: ["action"]
89
+ }
90
+ };
91
+
92
+ export async function handleSalesforceLearnContext(args) {
93
+ const { action, question_id, answer, context_type = "all", full_name, email, role, company_name, industry, business_process_description, section, key, value, overwrite = false } = args;
94
+
95
+ try {
96
+ switch (action) {
97
+ case "start_interview":
98
+ return await startContextInterview();
99
+ case "answer_question":
100
+ return await answerContextQuestion(question_id, answer);
101
+ case "show_context":
102
+ return await showStoredContext(context_type);
103
+ case "reset_context":
104
+ return await resetContext();
105
+ case "suggest_questions":
106
+ return await suggestIntelligentQuestions(context_type);
107
+ case "quick_setup":
108
+ return await quickSetupContext({ full_name, email, role, company_name, industry, business_process_description });
109
+ case "store_learning":
110
+ return await storeDynamicLearning({ section, key, value, overwrite });
111
+ default:
112
+ return {
113
+ content: [{
114
+ type: "text",
115
+ text: `❌ **Invalid action:** ${action}\n\nSupported actions: start_interview, answer_question, show_context, reset_context, suggest_questions, quick_setup, store_learning`
116
+ }]
117
+ };
118
+ }
119
+ } catch (error) {
120
+ debug.error('❌ Error in context learning:', error);
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: `❌ **Error:** ${error.message}`
125
+ }]
126
+ };
127
+ }
128
+ }
129
+
130
+ async function startContextInterview() {
131
+ const context = await loadContext();
132
+
133
+ // Generate initial questions based on what we don't know yet
134
+ const questions = [];
135
+
136
+ // Personal context questions
137
+ if (!context.personal?.name) {
138
+ questions.push({
139
+ id: "personal_name",
140
+ category: "personal",
141
+ question: "What is your full name (first and last name)?",
142
+ type: "text"
143
+ });
144
+ }
145
+
146
+ if (!context.personal?.email) {
147
+ questions.push({
148
+ id: "personal_email",
149
+ category: "personal",
150
+ question: "What is your email address?",
151
+ type: "email"
152
+ });
153
+ }
154
+
155
+ if (!context.personal?.role) {
156
+ questions.push({
157
+ id: "personal_role",
158
+ category: "personal",
159
+ question: "What is your professional position/role?",
160
+ type: "text"
161
+ });
162
+ }
163
+
164
+ // Business context questions
165
+ if (!context.business?.company_name) {
166
+ questions.push({
167
+ id: "business_company",
168
+ category: "business",
169
+ question: "Which company do you work for?",
170
+ type: "text"
171
+ });
172
+ }
173
+
174
+ if (!context.business?.industry) {
175
+ questions.push({
176
+ id: "business_industry",
177
+ category: "business",
178
+ question: "What industry is your company in?",
179
+ type: "text"
180
+ });
181
+ }
182
+
183
+ if (!context.business?.business_focus) {
184
+ questions.push({
185
+ id: "business_focus",
186
+ category: "business",
187
+ question: "What does your company do exactly? What products/services do you offer?",
188
+ type: "textarea"
189
+ });
190
+ }
191
+
192
+ // Store pending questions
193
+ context.interview = {
194
+ status: "in_progress",
195
+ started_at: new Date().toISOString(),
196
+ pending_questions: questions,
197
+ answered_questions: context.interview?.answered_questions || []
198
+ };
199
+
200
+ await saveContext(context);
201
+
202
+ if (questions.length === 0) {
203
+ return {
204
+ content: [{
205
+ type: "text",
206
+ text: `✅ **Context interview already complete!**\n\n` +
207
+ `All basic information has already been captured.\n\n` +
208
+ `💡 **Next steps:**\n` +
209
+ `- Use \`suggest_questions\` for advanced questions\n` +
210
+ `- Use \`show_context\` to display current context\n` +
211
+ `- Use \`reset_context\` to start over`
212
+ }]
213
+ };
214
+ }
215
+
216
+ const firstQuestion = questions[0];
217
+ let result = `🎤 **Context interview started**\n\n`;
218
+ result += `I'll ask you some questions to get to know you and your company better. `;
219
+ result += `This information will be saved and help me provide better support in future sessions.\n\n`;
220
+ result += `**Progress:** ${context.interview.answered_questions.length}/${questions.length + context.interview.answered_questions.length} questions answered\n\n`;
221
+ result += `---\n\n`;
222
+ result += `**${firstQuestion.category.toUpperCase()} - Question ${context.interview.answered_questions.length + 1}:**\n\n`;
223
+ result += `${firstQuestion.question}\n\n`;
224
+ result += `*Answer with:*\n`;
225
+ result += `\`\`\`json\n`;
226
+ result += `{\n`;
227
+ result += ` "action": "answer_question",\n`;
228
+ result += ` "question_id": "${firstQuestion.id}",\n`;
229
+ result += ` "answer": "Your answer here"\n`;
230
+ result += `}\n`;
231
+ result += `\`\`\``;
232
+
233
+ return {
234
+ content: [{
235
+ type: "text",
236
+ text: result
237
+ }]
238
+ };
239
+ }
240
+
241
+ async function answerContextQuestion(questionId, answer) {
242
+ const context = await loadContext();
243
+
244
+ // Allow additional questions even after interview completion
245
+ // Check if this is a data_model question or other additional questions
246
+ const isDataModelQuestion = questionId.startsWith('data_model') || questionId.includes('data_model');
247
+ const predefinedAdditionalQuestions = ['data_model_details', 'custom_objects_purpose', 'business_processes', 'integration_systems', 'reporting_needs'];
248
+ const isAdditionalQuestion = isDataModelQuestion || predefinedAdditionalQuestions.includes(questionId) || questionId.startsWith('additional_');
249
+
250
+ if (!context.interview) {
251
+ return {
252
+ content: [{
253
+ type: "text",
254
+ text: `⚠️ **No interview found**\n\nFirst start an interview with \`action: "start_interview"\``
255
+ }]
256
+ };
257
+ }
258
+
259
+ // For basic interview questions, require in_progress status
260
+ if (!isAdditionalQuestion && context.interview.status !== "in_progress") {
261
+ return {
262
+ content: [{
263
+ type: "text",
264
+ text: `⚠️ **Basic interview already completed**\n\n` +
265
+ `The basic interview is finished. You can:\n` +
266
+ `- Use \`suggest_questions\` for advanced questions\n` +
267
+ `- Add data model details with questions like \`data_model_details\`\n` +
268
+ `- Use \`reset_context\` to start a new interview`
269
+ }]
270
+ };
271
+ }
272
+
273
+ // Handle additional questions after interview completion
274
+ if (isAdditionalQuestion && context.interview.status === "completed") {
275
+ return await handleAdditionalQuestion(questionId, answer, context);
276
+ }
277
+
278
+ const questionIndex = context.interview.pending_questions.findIndex(q => q.id === questionId);
279
+ if (questionIndex === -1) {
280
+ // Check if this is a data_model question that should be handled differently
281
+ if (isDataModelQuestion) {
282
+ return await handleAdditionalQuestion(questionId, answer, context);
283
+ }
284
+ return {
285
+ content: [{
286
+ type: "text",
287
+ text: `❌ **Invalid question ID:** ${questionId}\n\nUse the ID from the current question or use \`suggest_questions\` to see available questions.`
288
+ }]
289
+ };
290
+ }
291
+
292
+ const question = context.interview.pending_questions[questionIndex];
293
+
294
+ // Store the answer in the appropriate context section
295
+ const [category, field] = question.id.split('_', 2);
296
+ if (!context[category]) context[category] = {};
297
+
298
+ // Map question IDs to context fields
299
+ const fieldMappings = {
300
+ 'personal_name': 'name',
301
+ 'personal_email': 'email',
302
+ 'personal_role': 'role',
303
+ 'business_company': 'company_name',
304
+ 'business_industry': 'industry',
305
+ 'business_focus': 'business_focus'
306
+ };
307
+
308
+ const contextField = fieldMappings[question.id] || field;
309
+ context[category][contextField] = answer;
310
+
311
+ // Move question from pending to answered
312
+ context.interview.answered_questions.push({
313
+ ...question,
314
+ answer: answer,
315
+ answered_at: new Date().toISOString()
316
+ });
317
+ context.interview.pending_questions.splice(questionIndex, 1);
318
+
319
+ await saveContext(context);
320
+
321
+ // Check if interview is complete
322
+ if (context.interview.pending_questions.length === 0) {
323
+ context.interview.status = "completed";
324
+ context.interview.completed_at = new Date().toISOString();
325
+ await saveContext(context);
326
+
327
+ let result = `✅ **Answer saved!**\n\n`;
328
+ result += `**${question.question}**\n`;
329
+ result += `*Answer:* ${answer}\n\n`;
330
+ result += `🎉 **Interview completed!**\n\n`;
331
+ result += `All basic information has been captured. `;
332
+ result += `I now know you better and can provide better support in future sessions.\n\n`;
333
+ result += `💡 **Tip:** Use \`suggest_questions\` for advanced questions about your Salesforce data model.`;
334
+
335
+ return {
336
+ content: [{
337
+ type: "text",
338
+ text: result
339
+ }]
340
+ };
341
+ }
342
+
343
+ // Show next question
344
+ const nextQuestion = context.interview.pending_questions[0];
345
+ let result = `✅ **Answer saved!**\n\n`;
346
+ result += `**${question.question}**\n`;
347
+ result += `*Answer:* ${answer}\n\n`;
348
+ result += `**Progress:** ${context.interview.answered_questions.length}/${context.interview.answered_questions.length + context.interview.pending_questions.length} questions answered\n\n`;
349
+ result += `---\n\n`;
350
+ result += `**${nextQuestion.category.toUpperCase()} - Next question:**\n\n`;
351
+ result += `${nextQuestion.question}\n\n`;
352
+ result += `*Answer with:*\n`;
353
+ result += `\`\`\`json\n`;
354
+ result += `{\n`;
355
+ result += ` "action": "answer_question",\n`;
356
+ result += ` "question_id": "${nextQuestion.id}",\n`;
357
+ result += ` "answer": "Your answer here"\n`;
358
+ result += `}\n`;
359
+ result += `\`\`\``;
360
+
361
+ return {
362
+ content: [{
363
+ type: "text",
364
+ text: result
365
+ }]
366
+ };
367
+ }
368
+
369
+ async function showStoredContext(contextType) {
370
+ const context = await loadContext();
371
+
372
+ let result = `📋 **Your stored context**\n\n`;
373
+
374
+ // Get all sections (excluding metadata fields)
375
+ const metadataFields = ['created_at', 'updated_at', 'interview'];
376
+ const allSections = Object.keys(context).filter(key => !metadataFields.includes(key));
377
+ const hasPersonalInfo = context.personal?.name && context.personal?.email && context.personal?.role;
378
+ const hasBusinessInfo = context.business?.company_name && context.business?.industry && context.business?.business_focus;
379
+ const hasDataModelInfo = Object.keys(context.data_model || {}).length > 0;
380
+
381
+ // Status overview
382
+ result += `## 📊 Context Status\n`;
383
+ result += `- **Total Sections:** ${allSections.length}\n`;
384
+ result += `- **Personal Information:** ${hasPersonalInfo ? '✅ Complete' : '⚠️ Incomplete'}\n`;
385
+ result += `- **Business Information:** ${hasBusinessInfo ? '✅ Complete' : '⚠️ Incomplete'}\n`;
386
+ result += `- **Data Model Context:** ${hasDataModelInfo ? '✅ Available' : '❌ Not captured'}\n`;
387
+
388
+ const completionPercentage = Math.round(((hasPersonalInfo ? 1 : 0) + (hasBusinessInfo ? 1 : 0) + (hasDataModelInfo ? 1 : 0)) / 3 * 100);
389
+ result += `- **Core Completeness:** ${completionPercentage}%\n\n`;
390
+
391
+ // Show specific section or all sections
392
+ const sectionsToShow = contextType === "all" ? allSections :
393
+ allSections.includes(contextType) ? [contextType] : [];
394
+
395
+ if (sectionsToShow.length === 0 && contextType !== "all") {
396
+ result += `⚠️ **Section "${contextType}" not found**\n\n`;
397
+ result += `**Available sections:** ${allSections.join(', ')}\n\n`;
398
+ }
399
+
400
+ // Display each section
401
+ for (const sectionName of sectionsToShow) {
402
+ const sectionData = context[sectionName];
403
+ if (!sectionData || Object.keys(sectionData).length === 0) continue;
404
+
405
+ // Create section header with appropriate emoji
406
+ const emoji = getSectionEmoji(sectionName);
407
+ const title = sectionName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
408
+ result += `## ${emoji} ${title}\n`;
409
+
410
+ // Handle special formatting for known sections
411
+ if (sectionName === 'personal') {
412
+ const standardFields = ['name', 'email', 'role', 'salesforce_usage'];
413
+ for (const field of standardFields) {
414
+ if (sectionData[field]) {
415
+ const label = field === 'role' ? 'Position' :
416
+ field === 'salesforce_usage' ? 'Salesforce Usage' :
417
+ field.charAt(0).toUpperCase() + field.slice(1);
418
+ result += `- **${label}:** ${sectionData[field]}\n`;
419
+ }
420
+ }
421
+
422
+ // Display dynamic fields
423
+ const dynamicFields = Object.keys(sectionData).filter(key => !standardFields.includes(key));
424
+ for (const field of dynamicFields) {
425
+ const label = field.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
426
+ result += `- **${label}:** ${sectionData[field]}\n`;
427
+ }
428
+ } else if (sectionName === 'business') {
429
+ const standardFields = ['company_name', 'industry', 'business_focus', 'primary_processes'];
430
+ for (const field of standardFields) {
431
+ if (sectionData[field]) {
432
+ const label = field === 'company_name' ? 'Company' :
433
+ field === 'business_focus' ? 'Business Focus' :
434
+ field === 'primary_processes' ? 'Primary Processes' :
435
+ field.charAt(0).toUpperCase() + field.slice(1);
436
+ result += `- **${label}:** ${sectionData[field]}\n`;
437
+ }
438
+ }
439
+
440
+ // Display dynamic fields
441
+ const dynamicFields = Object.keys(sectionData).filter(key => !standardFields.includes(key));
442
+ for (const field of dynamicFields) {
443
+ const label = field.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
444
+ result += `- **${label}:** ${sectionData[field]}\n`;
445
+ }
446
+ } else {
447
+ // For all other sections (including data_model and custom sections), show all fields
448
+ for (const [key, value] of Object.entries(sectionData)) {
449
+ const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
450
+ const displayValue = typeof value === 'string' && value.length > 200 ?
451
+ value.substring(0, 200) + '...' : value;
452
+ result += `- **${formattedKey}:** ${displayValue}\n`;
453
+ }
454
+ }
455
+ result += `\n`;
456
+ }
457
+
458
+ // Interview status
459
+ if (context.interview) {
460
+ result += `## 🎤 Interview Status\n`;
461
+ result += `- **Status:** ${context.interview.status === 'completed' ? '✅ Completed' : '🔄 In Progress'}\n`;
462
+ if (context.interview.started_at) {
463
+ result += `- **Started:** ${new Date(context.interview.started_at).toLocaleString()}\n`;
464
+ }
465
+ if (context.interview.completed_at) {
466
+ result += `- **Completed:** ${new Date(context.interview.completed_at).toLocaleString()}\n`;
467
+ }
468
+ if (context.interview.pending_questions && context.interview.pending_questions.length > 0) {
469
+ result += `- **Pending Questions:** ${context.interview.pending_questions.length}\n`;
470
+ }
471
+ if (context.interview.answered_questions) {
472
+ result += `- **Answered Questions:** ${context.interview.answered_questions.length}\n`;
473
+ }
474
+ result += `\n`;
475
+ }
476
+
477
+ // Recommendations based on what's missing
478
+ const recommendations = [];
479
+ if (!hasPersonalInfo) {
480
+ recommendations.push("**Complete personal information** - For personalized communication");
481
+ }
482
+ if (!hasBusinessInfo) {
483
+ recommendations.push("**Add business information** - For context-specific solutions");
484
+ }
485
+ if (!hasDataModelInfo && hasPersonalInfo && hasBusinessInfo) {
486
+ recommendations.push("**Capture data model context** - For specific Salesforce support");
487
+ }
488
+
489
+ if (recommendations.length > 0) {
490
+ result += `## 💡 Recommendations\n`;
491
+ for (const rec of recommendations) {
492
+ result += `- ${rec}\n`;
493
+ }
494
+ result += `\n`;
495
+ }
496
+
497
+ result += `## 🛠️ Available Actions\n`;
498
+ if (!hasPersonalInfo || !hasBusinessInfo) {
499
+ result += `- **Start/continue interview:** \`action: "start_interview"\`\n`;
500
+ }
501
+ if (hasPersonalInfo && hasBusinessInfo) {
502
+ result += `- **Intelligent questions:** \`action: "suggest_questions"\`\n`;
503
+ }
504
+ // Show additional questions even after interview completion
505
+ if (context.interview?.status === "completed") {
506
+ result += `- **Add data model details:** \`question_id: "data_model_details"\`\n`;
507
+ result += `- **Custom objects purpose:** \`question_id: "custom_objects_purpose"\`\n`;
508
+ result += `- **Business processes:** \`question_id: "business_processes"\`\n`;
509
+ result += `- **Integration systems:** \`question_id: "integration_systems"\`\n`;
510
+ result += `- **Reporting needs:** \`question_id: "reporting_needs"\`\n`;
511
+ }
512
+ result += `- **Store dynamic learning:** \`action: "store_learning"\` (AI can store any key-value information in any section)\n`;
513
+ result += `- **Reset context:** \`action: "reset_context"\`\n`;
514
+
515
+ return {
516
+ content: [{
517
+ type: "text",
518
+ text: result
519
+ }]
520
+ };
521
+ }
522
+
523
+ // Helper function to get appropriate emoji for sections
524
+ function getSectionEmoji(sectionName) {
525
+ const emojiMap = {
526
+ 'personal': '👤',
527
+ 'business': '🏢',
528
+ 'data_model': '🗃️',
529
+ 'technical_preferences': '⚙️',
530
+ 'workflow_patterns': '🔄',
531
+ 'integration_systems': '🔗',
532
+ 'security_settings': '🔒',
533
+ 'reporting_needs': '📊',
534
+ 'user_preferences': '⚡',
535
+ 'automation_rules': '🤖',
536
+ 'custom_processes': '📋',
537
+ 'system_configuration': '🔧',
538
+ 'aha_moments': '💡',
539
+ 'insights': '🌟',
540
+ 'breakthrough_insights': '💡',
541
+ 'pain_points': '😤',
542
+ 'frustrations': '😤',
543
+ 'challenges': '⚠️',
544
+ 'success_patterns': '✅',
545
+ 'wins': '🏆',
546
+ 'discoveries': '🔍',
547
+ 'realizations': '💭',
548
+ 'key_learnings': '📝',
549
+ 'workflow_insights': '🔄',
550
+ 'technical_discoveries': '🔬',
551
+ 'process_improvements': '📈',
552
+ 'optimization_opportunities': '⚡'
553
+ };
554
+
555
+ return emojiMap[sectionName] || '📁';
556
+ }
557
+
558
+ async function suggestIntelligentQuestions(contextType) {
559
+ // Check if installation has been learned
560
+ const hasInstallation = await hasInstallationDocumentation();
561
+ if (!hasInstallation) {
562
+ return {
563
+ content: [{
564
+ type: "text",
565
+ text: `⚠️ **Salesforce installation not learned**\n\n` +
566
+ `To ask intelligent questions about your data model, the Salesforce installation must first be analyzed.\n\n` +
567
+ `First run \`salesforce_learn\`.`
568
+ }]
569
+ };
570
+ }
571
+
572
+ const documentation = await getInstallationDocumentation();
573
+ const context = await loadContext();
574
+
575
+ const questions = [];
576
+ let questionId = 1;
577
+
578
+ // Business process questions based on custom objects
579
+ const customObjects = Object.entries(documentation.objects)
580
+ .filter(([name, obj]) => !obj.error && obj.basic_info?.custom)
581
+ .slice(0, 10); // Limit to avoid overwhelming
582
+
583
+ if (customObjects.length > 0 && (contextType === "all" || contextType === "data_model")) {
584
+ questions.push({
585
+ id: `custom_objects_purpose_${questionId++}`,
586
+ category: "data_model",
587
+ question: `You have ${customObjects.length} Custom Objects in Salesforce. Can you explain the business purpose of these objects?\n\nCustom Objects: ${customObjects.map(([name, obj]) => `${obj.basic_info.label} (${name})`).join(', ')}`,
588
+ type: "textarea",
589
+ priority: "high"
590
+ });
591
+ }
592
+
593
+ // Relationship questions
594
+ const objectsWithRelationships = Object.entries(documentation.objects)
595
+ .filter(([name, obj]) => !obj.error && obj.relationships &&
596
+ ((obj.relationships.parent_relationships?.length || 0) + (obj.relationships.child_relationships?.length || 0)) > 2)
597
+ .slice(0, 5);
598
+
599
+ if (objectsWithRelationships.length > 0 && (contextType === "all" || contextType === "data_model")) {
600
+ questions.push({
601
+ id: `relationship_meaning_${questionId++}`,
602
+ category: "data_model",
603
+ question: `Some of your objects have many relationships with each other. Can you explain the business connections between these objects?\n\nObjects with many relationships: ${objectsWithRelationships.map(([name, obj]) => obj.basic_info.label).join(', ')}`,
604
+ type: "textarea",
605
+ priority: "medium"
606
+ });
607
+ }
608
+
609
+ // User role and process questions
610
+ if (!context.business?.primary_processes && (contextType === "all" || contextType === "business")) {
611
+ questions.push({
612
+ id: `primary_processes_${questionId++}`,
613
+ category: "business",
614
+ question: "What are the main business processes you map in Salesforce? (e.g., Sales, Customer Support, Marketing, etc.)",
615
+ type: "textarea",
616
+ priority: "high"
617
+ });
618
+ }
619
+
620
+ if (!context.personal?.salesforce_usage && (contextType === "all" || contextType === "personal")) {
621
+ questions.push({
622
+ id: `salesforce_usage_${questionId++}`,
623
+ category: "personal",
624
+ question: "How do you primarily use Salesforce in your daily work? What tasks do you perform most frequently?",
625
+ type: "textarea",
626
+ priority: "medium"
627
+ });
628
+ }
629
+
630
+ // Data model complexity questions
631
+ const totalCustomFields = documentation.summary?.custom_fields || 0;
632
+ if (totalCustomFields > 50 && !context.data_model?.customization_strategy && (contextType === "all" || contextType === "data_model")) {
633
+ questions.push({
634
+ id: `customization_strategy_${questionId++}`,
635
+ category: "data_model",
636
+ question: `You have ${totalCustomFields} Custom Fields. What was the strategy behind these customizations? What specific business requirements did they fulfill?`,
637
+ type: "textarea",
638
+ priority: "medium"
639
+ });
640
+ }
641
+
642
+ // Sort questions by priority
643
+ const priorityOrder = { "high": 3, "medium": 2, "low": 1 };
644
+ questions.sort((a, b) => priorityOrder[b.priority] - priorityOrder[a.priority]);
645
+
646
+ if (questions.length === 0) {
647
+ return {
648
+ content: [{
649
+ type: "text",
650
+ text: `✅ **No additional questions available**\n\n` +
651
+ `Based on your current context and data model, there are currently no additional intelligent questions.\n\n` +
652
+ `💡 **Tip:** If you want to share additional information, you can enter it directly or reset the interview.`
653
+ }]
654
+ };
655
+ }
656
+
657
+ // Show top 3 questions
658
+ const topQuestions = questions.slice(0, 3);
659
+
660
+ let result = `🧠 **Intelligent questions based on your Salesforce data model**\n\n`;
661
+ result += `Based on your Salesforce installation, I have identified ${questions.length} relevant questions:\n\n`;
662
+
663
+ for (let i = 0; i < topQuestions.length; i++) {
664
+ const q = topQuestions[i];
665
+ result += `## ${i + 1}. ${q.category.toUpperCase()} - ${q.priority.toUpperCase()} PRIORITY\n\n`;
666
+ result += `${q.question}\n\n`;
667
+ result += `*Answer with:*\n`;
668
+ result += `\`\`\`json\n`;
669
+ result += `{\n`;
670
+ result += ` "action": "answer_question",\n`;
671
+ result += ` "question_id": "${q.id}",\n`;
672
+ result += ` "answer": "Your answer here"\n`;
673
+ result += `}\n`;
674
+ result += `\`\`\`\n\n`;
675
+ result += `---\n\n`;
676
+ }
677
+
678
+ if (questions.length > 3) {
679
+ result += `*... and ${questions.length - 3} more questions available*\n\n`;
680
+ }
681
+
682
+ result += `💡 **Note:** These questions help me better understand your Salesforce usage and provide more targeted support.`;
683
+
684
+ return {
685
+ content: [{
686
+ type: "text",
687
+ text: result
688
+ }]
689
+ };
690
+ }
691
+
692
+ async function resetContext() {
693
+ try {
694
+ await fs.unlink(CONTEXT_FILE);
695
+ } catch (error) {
696
+ // File might not exist, which is fine
697
+ }
698
+
699
+ return {
700
+ content: [{
701
+ type: "text",
702
+ text: `🗑️ **Context reset**\n\n` +
703
+ `All stored information has been deleted.\n\n` +
704
+ `You can now start a new interview with \`action: "start_interview"\``
705
+ }]
706
+ };
707
+ }
708
+
709
+ async function loadContext() {
710
+ try {
711
+ const data = await fs.readFile(CONTEXT_FILE, 'utf8');
712
+ return JSON.parse(data);
713
+ } catch (error) {
714
+ // File doesn't exist or is invalid, return empty context
715
+ return {
716
+ personal: {},
717
+ business: {},
718
+ data_model: {},
719
+ created_at: new Date().toISOString()
720
+ };
721
+ }
722
+ }
723
+
724
+ async function saveContext(context) {
725
+ context.updated_at = new Date().toISOString();
726
+
727
+ // Ensure cache directory exists
728
+ await ensureCacheDirectory();
729
+ debug.log('🔍 saveContext - CONTEXT_FILE:', CONTEXT_FILE);
730
+
731
+ await fs.writeFile(CONTEXT_FILE, JSON.stringify(context, null, 2));
732
+ }
733
+
734
+ async function quickSetupContext({ full_name, email, role, company_name, industry, business_process_description }) {
735
+ // Validate required fields
736
+ const missingFields = [];
737
+ if (!full_name) missingFields.push('full_name');
738
+ if (!email) missingFields.push('email');
739
+ if (!role) missingFields.push('role');
740
+ if (!company_name) missingFields.push('company_name');
741
+ if (!industry) missingFields.push('industry');
742
+ if (!business_process_description) missingFields.push('business_process_description');
743
+
744
+ if (missingFields.length > 0) {
745
+ return {
746
+ content: [{
747
+ type: "text",
748
+ text: `❌ **Missing required fields for quick setup:**\n\n${missingFields.map(field => `- ${field}`).join('\n')}\n\n` +
749
+ `**Example usage:**\n` +
750
+ `\`\`\`json\n` +
751
+ `{\n` +
752
+ ` "action": "quick_setup",\n` +
753
+ ` "full_name": "Max Mustermann",\n` +
754
+ ` "email": "max@company.com",\n` +
755
+ ` "role": "Sales Manager",\n` +
756
+ ` "company_name": "Mustermann GmbH",\n` +
757
+ ` "industry": "Software & Technology",\n` +
758
+ ` "business_process_description": "Wir sind ein IT-Dienstleister der... [hier kompletten Geschäftsprozess erklären]"\n` +
759
+ `}\n` +
760
+ `\`\`\``
761
+ }]
762
+ };
763
+ }
764
+
765
+ // Create complete context
766
+ const context = {
767
+ personal: {
768
+ name: full_name,
769
+ email: email,
770
+ role: role
771
+ },
772
+ business: {
773
+ company_name: company_name,
774
+ industry: industry,
775
+ business_focus: business_process_description
776
+ },
777
+ data_model: {},
778
+ created_at: new Date().toISOString(),
779
+ interview: {
780
+ status: "completed",
781
+ started_at: new Date().toISOString(),
782
+ completed_at: new Date().toISOString(),
783
+ pending_questions: [],
784
+ answered_questions: [
785
+ {
786
+ id: "quick_setup_all",
787
+ category: "business",
788
+ question: "Complete business process setup",
789
+ type: "quick_setup",
790
+ answer: "All information provided via quick setup",
791
+ answered_at: new Date().toISOString()
792
+ }
793
+ ]
794
+ }
795
+ };
796
+
797
+ await saveContext(context);
798
+
799
+ return {
800
+ content: [{
801
+ type: "text",
802
+ text: `🎉 **Quick Setup Complete!**\n\n` +
803
+ `All your information has been saved successfully:\n\n` +
804
+ `## 👤 Personal Information\n` +
805
+ `- **Name:** ${full_name}\n` +
806
+ `- **Email:** ${email}\n` +
807
+ `- **Position:** ${role}\n\n` +
808
+ `## 🏢 Business Information\n` +
809
+ `- **Company:** ${company_name}\n` +
810
+ `- **Industry:** ${industry}\n` +
811
+ `- **Business Process:** ${business_process_description.substring(0, 150)}${business_process_description.length > 150 ? '...' : ''}\n\n` +
812
+ `## ✅ Status\n` +
813
+ `- **Personal Information:** ✅ Complete\n` +
814
+ `- **Business Information:** ✅ Complete\n` +
815
+ `- **Overall Completeness:** 67%\n\n` +
816
+ `💡 **Next steps:**\n` +
817
+ `- Use \`suggest_questions\` for data model questions\n` +
818
+ `- The AI now knows you and can provide personalized support!`
819
+ }]
820
+ };
821
+ }
822
+
823
+ // Export function to get context for other tools
824
+ export async function getUserContext() {
825
+ return await loadContext();
826
+ }
827
+
828
+ // Handle additional questions after basic interview is completed
829
+ async function handleAdditionalQuestion(questionId, answer, context) {
830
+ // Ensure context structure exists
831
+ if (!context.data_model) context.data_model = {};
832
+ if (!context.interview) context.interview = {};
833
+ if (!context.interview.answered_questions) context.interview.answered_questions = [];
834
+
835
+ // Define common additional questions
836
+ const additionalQuestions = {
837
+ 'data_model_details': {
838
+ id: 'data_model_details',
839
+ category: 'data_model',
840
+ question: 'Can you describe your Salesforce data model? What custom objects, fields, and relationships are important to your business?',
841
+ type: 'textarea',
842
+ context_field: 'model_description'
843
+ },
844
+ 'custom_objects_purpose': {
845
+ id: 'custom_objects_purpose',
846
+ category: 'data_model',
847
+ question: 'What are the main purposes of your custom objects in Salesforce?',
848
+ type: 'textarea',
849
+ context_field: 'custom_objects_purpose'
850
+ },
851
+ 'business_processes': {
852
+ id: 'business_processes',
853
+ category: 'data_model',
854
+ question: 'What are your main business processes that you track in Salesforce?',
855
+ type: 'textarea',
856
+ context_field: 'business_processes'
857
+ },
858
+ 'integration_systems': {
859
+ id: 'integration_systems',
860
+ category: 'data_model',
861
+ question: 'What external systems do you integrate with Salesforce?',
862
+ type: 'textarea',
863
+ context_field: 'integration_systems'
864
+ },
865
+ 'reporting_needs': {
866
+ id: 'reporting_needs',
867
+ category: 'data_model',
868
+ question: 'What kind of reports and dashboards are most important to your business?',
869
+ type: 'textarea',
870
+ context_field: 'reporting_needs'
871
+ }
872
+ };
873
+
874
+ // Check if this is a predefined additional question
875
+ const questionDef = additionalQuestions[questionId];
876
+
877
+ if (questionDef) {
878
+ // Store the answer in the data_model context
879
+ context.data_model[questionDef.context_field] = answer;
880
+
881
+ // Add to answered questions
882
+ context.interview.answered_questions.push({
883
+ ...questionDef,
884
+ answer: answer,
885
+ answered_at: new Date().toISOString()
886
+ });
887
+
888
+ await saveContext(context);
889
+
890
+ let result = `✅ **Additional information saved!**\n\n`;
891
+ result += `**${questionDef.question}**\n`;
892
+ result += `*Your answer:* ${answer}\n\n`;
893
+ result += `💡 **This information has been added to your data model context.**\n\n`;
894
+ result += `**Available additional questions:**\n`;
895
+
896
+ // Show other available additional questions
897
+ for (const [qId, qDef] of Object.entries(additionalQuestions)) {
898
+ if (qId !== questionId && !context.data_model[qDef.context_field]) {
899
+ result += `- **${qDef.category.replace('_', ' ')}:** Use \`question_id: "${qId}"\`\n`;
900
+ }
901
+ }
902
+
903
+ result += `\n💡 **Tip:** Use \`suggest_questions\` for intelligent questions based on your Salesforce installation.`;
904
+
905
+ return {
906
+ content: [{
907
+ type: "text",
908
+ text: result
909
+ }]
910
+ };
911
+ }
912
+
913
+ // Handle dynamic questions from suggest_questions or AI-generated questions
914
+ // Check if this looks like a question ID from suggest_questions or is a completely new key
915
+ if (questionId.includes('_') || !additionalQuestions[questionId]) {
916
+ // Determine the section and key
917
+ let targetSection = 'data_model';
918
+ let contextField = questionId;
919
+
920
+ // Parse section from question ID if available
921
+ if (questionId.includes('_')) {
922
+ const [category, ...rest] = questionId.split('_');
923
+ if (['personal', 'business', 'data'].includes(category)) {
924
+ targetSection = category === 'data' ? 'data_model' : category;
925
+ contextField = rest.join('_');
926
+ }
927
+ }
928
+
929
+ // For completely unknown question IDs, try to infer meaning
930
+ if (!contextField || contextField === questionId) {
931
+ // Use the full question ID as the field name, cleaned up
932
+ contextField = questionId.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
933
+ }
934
+
935
+ // Remove trailing numbers that might be from suggest_questions
936
+ contextField = contextField.replace(/\d+$/, '');
937
+
938
+ if (!context[targetSection]) context[targetSection] = {};
939
+ context[targetSection][contextField] = answer;
940
+
941
+ // Add to answered questions
942
+ context.interview.answered_questions.push({
943
+ id: questionId,
944
+ category: targetSection,
945
+ question: `Dynamic question: ${questionId}`,
946
+ type: 'dynamic',
947
+ answer: answer,
948
+ answered_at: new Date().toISOString(),
949
+ context_field: contextField
950
+ });
951
+
952
+ await saveContext(context);
953
+
954
+ return {
955
+ content: [{
956
+ type: "text",
957
+ text: `✅ **Dynamic answer saved!**\n\n` +
958
+ `**Question ID:** ${questionId}\n` +
959
+ `**Stored in:** ${targetSection}.${contextField}\n` +
960
+ `**Your answer:** ${answer}\n\n` +
961
+ `🧠 **This learning has been added to your context and will be remembered across sessions.**\n\n` +
962
+ `💡 Use \`show_context\` to see all stored information.`
963
+ }]
964
+ };
965
+ }
966
+
967
+ // If question ID is not recognized, provide helpful guidance
968
+ return {
969
+ content: [{
970
+ type: "text",
971
+ text: `❌ **Question ID not recognized:** ${questionId}\n\n` +
972
+ `**Available additional questions:**\n` +
973
+ Object.entries(additionalQuestions).map(([qId, qDef]) =>
974
+ `- **${qDef.category.replace('_', ' ')}:** \`${qId}\` - ${qDef.question.substring(0, 80)}...`
975
+ ).join('\n') + '\n\n' +
976
+ `💡 **Or use \`suggest_questions\` for intelligent questions based on your Salesforce data.**`
977
+ }]
978
+ };
979
+ }
980
+
981
+ async function storeDynamicLearning({ section, key, value, overwrite = false }) {
982
+ if (!section || !key || !value) {
983
+ return {
984
+ content: [{
985
+ type: "text",
986
+ text: `❌ **Missing required parameters for store_learning**\n\n` +
987
+ `Required: section, key, value\n` +
988
+ `- **section:** any descriptive name (e.g., 'personal', 'business', 'technical_preferences', 'workflow_patterns')\n` +
989
+ `- **key:** descriptive name (e.g., 'preferred_communication_style')\n` +
990
+ `- **value:** the information to store\n` +
991
+ `- **overwrite:** true/false (optional, default: false)`
992
+ }]
993
+ };
994
+ }
995
+
996
+ // Clean section name: convert to lowercase, replace spaces with underscores, remove special chars
997
+ const cleanSection = section.toLowerCase()
998
+ .replace(/[^a-z0-9_]/g, '_')
999
+ .replace(/_{2,}/g, '_')
1000
+ .replace(/^_+|_+$/g, ''); // Remove leading/trailing underscores
1001
+
1002
+ const context = await loadContext();
1003
+
1004
+ // Ensure section exists - create dynamically if needed
1005
+ if (!context[cleanSection]) {
1006
+ context[cleanSection] = {};
1007
+ }
1008
+
1009
+ // Check if key already exists and handle overwrite logic
1010
+ const keyExists = context[cleanSection].hasOwnProperty(key);
1011
+ if (keyExists && !overwrite) {
1012
+ return {
1013
+ content: [{
1014
+ type: "text",
1015
+ text: `⚠️ **Key already exists:** \`${key}\` in \`${cleanSection}\`\n\n` +
1016
+ `**Current value:** ${context[cleanSection][key]}\n` +
1017
+ `**New value:** ${value}\n\n` +
1018
+ `Use \`overwrite: true\` to replace the existing value, or choose a different key name.`
1019
+ }]
1020
+ };
1021
+ }
1022
+
1023
+ const previousValue = keyExists ? context[cleanSection][key] : null;
1024
+
1025
+ // Store the learning
1026
+ context[cleanSection][key] = value;
1027
+ context.updated_at = new Date().toISOString();
1028
+
1029
+ // Track the learning in the interview history for transparency
1030
+ if (!context.interview) {
1031
+ context.interview = {
1032
+ status: "completed",
1033
+ started_at: new Date().toISOString(),
1034
+ completed_at: new Date().toISOString(),
1035
+ pending_questions: [],
1036
+ answered_questions: []
1037
+ };
1038
+ }
1039
+
1040
+ if (!context.interview.answered_questions) {
1041
+ context.interview.answered_questions = [];
1042
+ }
1043
+
1044
+ context.interview.answered_questions.push({
1045
+ id: `dynamic_learning_${Date.now()}`,
1046
+ category: cleanSection,
1047
+ question: `Dynamic learning: ${key}`,
1048
+ type: 'dynamic_learning',
1049
+ answer: value,
1050
+ answered_at: new Date().toISOString(),
1051
+ learning_key: key,
1052
+ overwritten: keyExists,
1053
+ previous_value: previousValue
1054
+ });
1055
+
1056
+ await saveContext(context);
1057
+
1058
+ let result = `✅ **Learning stored successfully!**\n\n`;
1059
+ result += `**Section:** ${cleanSection}${cleanSection !== section ? ` (cleaned from "${section}")` : ''}\n`;
1060
+ result += `**Key:** ${key}\n`;
1061
+ result += `**Value:** ${value}\n\n`;
1062
+
1063
+ if (keyExists) {
1064
+ result += `📝 **Note:** This ${overwrite ? 'replaced' : 'overwrote'} the previous value:\n`;
1065
+ result += `*Previous:* ${previousValue}\n\n`;
1066
+ }
1067
+
1068
+ result += `🧠 **AI Context Enhanced:** This information will be remembered across all future sessions.\n\n`;
1069
+ result += `**Quick access:** Use \`show_context\` with \`context_type: "${cleanSection}"\` to see all ${cleanSection} information.`;
1070
+
1071
+ return {
1072
+ content: [{
1073
+ type: "text",
1074
+ text: result
1075
+ }]
1076
+ };
1077
+ }