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.
- package/.eslintrc.json +20 -0
- package/LICENSE +24 -0
- package/README.md +76 -0
- package/auth.js +148 -0
- package/bin/config-helper.js +51 -0
- package/bin/mcp-salesforce.js +12 -0
- package/bin/setup.js +266 -0
- package/bin/status.js +134 -0
- package/docs/README.md +52 -0
- package/docs/step1.png +0 -0
- package/docs/step2.png +0 -0
- package/docs/step3.png +0 -0
- package/docs/step4.png +0 -0
- package/examples/README.md +35 -0
- package/package.json +16 -0
- package/scripts/README.md +30 -0
- package/src/auth/file-storage.js +447 -0
- package/src/auth/oauth.js +417 -0
- package/src/auth/token-manager.js +207 -0
- package/src/backup/manager.js +949 -0
- package/src/index.js +168 -0
- package/src/salesforce/client.js +388 -0
- package/src/sf-client.js +79 -0
- package/src/tools/auth.js +190 -0
- package/src/tools/backup.js +486 -0
- package/src/tools/create.js +109 -0
- package/src/tools/delegate-hygiene.js +268 -0
- package/src/tools/delegate-validate.js +212 -0
- package/src/tools/delegate-verify.js +143 -0
- package/src/tools/delete.js +72 -0
- package/src/tools/describe.js +132 -0
- package/src/tools/installation-info.js +656 -0
- package/src/tools/learn-context.js +1077 -0
- package/src/tools/learn.js +351 -0
- package/src/tools/query.js +82 -0
- package/src/tools/repair-credentials.js +77 -0
- package/src/tools/setup.js +120 -0
- package/src/tools/time_machine.js +347 -0
- package/src/tools/update.js +138 -0
- package/src/tools.js +214 -0
- package/src/utils/cache.js +120 -0
- package/src/utils/debug.js +52 -0
- package/src/utils/logger.js +19 -0
- 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
|
+
}
|