fraim 2.0.179 → 2.0.182
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/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/api/admin/payments.js +33 -0
- package/dist/src/api/admin/sales-leads.js +21 -0
- package/dist/src/api/payment/create-session.js +338 -0
- package/dist/src/api/payment/dashboard-link.js +149 -0
- package/dist/src/api/payment/session-details.js +31 -0
- package/dist/src/api/payment/webhook.js +587 -0
- package/dist/src/api/personas/me.js +29 -0
- package/dist/src/api/pricing/get-config.js +25 -0
- package/dist/src/api/sales/contact.js +44 -0
- package/dist/src/cli/commands/add-ide.js +9 -2
- package/dist/src/cli/commands/setup.js +14 -44
- package/dist/src/cli/distribution/marketplace-bundles.js +5 -1
- package/dist/src/cli/setup/ide-detector.js +7 -2
- package/dist/src/core/config-loader.js +10 -8
- package/dist/src/core/types.js +2 -1
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/fraim/config-loader.js +11 -0
- package/dist/src/fraim/db-service.js +2387 -0
- package/dist/src/fraim/issues.js +152 -0
- package/dist/src/fraim/template-processor.js +184 -0
- package/dist/src/fraim/utils/request-utils.js +23 -0
- package/dist/src/middleware/auth.js +266 -0
- package/dist/src/middleware/cors-config.js +111 -0
- package/dist/src/middleware/logger.js +116 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/middleware/reject-query-api-key.js +45 -0
- package/dist/src/middleware/security-headers.js +41 -0
- package/dist/src/middleware/telemetry.js +134 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/analytics.js +1447 -0
- package/dist/src/routes/app-routes.js +32 -0
- package/dist/src/routes/auth-routes.js +505 -0
- package/dist/src/routes/oauth-routes.js +325 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/admin-service.js +229 -0
- package/dist/src/services/audit-log-persistence.js +60 -0
- package/dist/src/services/audit-log.js +69 -0
- package/dist/src/services/cookie-service.js +129 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/demo-seed-service.js +139 -0
- package/dist/src/services/email-code.js +23 -0
- package/dist/src/services/email-service-clean.js +782 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/installer-service.js +131 -0
- package/dist/src/services/mcp-oauth-store.js +33 -0
- package/dist/src/services/mcp-service.js +823 -0
- package/dist/src/services/oauth-helpers.js +127 -0
- package/dist/src/services/org-service.js +89 -0
- package/dist/src/services/persona-entitlement-service.js +288 -0
- package/dist/src/services/provider-service.js +215 -0
- package/dist/src/services/registry-service.js +628 -0
- package/dist/src/services/session-service.js +86 -0
- package/dist/src/services/trial-reminder-service.js +120 -0
- package/dist/src/services/usage-analytics-service.js +419 -0
- package/dist/src/services/workspace-identity.js +21 -0
- package/dist/src/types/analytics.js +2 -0
- package/dist/src/utils/payment-calculator.js +52 -0
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +33 -0
- package/extensions/office-word/taskpane.html +242 -0
- package/package.json +12 -2
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.McpService = exports.REQUIRED_QUALITY_FIELDS = exports.QUALITY_SCORE_JOBS = exports.QUALITY_PRODUCING_JOBS = exports.DEFAULT_LAUNCH_PHRASE_MAPPINGS = void 0;
|
|
37
|
+
const fs_1 = require("fs");
|
|
38
|
+
const crypto_1 = require("crypto");
|
|
39
|
+
const job_parser_1 = require("../core/utils/job-parser");
|
|
40
|
+
const provider_utils_1 = require("../core/utils/provider-utils");
|
|
41
|
+
const issues_1 = require("../fraim/issues");
|
|
42
|
+
const tool_schemas_1 = require("../mcp/tool-schemas");
|
|
43
|
+
const semver = __importStar(require("semver"));
|
|
44
|
+
const compat_1 = require("../config/compat");
|
|
45
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
46
|
+
const quality_evidence_1 = require("../core/quality-evidence");
|
|
47
|
+
const feature_flags_1 = require("../config/feature-flags");
|
|
48
|
+
const persona_entitlement_service_1 = require("./persona-entitlement-service");
|
|
49
|
+
const FRAIM_PROMPT_TEXT = `You are running in FRAIM mode. Follow this process:
|
|
50
|
+
|
|
51
|
+
0. **Preload deferred FRAIM tools when needed**: If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call ToolSearch once to load fraim_connect, list_fraim_jobs, get_fraim_job, get_fraim_file, seekMentoring. Do the preload as one batched discovery step, not one search per tool.
|
|
52
|
+
|
|
53
|
+
1. **If the user did not specify a FRAIM job or topic**: If local FRAIM job stubs are present in the workspace, inspect those first and match the request locally. Also inspect fraim/personalized-employee/jobs/ for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call list_fraim_jobs() to view the full catalog, including any proxy-discoverable personalized jobs.
|
|
54
|
+
|
|
55
|
+
2. **Find the match**: Match the user's request to a FRAIM job from the local stub catalog, fraim/personalized-employee/jobs/, or the full list_fraim_jobs() response. If no job matches, try a likely FRAIM skill with get_fraim_file({ path: "skills/<likely-category>/<argument>.md" }) and confirm the match with the user.
|
|
56
|
+
|
|
57
|
+
3. **Load the full content**:
|
|
58
|
+
- For jobs, call get_fraim_job({ job: "<matched-job-name>" }).
|
|
59
|
+
- For skills, use the content returned by get_fraim_file(...).
|
|
60
|
+
|
|
61
|
+
4. **Execute**:
|
|
62
|
+
- For jobs, follow the phased instructions and use seekMentoring when the job requires phase transitions.
|
|
63
|
+
- For skills, apply the skill steps directly to the user's current context.`;
|
|
64
|
+
exports.DEFAULT_LAUNCH_PHRASE_MAPPINGS = {
|
|
65
|
+
'Onboard this project': 'get_fraim_job({ job: "project-onboarding" })',
|
|
66
|
+
'sleep on learnings': 'get_fraim_job({ job: "sleep-on-learnings" })',
|
|
67
|
+
'Personalize my employee': 'get_fraim_job({ job: "evolve-employee" })',
|
|
68
|
+
'Evolve my employee': 'get_fraim_job({ job: "evolve-employee" })'
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Re-export quality evidence contract from src/core/quality-evidence.
|
|
72
|
+
* Kept here for backward compatibility with existing imports from
|
|
73
|
+
* tests/test-quality-scores.ts and other services.
|
|
74
|
+
*/
|
|
75
|
+
exports.QUALITY_PRODUCING_JOBS = quality_evidence_1.QUALITY_PRODUCING_JOBS;
|
|
76
|
+
exports.QUALITY_SCORE_JOBS = quality_evidence_1.QUALITY_SCORE_JOBS;
|
|
77
|
+
exports.REQUIRED_QUALITY_FIELDS = quality_evidence_1.REQUIRED_QUALITY_FIELDS;
|
|
78
|
+
class McpService {
|
|
79
|
+
constructor(registryService, sessionManager, aiMentor, dbService, analyticsServiceOrVersion, serverVersion) {
|
|
80
|
+
this.registryService = registryService;
|
|
81
|
+
this.sessionManager = sessionManager;
|
|
82
|
+
this.aiMentor = aiMentor;
|
|
83
|
+
this.dbService = dbService;
|
|
84
|
+
// Support both old 5-arg and new 6-arg signatures
|
|
85
|
+
if (typeof analyticsServiceOrVersion === 'string') {
|
|
86
|
+
this.serverVersion = analyticsServiceOrVersion;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.analyticsService = analyticsServiceOrVersion;
|
|
90
|
+
this.serverVersion = serverVersion;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getProjectRulesPath() {
|
|
94
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(process.env.FRAIM_WORKSPACE_ROOT || process.cwd(), 'personalized-employee', 'rules', 'project_rules.md');
|
|
95
|
+
}
|
|
96
|
+
buildDiscoveryResponse(matches) {
|
|
97
|
+
const lines = ['# Available FRAIM Jobs'];
|
|
98
|
+
lines.push('', 'Showing the full FRAIM job catalog.');
|
|
99
|
+
const categorized = {};
|
|
100
|
+
for (const match of matches) {
|
|
101
|
+
if (!categorized[match.category]) {
|
|
102
|
+
categorized[match.category] = [];
|
|
103
|
+
}
|
|
104
|
+
categorized[match.category].push(match);
|
|
105
|
+
}
|
|
106
|
+
for (const [category, jobList] of Object.entries(categorized)) {
|
|
107
|
+
lines.push('', `## ${category}`);
|
|
108
|
+
for (const job of jobList) {
|
|
109
|
+
lines.push(`- **${job.name}**: ${job.description}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
lines.push('', '## Next Step', 'Call `get_fraim_job({ job: "<name>" })` for the best match, then use `seekMentoring` for phase progression.');
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Handle incoming MCP requests
|
|
117
|
+
*/
|
|
118
|
+
async handleRequest(reqBody, context) {
|
|
119
|
+
const { method, params, id } = reqBody;
|
|
120
|
+
try {
|
|
121
|
+
let result;
|
|
122
|
+
if (method === 'initialize') {
|
|
123
|
+
result = this.handleInitialize();
|
|
124
|
+
}
|
|
125
|
+
else if (method === 'tools/list') {
|
|
126
|
+
result = this.handleListTools(context);
|
|
127
|
+
}
|
|
128
|
+
else if (method === 'resources/list') {
|
|
129
|
+
result = this.handleListResources();
|
|
130
|
+
}
|
|
131
|
+
else if (method === 'prompts/list') {
|
|
132
|
+
result = this.handleListPrompts();
|
|
133
|
+
}
|
|
134
|
+
else if (method === 'prompts/get') {
|
|
135
|
+
result = this.handleGetPrompt(params);
|
|
136
|
+
}
|
|
137
|
+
else if (method === 'tools/call') {
|
|
138
|
+
result = await this.handleToolCall(params, context);
|
|
139
|
+
}
|
|
140
|
+
else if (method === 'notifications/initialized') {
|
|
141
|
+
// Notifications don't need a response
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw new Error(`Method not found: ${method}`);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error(`[req:${context.requestId}] MCP service error:`, error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
handleInitialize() {
|
|
155
|
+
return {
|
|
156
|
+
protocolVersion: '2024-11-05',
|
|
157
|
+
serverInfo: {
|
|
158
|
+
name: 'fraim-mcp-server',
|
|
159
|
+
version: this.serverVersion
|
|
160
|
+
},
|
|
161
|
+
capabilities: {
|
|
162
|
+
tools: {},
|
|
163
|
+
resources: {},
|
|
164
|
+
prompts: {}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
handleListTools(context) {
|
|
169
|
+
const surface = !context
|
|
170
|
+
? 'local-proxy'
|
|
171
|
+
: context.localMcpVersion && context.localMcpVersion !== 'unknown'
|
|
172
|
+
? 'local-proxy'
|
|
173
|
+
: 'remote';
|
|
174
|
+
return {
|
|
175
|
+
tools: (0, tool_schemas_1.getToolDefinitions)(this.getAvailableJobs(), { surface })
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
handleListResources() {
|
|
179
|
+
return { resources: [] };
|
|
180
|
+
}
|
|
181
|
+
handleListPrompts() {
|
|
182
|
+
return {
|
|
183
|
+
prompts: [
|
|
184
|
+
{
|
|
185
|
+
name: 'fraim',
|
|
186
|
+
description: 'Activate a FRAIM job or skill. Scans the job catalog, matches your request, loads full phased instructions, and executes.',
|
|
187
|
+
arguments: [
|
|
188
|
+
{
|
|
189
|
+
name: 'task',
|
|
190
|
+
description: 'Job, skill, or task to run (e.g. "feature-specification", "sleep on learnings"). Omit to let FRAIM match from context.',
|
|
191
|
+
required: false
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
handleGetPrompt(params) {
|
|
199
|
+
const { name, arguments: promptArgs } = params ?? {};
|
|
200
|
+
if (name !== 'fraim') {
|
|
201
|
+
const err = new Error(`Prompt not found: ${name}`);
|
|
202
|
+
err.code = -32602;
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
const task = promptArgs?.task ? String(promptArgs.task) : '';
|
|
206
|
+
const taskSuffix = task ? `\n\nTask: ${task}` : '';
|
|
207
|
+
return {
|
|
208
|
+
description: 'Activate a FRAIM job or skill',
|
|
209
|
+
messages: [
|
|
210
|
+
{
|
|
211
|
+
role: 'user',
|
|
212
|
+
content: {
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: FRAIM_PROMPT_TEXT + taskSuffix
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async handleToolCall(params, context) {
|
|
221
|
+
let result = await this.handleToolCallInternal(params, context);
|
|
222
|
+
// Inject migration notice for old clients
|
|
223
|
+
if (context.localMcpVersion && context.localMcpVersion !== 'unknown') {
|
|
224
|
+
try {
|
|
225
|
+
// Check for breaking changes/minimum version
|
|
226
|
+
const isOutdated = semver.valid(context.localMcpVersion) &&
|
|
227
|
+
semver.lt(context.localMcpVersion, compat_1.MINIMUM_CLIENT_VERSION);
|
|
228
|
+
if (isOutdated && result && Array.isArray(result.content)) {
|
|
229
|
+
// Avoid duplicating the notice if somehow we recursively called
|
|
230
|
+
const hasNotice = result.content.some((c) => c.text && c.text.includes('migration_notice'));
|
|
231
|
+
if (!hasNotice) {
|
|
232
|
+
result.content.push({
|
|
233
|
+
type: 'text',
|
|
234
|
+
text: (0, compat_1.getBreakingChangesNotice)(context.localMcpVersion)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
// Ignore semver errors
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
async handleToolCallInternal(params, context) {
|
|
246
|
+
const { name: toolName, arguments: toolArgs } = params;
|
|
247
|
+
if (!toolName)
|
|
248
|
+
throw new Error('Tool name is required');
|
|
249
|
+
const args = toolArgs || {};
|
|
250
|
+
switch (toolName) {
|
|
251
|
+
case 'get_fraim_file':
|
|
252
|
+
if (!args.path)
|
|
253
|
+
throw new Error('Argument "path" is required for get_fraim_file');
|
|
254
|
+
return await this.handleGetFile(args.path, { raw: args.raw });
|
|
255
|
+
case 'get_fraim_job':
|
|
256
|
+
if (!args.job)
|
|
257
|
+
throw new Error('Argument "job" is required for get_fraim_job');
|
|
258
|
+
return await this.handleGetJob(args.job, context.apiKey, context.userId);
|
|
259
|
+
case 'seekMentoring':
|
|
260
|
+
if (!args.jobName || !args.issueNumber || !args.currentPhase || !args.status || !args.jobId) {
|
|
261
|
+
throw new Error('Arguments "jobName", "jobId", "issueNumber", "currentPhase", and "status" are required for seekMentoring');
|
|
262
|
+
}
|
|
263
|
+
return await this.handleSeekMentoring(args, context.userId);
|
|
264
|
+
case 'list_fraim_jobs':
|
|
265
|
+
return await this.handleListJobs();
|
|
266
|
+
case 'file_fraim_github_issue':
|
|
267
|
+
case 'file_issue': // Backward compatibility alias
|
|
268
|
+
if (!args.title || !args.body)
|
|
269
|
+
throw new Error('Arguments "title" and "body" are required for file_fraim_github_issue');
|
|
270
|
+
const result = await (0, issues_1.fileFraimIssue)({ ...args, reporterEmail: context.userId });
|
|
271
|
+
return {
|
|
272
|
+
content: [{
|
|
273
|
+
type: 'text',
|
|
274
|
+
text: JSON.stringify(result, null, 2)
|
|
275
|
+
}]
|
|
276
|
+
};
|
|
277
|
+
case 'list_my_fraim_github_issues':
|
|
278
|
+
const listResult = await (0, issues_1.listFraimIssuesByReporter)({
|
|
279
|
+
reporterEmail: context.userId,
|
|
280
|
+
limit: args.limit
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
content: [{
|
|
284
|
+
type: 'text',
|
|
285
|
+
text: JSON.stringify(listResult, null, 2)
|
|
286
|
+
}]
|
|
287
|
+
};
|
|
288
|
+
case 'fraim_connect':
|
|
289
|
+
return await this.handleFraimConnect(args, context.apiKey, context.userId, context);
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`Unknown tool: ${toolName} `);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async handleGetFile(path, options = {}) {
|
|
295
|
+
const metadata = this.registryService.findFileByPath(path);
|
|
296
|
+
const isRaw = options.raw === true;
|
|
297
|
+
if (!metadata) {
|
|
298
|
+
throw new Error(`File not found: ${path}. Check for typos in the file name or file path.`);
|
|
299
|
+
}
|
|
300
|
+
if (!isRaw && metadata.type === 'job') {
|
|
301
|
+
throw new Error(`Jobs cannot be retrieved via get_fraim_file. Use get_fraim_job({ job: "${metadata.name.replace('.md', '')}" }) instead.`);
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
let content = (0, fs_1.readFileSync)(metadata.fullPath, 'utf8');
|
|
305
|
+
if (isRaw) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{
|
|
308
|
+
type: 'text',
|
|
309
|
+
text: content
|
|
310
|
+
}]
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const hasProxyTemplateVars = /\{\{proxy\.[^}]+\}\}/.test(content);
|
|
314
|
+
const hasAgentTemplateVars = /\{\{agent\.[^}]+\}\}/.test(content);
|
|
315
|
+
let header = `# ${metadata.name} \n\n`;
|
|
316
|
+
header += `** Path:** ${path} \n`;
|
|
317
|
+
header += `** Type:** ${metadata.type} \n`;
|
|
318
|
+
if (metadata.category) {
|
|
319
|
+
header += `** Category:** ${metadata.category} \n`;
|
|
320
|
+
}
|
|
321
|
+
if (hasProxyTemplateVars) {
|
|
322
|
+
header += `\n## Proxy Variables\n\n`;
|
|
323
|
+
header += `This file contains placeholders like \`{{proxy.config.project.name}}\`.\n\n`;
|
|
324
|
+
header += `These are resolved deterministically by the local FRAIM proxy and should not be manually replaced by the agent.\n\n`;
|
|
325
|
+
}
|
|
326
|
+
if (hasAgentTemplateVars) {
|
|
327
|
+
header += `\n## Agent Variables Detected\n\n`;
|
|
328
|
+
header += `This file contains placeholders like \`{{agent.issue_number}}\`.\n\n`;
|
|
329
|
+
header += `These must be filled by the agent/runtime from the active task context.\n\n`;
|
|
330
|
+
}
|
|
331
|
+
if (metadata.type === 'script') {
|
|
332
|
+
header += `\n## 🛡️ Ephemeral Execution Mandatory (IP Protection)\n\n`;
|
|
333
|
+
header += `This script MUST be executed ephemerally (see rules/ephemeral-execution.md).\n\n`;
|
|
334
|
+
}
|
|
335
|
+
header += `---\n\n`;
|
|
336
|
+
return {
|
|
337
|
+
content: [{
|
|
338
|
+
type: 'text',
|
|
339
|
+
text: header + content
|
|
340
|
+
}]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async handleGetJob(jobName, apiKey, userId) {
|
|
348
|
+
let effectiveUserId = userId;
|
|
349
|
+
if (!effectiveUserId && apiKey) {
|
|
350
|
+
try {
|
|
351
|
+
const apiKeyData = await this.dbService.getApiKeyByKey(apiKey);
|
|
352
|
+
effectiveUserId = apiKeyData?.userId;
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
console.error(`⌠McpService: API key lookup error:`, e);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const personaLock = await this.maybeBuildPersonaLockResponse(jobName, effectiveUserId, apiKey, false);
|
|
359
|
+
if (personaLock) {
|
|
360
|
+
return personaLock;
|
|
361
|
+
}
|
|
362
|
+
const result = await this.aiMentor.getJobOverview(jobName);
|
|
363
|
+
if (!result) {
|
|
364
|
+
return {
|
|
365
|
+
content: [{
|
|
366
|
+
type: 'text',
|
|
367
|
+
text: `Job "${jobName}" not found.`
|
|
368
|
+
}]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Generate jobId for tracking
|
|
372
|
+
const jobId = (0, crypto_1.randomUUID)();
|
|
373
|
+
let sessionId = 'unknown';
|
|
374
|
+
// Get session info for tracking
|
|
375
|
+
if (apiKey) {
|
|
376
|
+
try {
|
|
377
|
+
const session = await this.dbService.getActiveSessionByApiKey(apiKey);
|
|
378
|
+
if (session) {
|
|
379
|
+
sessionId = session.sessionId;
|
|
380
|
+
userId = session.userId;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.error(`❌ McpService: Session lookup error:`, e);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Create job execution record using usage events
|
|
388
|
+
if (userId && this.analyticsService) {
|
|
389
|
+
try {
|
|
390
|
+
await this.analyticsService.logJobStart(jobId, jobName, userId, sessionId);
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
console.error(`❌ McpService: Failed to log job start:`, e);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
let response = result.overview;
|
|
397
|
+
// Inject phase-authority for phased jobs
|
|
398
|
+
if (!result.isSimple) {
|
|
399
|
+
response = `${this.aiMentor.getCompactPhaseAuthority()}\n\n${response}`;
|
|
400
|
+
}
|
|
401
|
+
if (apiKey) {
|
|
402
|
+
try {
|
|
403
|
+
const session = await this.dbService.getActiveSessionByApiKey(apiKey);
|
|
404
|
+
if (session?.repo) {
|
|
405
|
+
const provider = session.repo.provider || (0, provider_utils_1.detectProvider)(session.repo.url);
|
|
406
|
+
const lines = response.split('\n');
|
|
407
|
+
if (lines.length > 0) {
|
|
408
|
+
lines.splice(1, 0, `\n**Platform:** ${provider.toUpperCase()}\n`);
|
|
409
|
+
response = lines.join('\n');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Learning context is injected by the local proxy (learning-context-builder.ts),
|
|
413
|
+
// not by the server, because learning files live in the local workspace.
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
console.error(`❌ McpService: Session lookup error:`, e);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!result.isSimple) {
|
|
420
|
+
response += `\n\n---\n\n**Job ID:** \`${jobId}\`\n\n**This job has phases.** Use \`seekMentoring\` with the jobId above to get phase-specific instructions.`;
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
content: [{
|
|
424
|
+
type: 'text',
|
|
425
|
+
text: response
|
|
426
|
+
}]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async handleListJobs() {
|
|
430
|
+
const jobs = Array.from(this.registryService.getFileIndex().values()).filter((f) => f.type === 'job' && !f.isStub);
|
|
431
|
+
const uniqueJobs = new Map();
|
|
432
|
+
for (const job of jobs) {
|
|
433
|
+
const jobName = job.name.replace('.md', '');
|
|
434
|
+
if (!uniqueJobs.has(jobName)) {
|
|
435
|
+
uniqueJobs.set(jobName, job);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const matches = [];
|
|
439
|
+
for (const job of uniqueJobs.values()) {
|
|
440
|
+
const category = job.category || 'Other';
|
|
441
|
+
const jobName = job.name.replace('.md', '');
|
|
442
|
+
const description = this.getJobDescription(jobName, job.fullPath);
|
|
443
|
+
matches.push({
|
|
444
|
+
name: jobName,
|
|
445
|
+
description,
|
|
446
|
+
category
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
matches.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
450
|
+
const response = this.buildDiscoveryResponse(matches);
|
|
451
|
+
return {
|
|
452
|
+
content: [{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: response
|
|
455
|
+
}]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async handleListFiles(type, category) {
|
|
459
|
+
let files = Array.from(this.registryService.getFileIndex().values());
|
|
460
|
+
if (type !== 'all') {
|
|
461
|
+
files = files.filter(f => f.type === type);
|
|
462
|
+
}
|
|
463
|
+
if (category) {
|
|
464
|
+
files = files.filter(f => f.category === category);
|
|
465
|
+
}
|
|
466
|
+
files.sort((a, b) => {
|
|
467
|
+
if (a.type !== b.type)
|
|
468
|
+
return a.type.localeCompare(b.type);
|
|
469
|
+
return a.path.localeCompare(b.path);
|
|
470
|
+
});
|
|
471
|
+
const filesList = files.map(f => {
|
|
472
|
+
return `- **${f.type}**${f.category ? ` (${f.category})` : ''}: \`${f.path}\``;
|
|
473
|
+
}).join('\n');
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: 'text',
|
|
477
|
+
text: `# FRAIM Files\n\n**Total:** ${files.length} file(s)\n\n${filesList}`
|
|
478
|
+
}]
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async handleFraimConnect(args, apiKey, userId, context) {
|
|
482
|
+
if (!apiKey)
|
|
483
|
+
throw new Error('No API Key found');
|
|
484
|
+
if (!args.agent?.name || !args.agent?.model) {
|
|
485
|
+
throw new Error('Agent information is required (name and model)');
|
|
486
|
+
}
|
|
487
|
+
const isLocalProxyCall = !!context?.localMcpVersion && context.localMcpVersion !== 'unknown';
|
|
488
|
+
const hasCompleteMachine = !!args.machine?.hostname &&
|
|
489
|
+
!!args.machine?.platform &&
|
|
490
|
+
typeof args.machine?.memory === 'number' &&
|
|
491
|
+
typeof args.machine?.cpus === 'number';
|
|
492
|
+
const hasRepoUrl = !!args.repo?.url;
|
|
493
|
+
if (isLocalProxyCall && (!args.machine?.hostname || !args.machine?.platform)) {
|
|
494
|
+
throw new Error('Machine hostname and platform are required');
|
|
495
|
+
}
|
|
496
|
+
if (isLocalProxyCall && (typeof args.machine?.memory !== 'number' || typeof args.machine?.cpus !== 'number')) {
|
|
497
|
+
throw new Error('Machine memory and cpus are required');
|
|
498
|
+
}
|
|
499
|
+
if (isLocalProxyCall && !args.repo?.url) {
|
|
500
|
+
throw new Error('Repository URL is required');
|
|
501
|
+
}
|
|
502
|
+
if (args.issueTracking) {
|
|
503
|
+
this.validateIssueTrackingConfig(args.issueTracking);
|
|
504
|
+
}
|
|
505
|
+
const finalMachineInfo = hasCompleteMachine
|
|
506
|
+
? args.machine
|
|
507
|
+
: {
|
|
508
|
+
...(args.machine || {}),
|
|
509
|
+
hostname: args.machine?.hostname || 'hosted-marketplace-client',
|
|
510
|
+
platform: args.machine?.platform || 'web',
|
|
511
|
+
memory: typeof args.machine?.memory === 'number' ? args.machine.memory : 0,
|
|
512
|
+
cpus: typeof args.machine?.cpus === 'number' ? args.machine.cpus : 0,
|
|
513
|
+
context: 'hosted-marketplace'
|
|
514
|
+
};
|
|
515
|
+
const finalRepoInput = hasRepoUrl
|
|
516
|
+
? args.repo
|
|
517
|
+
: {
|
|
518
|
+
url: 'https://github.com/mathursrus/FRAIM',
|
|
519
|
+
owner: 'mathursrus',
|
|
520
|
+
name: 'FRAIM',
|
|
521
|
+
branch: 'master',
|
|
522
|
+
context: 'hosted-marketplace'
|
|
523
|
+
};
|
|
524
|
+
const provider = (0, provider_utils_1.detectProvider)(finalRepoInput.url);
|
|
525
|
+
const finalRepoInfo = { ...finalRepoInput, provider };
|
|
526
|
+
const finalIssueTracking = args.issueTracking ? this.normalizeIssueTrackingConfig(args.issueTracking) : undefined;
|
|
527
|
+
const sessionId = (0, crypto_1.randomUUID)();
|
|
528
|
+
const finalUserId = userId || 'unknown';
|
|
529
|
+
const session = {
|
|
530
|
+
sessionId,
|
|
531
|
+
userId: finalUserId,
|
|
532
|
+
agent: args.agent,
|
|
533
|
+
machine: finalMachineInfo,
|
|
534
|
+
repo: finalRepoInfo,
|
|
535
|
+
...(finalIssueTracking ? { issueTracking: finalIssueTracking } : {}),
|
|
536
|
+
startTime: new Date(),
|
|
537
|
+
lastActive: new Date()
|
|
538
|
+
};
|
|
539
|
+
const keyData = await this.dbService.verifyApiKey(apiKey);
|
|
540
|
+
if (keyData)
|
|
541
|
+
session.userId = keyData.userId;
|
|
542
|
+
await this.dbService.createSession(session);
|
|
543
|
+
this.sessionManager.registerSession(apiKey, sessionId);
|
|
544
|
+
const projectRulesPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/rules/project_rules.md');
|
|
545
|
+
const userEmail = session.userId !== 'unknown' ? session.userId : null;
|
|
546
|
+
const lines = [
|
|
547
|
+
`FRAIM session active. Session ID: ${sessionId}`,
|
|
548
|
+
'',
|
|
549
|
+
'## Start Here',
|
|
550
|
+
'- If local FRAIM job stubs are present under `fraim/ai-employee/jobs/` or `fraim/ai-manager/jobs/`, inspect those first to choose the best job.',
|
|
551
|
+
'- Also inspect `fraim/personalized-employee/jobs/` for local job overrides or repo-specific jobs.',
|
|
552
|
+
'- If the user already named a job, call `get_fraim_job({ job: "<job-name>" })`.',
|
|
553
|
+
'- If local stubs are missing or you cannot inspect workspace files, call `list_fraim_jobs()` to view the full catalog, then pick the best match yourself.',
|
|
554
|
+
'- Use `get_fraim_file(...)` only for the specific skills or rules needed for the current task.',
|
|
555
|
+
'- Once a phased job starts, use `seekMentoring` for phase-specific instructions and transitions.'
|
|
556
|
+
];
|
|
557
|
+
if ((0, fs_1.existsSync)(this.getProjectRulesPath())) {
|
|
558
|
+
lines.push('', '## Required Project Rules', `Read \`${projectRulesPath}\` before doing work in this repo.`);
|
|
559
|
+
}
|
|
560
|
+
if (userEmail) {
|
|
561
|
+
lines.push('', '## Your Identity', '', `Your identity for this session: **${userEmail}**`);
|
|
562
|
+
}
|
|
563
|
+
// Learning context is injected by the local proxy (learning-context-builder.ts)
|
|
564
|
+
// after this response is received, because learning files live in the local workspace.
|
|
565
|
+
return {
|
|
566
|
+
content: [{
|
|
567
|
+
type: 'text',
|
|
568
|
+
text: lines.join('\n')
|
|
569
|
+
}],
|
|
570
|
+
sessionId: sessionId
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
validateIssueTrackingConfig(issueTracking) {
|
|
574
|
+
if (!issueTracking.provider) {
|
|
575
|
+
throw new Error('issueTracking.provider is required when issueTracking is provided');
|
|
576
|
+
}
|
|
577
|
+
if (issueTracking.provider === 'github') {
|
|
578
|
+
if (!issueTracking.owner || !issueTracking.name) {
|
|
579
|
+
throw new Error('GitHub issueTracking requires owner and name');
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (issueTracking.provider === 'ado') {
|
|
584
|
+
if (!issueTracking.organization || !issueTracking.project || !issueTracking.name) {
|
|
585
|
+
throw new Error('ADO issueTracking requires organization, project, and name');
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (issueTracking.provider === 'jira') {
|
|
590
|
+
if (!issueTracking.baseUrl || !issueTracking.projectKey) {
|
|
591
|
+
throw new Error('Jira issueTracking requires baseUrl and projectKey');
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (issueTracking.provider === 'linear') {
|
|
596
|
+
if (!issueTracking.teamId) {
|
|
597
|
+
throw new Error('Linear issueTracking requires teamId');
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (issueTracking.provider === 'gitlab') {
|
|
602
|
+
const hasProjectPath = typeof issueTracking.projectPath === 'string' && issueTracking.projectPath.length > 0;
|
|
603
|
+
const hasNamespaceAndName = typeof issueTracking.namespace === 'string' && issueTracking.namespace.length > 0 && typeof issueTracking.name === 'string' && issueTracking.name.length > 0;
|
|
604
|
+
if (!hasProjectPath && !hasNamespaceAndName) {
|
|
605
|
+
throw new Error('GitLab issueTracking requires projectPath or namespace + name');
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
throw new Error(`Unsupported issueTracking provider: ${issueTracking.provider}`);
|
|
610
|
+
}
|
|
611
|
+
normalizeIssueTrackingConfig(issueTracking) {
|
|
612
|
+
if (!issueTracking || issueTracking.provider !== 'gitlab') {
|
|
613
|
+
return { ...issueTracking };
|
|
614
|
+
}
|
|
615
|
+
if (issueTracking.projectPath || !issueTracking.namespace || !issueTracking.name) {
|
|
616
|
+
return { ...issueTracking };
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
...issueTracking,
|
|
620
|
+
projectPath: `${issueTracking.namespace}/${issueTracking.name}`
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Validate `evidence.quality` against REQUIRED_QUALITY_FIELDS.
|
|
625
|
+
* Thin wrapper around the pure core helper, preserved as a static
|
|
626
|
+
* method for existing test-quality-scores.ts imports.
|
|
627
|
+
*/
|
|
628
|
+
static validateQualityEvidence(quality, jobName) {
|
|
629
|
+
return (0, quality_evidence_1.validateQualityEvidence)(quality, jobName);
|
|
630
|
+
}
|
|
631
|
+
buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
632
|
+
return (0, quality_evidence_1.buildQualityRejectionMessage)(jobName, currentPhase, errors);
|
|
633
|
+
}
|
|
634
|
+
async maybeBuildPersonaLockResponse(jobName, userId, apiKey, consumeJobCredit = false) {
|
|
635
|
+
if (!(0, feature_flags_1.isPersonaEntitlementsEnabled)() || !userId) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
const decision = await (0, persona_entitlement_service_1.evaluatePersonaAccess)(this.dbService, userId, jobName, apiKey, `/fraim-brain?job=${encodeURIComponent(jobName)}`);
|
|
639
|
+
if (decision.allowed) {
|
|
640
|
+
if (consumeJobCredit && decision.entitlement?.hireMode === 'job' && decision.personaKey) {
|
|
641
|
+
const consumed = await this.dbService.consumePersonaJobCredit(decision.workspaceId, decision.personaKey);
|
|
642
|
+
if (!consumed) {
|
|
643
|
+
const refreshedDecision = await (0, persona_entitlement_service_1.evaluatePersonaAccess)(this.dbService, userId, jobName, apiKey, `/fraim-brain?job=${encodeURIComponent(jobName)}`);
|
|
644
|
+
if (!refreshedDecision.lock) {
|
|
645
|
+
return {
|
|
646
|
+
content: [{
|
|
647
|
+
type: 'text',
|
|
648
|
+
text: 'This persona job credit is no longer available. Hire the persona again to continue.'
|
|
649
|
+
}]
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
content: [{
|
|
654
|
+
type: 'text',
|
|
655
|
+
text: `${refreshedDecision.lock.message}\n\n${refreshedDecision.lock.ctaLabel}: ${refreshedDecision.lock.hireUrl}`
|
|
656
|
+
}],
|
|
657
|
+
personaLock: {
|
|
658
|
+
...refreshedDecision.lock,
|
|
659
|
+
jobName,
|
|
660
|
+
workspaceId: refreshedDecision.workspaceId,
|
|
661
|
+
ownedPersonaKeys: refreshedDecision.ownedPersonaKeys || []
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
if (!decision.lock) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
content: [{
|
|
673
|
+
type: 'text',
|
|
674
|
+
text: `${decision.lock.message}\n\n${decision.lock.ctaLabel}: ${decision.lock.hireUrl}`
|
|
675
|
+
}],
|
|
676
|
+
personaLock: {
|
|
677
|
+
...decision.lock,
|
|
678
|
+
jobName,
|
|
679
|
+
workspaceId: decision.workspaceId,
|
|
680
|
+
ownedPersonaKeys: decision.ownedPersonaKeys || []
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
async handleSeekMentoring(args, userId) {
|
|
685
|
+
const personaLock = await this.maybeBuildPersonaLockResponse(args.jobName, userId, undefined, args.status === 'starting');
|
|
686
|
+
if (personaLock) {
|
|
687
|
+
return personaLock;
|
|
688
|
+
}
|
|
689
|
+
// Validate jobId exists and belongs to the user
|
|
690
|
+
if (userId && this.analyticsService) {
|
|
691
|
+
const isValidJob = await this.analyticsService.validateJobId(args.jobId, userId);
|
|
692
|
+
if (!isValidJob) {
|
|
693
|
+
console.warn(`[seekMentoring] Unrecognized jobId: ${args.jobId} for user ${userId} — proceeding without tracking`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const result = await this.aiMentor.handleMentoringRequest({
|
|
697
|
+
jobName: args.jobName,
|
|
698
|
+
issueNumber: args.issueNumber,
|
|
699
|
+
currentPhase: args.currentPhase,
|
|
700
|
+
status: args.status,
|
|
701
|
+
evidence: args.evidence,
|
|
702
|
+
findings: args.findings
|
|
703
|
+
});
|
|
704
|
+
// Quality enforcement (Issue #251): quality-producing jobs MUST emit
|
|
705
|
+
// a valid `evidence.quality` object on their final completion call.
|
|
706
|
+
//
|
|
707
|
+
// NOTE: In normal operation the local MCP proxy short-circuits
|
|
708
|
+
// seekMentoring (stdio-server.ts) and enforces the same contract
|
|
709
|
+
// there, so this server-side block is a FALL-THROUGH SAFETY NET.
|
|
710
|
+
// It only runs when the proxy's local AIMentor throws and the
|
|
711
|
+
// proxy falls through to proxying the call to the remote. The
|
|
712
|
+
// primary enforcement + quality-score emission path lives in
|
|
713
|
+
// stdio-server.ts around the seekMentoring short-circuit and the
|
|
714
|
+
// POST /api/analytics/quality-score endpoint in src/routes/analytics.ts.
|
|
715
|
+
const isQualityJob = exports.QUALITY_PRODUCING_JOBS.includes(args.jobName);
|
|
716
|
+
const isFinalCompletion = args.status === 'complete' &&
|
|
717
|
+
(result.nextPhase === null || result.nextPhase === undefined);
|
|
718
|
+
if (isQualityJob && isFinalCompletion) {
|
|
719
|
+
const qualityErrors = [
|
|
720
|
+
...(McpService.validateQualityEvidence(args.evidence?.quality, args.jobName) || []),
|
|
721
|
+
...(typeof args.evidence?.artifactPath === 'string' && args.evidence.artifactPath.trim()
|
|
722
|
+
? []
|
|
723
|
+
: ['evidence.artifactPath is required'])
|
|
724
|
+
];
|
|
725
|
+
if (qualityErrors.length > 0) {
|
|
726
|
+
const rejectionMessage = this.buildQualityRejectionMessage(args.jobName, args.currentPhase, qualityErrors);
|
|
727
|
+
if (userId && this.analyticsService) {
|
|
728
|
+
try {
|
|
729
|
+
const sessionId = args.sessionId || 'unknown';
|
|
730
|
+
await this.analyticsService.logMentoringCall(args.jobId, args.jobName, userId, sessionId, args.currentPhase, 'incomplete', args.currentPhase, false);
|
|
731
|
+
}
|
|
732
|
+
catch (e) {
|
|
733
|
+
console.error(`❌ McpService: Failed to log quality rejection:`, e);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
content: [{ type: 'text', text: rejectionMessage }]
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Update job execution tracking using usage events
|
|
742
|
+
if (userId && this.analyticsService) {
|
|
743
|
+
try {
|
|
744
|
+
const sessionId = args.sessionId || 'unknown';
|
|
745
|
+
await this.analyticsService.logMentoringCall(args.jobId, args.jobName, userId, sessionId, args.currentPhase, args.status, result.nextPhase, result.message ? true : false);
|
|
746
|
+
// Log explicit completion event when job finishes
|
|
747
|
+
if (result.nextPhase === null || result.nextPhase === undefined) {
|
|
748
|
+
await this.analyticsService.logJobComplete(args.jobId, args.jobName, userId, sessionId, args.currentPhase);
|
|
749
|
+
// Emit quality score for jobs that produce quality assessments (Issue #251).
|
|
750
|
+
// Validation already happened above for quality-producing jobs, so this
|
|
751
|
+
// call is guaranteed to receive `evidence.artifactPath` and a well-formed `evidence.quality` object.
|
|
752
|
+
if (isQualityJob) {
|
|
753
|
+
await this.analyticsService.logQualityScore(userId, args.jobName, args.jobId, sessionId, args.evidence.quality, args.evidence?.artifactPath, args.evidence?.reviewContext?.repoIdentifier, args.evidence?.reviewContext);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (e) {
|
|
758
|
+
console.error(`❌ McpService: Failed to log mentoring/completion:`, e);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
content: [{
|
|
763
|
+
type: 'text',
|
|
764
|
+
text: result.message
|
|
765
|
+
}]
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
getJobDescription(jobName, fullPath) {
|
|
769
|
+
if (fullPath && (0, fs_1.existsSync)(fullPath)) {
|
|
770
|
+
try {
|
|
771
|
+
const dynamicDesc = job_parser_1.JobParser.extractDescription(fullPath);
|
|
772
|
+
if (dynamicDesc && dynamicDesc.length > 5)
|
|
773
|
+
return dynamicDesc;
|
|
774
|
+
}
|
|
775
|
+
catch (e) { }
|
|
776
|
+
}
|
|
777
|
+
return 'FRAIM Job';
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Raw API handler — mirrors MCP tools but returns content WITHOUT include resolution or token substitution.
|
|
781
|
+
* Used exclusively by the local proxy to fetch content it can then resolve locally (personalized-employee first).
|
|
782
|
+
*/
|
|
783
|
+
async handleToolRaw(tool, args) {
|
|
784
|
+
switch (tool) {
|
|
785
|
+
case 'get_fraim_file': {
|
|
786
|
+
const path = args.path;
|
|
787
|
+
if (!path)
|
|
788
|
+
throw new Error('path is required');
|
|
789
|
+
// raw=true: skips include resolution and header injection
|
|
790
|
+
return this.handleGetFile(path, { raw: true });
|
|
791
|
+
}
|
|
792
|
+
case 'get_fraim_job': {
|
|
793
|
+
const jobName = args.job;
|
|
794
|
+
if (!jobName)
|
|
795
|
+
throw new Error('job is required');
|
|
796
|
+
const result = await this.aiMentor.getJobOverview(jobName);
|
|
797
|
+
if (!result) {
|
|
798
|
+
return { content: [{ type: 'text', text: `Job "${jobName}" not found.` }] };
|
|
799
|
+
}
|
|
800
|
+
return { content: [{ type: 'text', text: result.overview }] };
|
|
801
|
+
}
|
|
802
|
+
case 'seekMentoring': {
|
|
803
|
+
const result = await this.aiMentor.handleMentoringRequest({
|
|
804
|
+
jobName: args.jobName,
|
|
805
|
+
issueNumber: args.issueNumber,
|
|
806
|
+
currentPhase: args.currentPhase,
|
|
807
|
+
status: args.status,
|
|
808
|
+
evidence: args.evidence,
|
|
809
|
+
findings: args.findings,
|
|
810
|
+
skipIncludes: true // ← the key difference: raw, no include resolution
|
|
811
|
+
});
|
|
812
|
+
return { content: [{ type: 'text', text: result.message }] };
|
|
813
|
+
}
|
|
814
|
+
default:
|
|
815
|
+
throw new Error(`Unknown raw tool: ${tool}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
getAvailableJobs() {
|
|
819
|
+
const jobs = Array.from(this.registryService.getFileIndex().values()).filter(f => f.type === 'job' && !f.isStub);
|
|
820
|
+
return Array.from(new Set(jobs.map(f => f.name.replace('.md', ''))));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
exports.McpService = McpService;
|