fraim-framework 2.0.177 → 2.0.180
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/ai-hub/server.js +50 -1
- 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-provider.js +74 -61
- package/dist/src/cli/commands/add-surface.js +128 -0
- package/dist/src/cli/commands/login.js +5 -69
- package/dist/src/cli/commands/setup.js +27 -347
- package/dist/src/cli/distribution/marketplace-bundles.js +580 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +5 -3
- package/dist/src/cli/mcp/mcp-server-registry.js +10 -3
- package/dist/src/cli/providers/local-provider-registry.js +2 -3
- package/dist/src/cli/setup/auto-mcp-setup.js +9 -32
- package/dist/src/cli/setup/ide-detector.js +34 -14
- package/dist/src/config/persona-capability-bundles.js +17 -13
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/first-run/session-service.js +2 -2
- 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/local-mcp-server/stdio-server.js +28 -4
- package/dist/src/local-mcp-server/usage-collector.js +24 -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 +14 -3
- package/public/ai-hub/index.html +14 -2
- package/public/ai-hub/script.js +340 -66
- package/public/ai-hub/styles.css +83 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fileFraimIssue = fileFraimIssue;
|
|
7
|
+
exports.listFraimIssuesByReporter = listFraimIssuesByReporter;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const FRAIM_REPO_OWNER = 'mathursrus';
|
|
10
|
+
const FRAIM_REPO_NAME = 'FRAIM';
|
|
11
|
+
const DEFAULT_LIST_LIMIT = 20;
|
|
12
|
+
const MAX_LIST_LIMIT = 100;
|
|
13
|
+
function normalizeLimit(limit) {
|
|
14
|
+
if (!Number.isFinite(limit))
|
|
15
|
+
return DEFAULT_LIST_LIMIT;
|
|
16
|
+
const parsed = Math.trunc(limit);
|
|
17
|
+
if (parsed <= 0)
|
|
18
|
+
return DEFAULT_LIST_LIMIT;
|
|
19
|
+
return Math.min(parsed, MAX_LIST_LIMIT);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* File an issue in the FRAIM repository
|
|
23
|
+
*
|
|
24
|
+
* This function creates issues in the FRAIM repository for bug reports,
|
|
25
|
+
* feature requests, and workflow contributions.
|
|
26
|
+
*/
|
|
27
|
+
async function fileFraimIssue(params) {
|
|
28
|
+
const { title, labels, dryRun, reporterEmail } = params;
|
|
29
|
+
// Prepend reporter line so the close-the-loop job can identify who filed the issue
|
|
30
|
+
const reporterLine = reporterEmail ? `**Reported by:** ${reporterEmail}\n\n` : '';
|
|
31
|
+
const body = reporterLine + params.body;
|
|
32
|
+
// Always target the FRAIM repository
|
|
33
|
+
const owner = FRAIM_REPO_OWNER;
|
|
34
|
+
const repo = FRAIM_REPO_NAME;
|
|
35
|
+
if (dryRun) {
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
dryRun: true,
|
|
39
|
+
message: `[DRY RUN] Would create GitHub issue: "${title}" in ${owner}/${repo}.`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Check for GitHub token
|
|
44
|
+
const token = process.env.GITHUB_TOKEN;
|
|
45
|
+
if (!token) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
49
|
+
|
|
50
|
+
Please set GITHUB_TOKEN environment variable to file issues in ${owner}/${repo}.`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Create the issue in FRAIM repository
|
|
54
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues`;
|
|
55
|
+
const payload = { title, body };
|
|
56
|
+
if (labels && labels.length > 0) {
|
|
57
|
+
payload.labels = labels;
|
|
58
|
+
}
|
|
59
|
+
const response = await axios_1.default.post(url, payload, {
|
|
60
|
+
headers: {
|
|
61
|
+
'Authorization': `Bearer ${token}`,
|
|
62
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
issueNumber: response.data.number,
|
|
69
|
+
htmlUrl: response.data.html_url
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
let errorMessage = 'Unknown error';
|
|
74
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
75
|
+
errorMessage = `Status: ${error.response?.status} - ${JSON.stringify(error.response?.data)}`;
|
|
76
|
+
}
|
|
77
|
+
else if (error instanceof Error) {
|
|
78
|
+
errorMessage = error.message;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
message: `Failed to create issue in FRAIM repository: ${errorMessage}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* List issues filed into the FRAIM repository by a specific reporter email.
|
|
88
|
+
*
|
|
89
|
+
* Uses GitHub search against the existing "Reported by:" marker in issue bodies
|
|
90
|
+
* so we do not need a second FRAIM-owned index.
|
|
91
|
+
*/
|
|
92
|
+
async function listFraimIssuesByReporter(params) {
|
|
93
|
+
const { reporterEmail } = params;
|
|
94
|
+
const limit = normalizeLimit(params.limit);
|
|
95
|
+
if (!reporterEmail) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
message: 'Reporter email is required to list FRAIM GitHub issues.'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const token = process.env.GITHUB_TOKEN;
|
|
102
|
+
if (!token) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
106
|
+
|
|
107
|
+
Please set GITHUB_TOKEN environment variable to query issues in ${FRAIM_REPO_OWNER}/${FRAIM_REPO_NAME}.`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const query = `repo:${FRAIM_REPO_OWNER}/${FRAIM_REPO_NAME} is:issue "Reported by: ${reporterEmail}" in:body`;
|
|
111
|
+
try {
|
|
112
|
+
const response = await axios_1.default.get('https://api.github.com/search/issues', {
|
|
113
|
+
headers: {
|
|
114
|
+
'Authorization': `Bearer ${token}`,
|
|
115
|
+
'Accept': 'application/vnd.github+json',
|
|
116
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
117
|
+
},
|
|
118
|
+
params: {
|
|
119
|
+
q: query,
|
|
120
|
+
sort: 'created',
|
|
121
|
+
order: 'desc',
|
|
122
|
+
per_page: limit,
|
|
123
|
+
page: 1
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const issues = Array.isArray(response.data?.items)
|
|
127
|
+
? response.data.items.map((item) => ({
|
|
128
|
+
issueNumber: item.number,
|
|
129
|
+
title: item.title,
|
|
130
|
+
status: item.state === 'closed' ? 'closed' : 'open',
|
|
131
|
+
createdAt: item.created_at
|
|
132
|
+
}))
|
|
133
|
+
: [];
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
issues
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
let errorMessage = 'Unknown error';
|
|
141
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
142
|
+
errorMessage = `Status: ${error.response?.status} - ${JSON.stringify(error.response?.data)}`;
|
|
143
|
+
}
|
|
144
|
+
else if (error instanceof Error) {
|
|
145
|
+
errorMessage = error.message;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
message: `Failed to query issues in the FRAIM repository: ${errorMessage}`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TemplateEngine = void 0;
|
|
4
|
+
exports.getTemplateEngine = getTemplateEngine;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const provider_utils_1 = require("../core/utils/provider-utils");
|
|
8
|
+
const object_utils_1 = require("../core/utils/object-utils");
|
|
9
|
+
/**
|
|
10
|
+
* Multi-Provider Template Engine
|
|
11
|
+
* Processes workflow templates with provider-specific action mappings
|
|
12
|
+
*/
|
|
13
|
+
class TemplateEngine {
|
|
14
|
+
constructor(registryPath) {
|
|
15
|
+
this.providers = new Map();
|
|
16
|
+
this.registryPath = registryPath || (0, path_1.join)(__dirname, '../../registry');
|
|
17
|
+
this.loadProviders();
|
|
18
|
+
}
|
|
19
|
+
loadProviders() {
|
|
20
|
+
try {
|
|
21
|
+
// Load GitHub provider templates
|
|
22
|
+
const githubPath = (0, path_1.join)(this.registryPath, 'providers/github.json');
|
|
23
|
+
if ((0, fs_1.existsSync)(githubPath)) {
|
|
24
|
+
const githubTemplates = JSON.parse((0, fs_1.readFileSync)(githubPath, 'utf-8'));
|
|
25
|
+
this.providers.set('github', githubTemplates);
|
|
26
|
+
}
|
|
27
|
+
// Load ADO provider templates
|
|
28
|
+
const adoPath = (0, path_1.join)(this.registryPath, 'providers/ado.json');
|
|
29
|
+
if ((0, fs_1.existsSync)(adoPath)) {
|
|
30
|
+
const adoTemplates = JSON.parse((0, fs_1.readFileSync)(adoPath, 'utf-8'));
|
|
31
|
+
this.providers.set('ado', adoTemplates);
|
|
32
|
+
}
|
|
33
|
+
// Load Jira provider templates
|
|
34
|
+
const jiraPath = (0, path_1.join)(this.registryPath, 'providers/jira.json');
|
|
35
|
+
if ((0, fs_1.existsSync)(jiraPath)) {
|
|
36
|
+
const jiraTemplates = JSON.parse((0, fs_1.readFileSync)(jiraPath, 'utf-8'));
|
|
37
|
+
this.providers.set('jira', jiraTemplates);
|
|
38
|
+
}
|
|
39
|
+
// Load GitLab provider templates
|
|
40
|
+
const gitlabPath = (0, path_1.join)(this.registryPath, 'providers/gitlab.json');
|
|
41
|
+
if ((0, fs_1.existsSync)(gitlabPath)) {
|
|
42
|
+
const gitlabTemplates = JSON.parse((0, fs_1.readFileSync)(gitlabPath, 'utf-8'));
|
|
43
|
+
this.providers.set('gitlab', gitlabTemplates);
|
|
44
|
+
}
|
|
45
|
+
console.log(`✅ Loaded ${this.providers.size} provider templates`);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.warn('⚠️ Failed to load provider templates:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Process workflow content with provider actions only
|
|
53
|
+
*/
|
|
54
|
+
processWorkflow(workflowContent, repositoryInfo) {
|
|
55
|
+
// Only process provider actions, no client-side config variables
|
|
56
|
+
return this.processProviderActions(workflowContent, repositoryInfo);
|
|
57
|
+
}
|
|
58
|
+
processProviderActions(workflowContent, repositoryInfo) {
|
|
59
|
+
// Determine code provider from repository info using shared utility
|
|
60
|
+
const repoData = repositoryInfo?.repository || repositoryInfo;
|
|
61
|
+
const codeProvider = repoData?.provider || (0, provider_utils_1.detectProvider)(repoData?.url);
|
|
62
|
+
// Determine issue provider from explicit issueTracking context only
|
|
63
|
+
const issueProvider = repositoryInfo?.issueTracking?.provider;
|
|
64
|
+
// Get templates for both providers
|
|
65
|
+
const codeTemplates = this.providers.get(codeProvider);
|
|
66
|
+
const issueTemplates = issueProvider ? this.providers.get(issueProvider) : undefined;
|
|
67
|
+
if (!codeTemplates && !issueTemplates) {
|
|
68
|
+
console.warn(`⚠️ No templates found for providers: code=${codeProvider}, issue=${issueProvider}`);
|
|
69
|
+
return workflowContent;
|
|
70
|
+
}
|
|
71
|
+
// Process with both providers
|
|
72
|
+
return this.processWithSplitProviders(workflowContent, codeProvider, issueProvider, repositoryInfo);
|
|
73
|
+
}
|
|
74
|
+
processWithSplitProviders(workflowContent, codeProvider, issueProvider, repositoryInfo) {
|
|
75
|
+
const codeTemplates = this.providers.get(codeProvider);
|
|
76
|
+
const issueTemplates = issueProvider ? this.providers.get(issueProvider) : undefined;
|
|
77
|
+
let processed = workflowContent;
|
|
78
|
+
// Define which actions are code-related vs issue-related
|
|
79
|
+
const issueActions = new Set([
|
|
80
|
+
'get_issue', 'update_issue_status', 'add_issue_comment',
|
|
81
|
+
'create_issue', 'assign_issue', 'search_issues',
|
|
82
|
+
'close_issue', 'list_issues'
|
|
83
|
+
]);
|
|
84
|
+
const codeActions = new Set([
|
|
85
|
+
'create_pr', 'get_pr', 'get_pr_comments', 'get_pr_review_comments',
|
|
86
|
+
'get_pr_reviews', 'add_pr_comment', 'merge_pr', 'create_branch',
|
|
87
|
+
'switch_branch', 'commit_changes', 'push_branch'
|
|
88
|
+
]);
|
|
89
|
+
// Replace issue actions with issue provider templates
|
|
90
|
+
if (issueTemplates) {
|
|
91
|
+
for (const [action, template] of Object.entries(issueTemplates)) {
|
|
92
|
+
if (issueActions.has(action)) {
|
|
93
|
+
const regex = new RegExp(`{{${action}}}`, 'g');
|
|
94
|
+
const renderedTemplate = this.renderTemplate(template, repositoryInfo);
|
|
95
|
+
processed = processed.replace(regex, renderedTemplate);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Replace code actions with code provider templates
|
|
100
|
+
if (codeTemplates) {
|
|
101
|
+
for (const [action, template] of Object.entries(codeTemplates)) {
|
|
102
|
+
if (codeActions.has(action)) {
|
|
103
|
+
const regex = new RegExp(`{{${action}}}`, 'g');
|
|
104
|
+
const renderedTemplate = this.renderTemplate(template, repositoryInfo);
|
|
105
|
+
processed = processed.replace(regex, renderedTemplate);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return processed;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Render template with repository variable substitution
|
|
113
|
+
*/
|
|
114
|
+
renderTemplate(template, repositoryInfo) {
|
|
115
|
+
if (!repositoryInfo)
|
|
116
|
+
return template;
|
|
117
|
+
return template.replace(/{{([^}]+)}}/g, (match, path) => {
|
|
118
|
+
const trimmedPath = path.trim();
|
|
119
|
+
// Handle proxy.repository.* variables
|
|
120
|
+
if (trimmedPath.startsWith('proxy.repository.')) {
|
|
121
|
+
const repoPath = trimmedPath.substring('proxy.repository.'.length);
|
|
122
|
+
let repoData = repositoryInfo.repository || repositoryInfo;
|
|
123
|
+
const value = (0, object_utils_1.getNestedValue)(repoData, repoPath);
|
|
124
|
+
return value !== undefined ? String(value) : match;
|
|
125
|
+
}
|
|
126
|
+
// Handle proxy.issueTracking.* variables (for split provider mode)
|
|
127
|
+
if (trimmedPath.startsWith('proxy.issueTracking.')) {
|
|
128
|
+
const issueTrackingPath = trimmedPath.substring('proxy.issueTracking.'.length);
|
|
129
|
+
const issueTrackingData = repositoryInfo.issueTracking;
|
|
130
|
+
if (issueTrackingData) {
|
|
131
|
+
const value = (0, object_utils_1.getNestedValue)(issueTrackingData, issueTrackingPath);
|
|
132
|
+
return value !== undefined ? String(value) : match;
|
|
133
|
+
}
|
|
134
|
+
return match;
|
|
135
|
+
}
|
|
136
|
+
return match;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get available providers
|
|
141
|
+
*/
|
|
142
|
+
getAvailableProviders() {
|
|
143
|
+
return Array.from(this.providers.keys());
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get available actions for a provider
|
|
147
|
+
*/
|
|
148
|
+
getProviderActions(provider) {
|
|
149
|
+
const templates = this.providers.get(provider);
|
|
150
|
+
return templates ? Object.keys(templates) : [];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Validate that all template actions in content are supported
|
|
154
|
+
*/
|
|
155
|
+
validateWorkflow(workflowContent, provider) {
|
|
156
|
+
const actionRegex = /{{(\w+)}}/g;
|
|
157
|
+
const actions = new Set();
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = actionRegex.exec(workflowContent)) !== null) {
|
|
160
|
+
// Skip config variables
|
|
161
|
+
if (!match[1].startsWith('config.')) {
|
|
162
|
+
actions.add(match[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const providerActions = this.getProviderActions(provider);
|
|
166
|
+
const missingActions = Array.from(actions).filter(action => !providerActions.includes(action));
|
|
167
|
+
return {
|
|
168
|
+
valid: missingActions.length === 0,
|
|
169
|
+
missingActions
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.TemplateEngine = TemplateEngine;
|
|
174
|
+
// Global template engine instance
|
|
175
|
+
let globalTemplateEngine = null;
|
|
176
|
+
/**
|
|
177
|
+
* Get or create global template engine instance
|
|
178
|
+
*/
|
|
179
|
+
function getTemplateEngine(registryPath) {
|
|
180
|
+
if (!globalTemplateEngine || (registryPath && registryPath !== globalTemplateEngine['registryPath'])) {
|
|
181
|
+
globalTemplateEngine = new TemplateEngine(registryPath);
|
|
182
|
+
}
|
|
183
|
+
return globalTemplateEngine;
|
|
184
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_REGEX = void 0;
|
|
4
|
+
exports.validateEmail = validateEmail;
|
|
5
|
+
exports.getRequestMeta = getRequestMeta;
|
|
6
|
+
/**
|
|
7
|
+
* Standard email validation regex
|
|
8
|
+
*/
|
|
9
|
+
exports.EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* Validate an email address
|
|
12
|
+
*/
|
|
13
|
+
function validateEmail(email) {
|
|
14
|
+
return exports.EMAIL_REGEX.test(email);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract IP and UserAgent from an Express Request
|
|
18
|
+
*/
|
|
19
|
+
function getRequestMeta(req) {
|
|
20
|
+
const ip = req.headers['x-forwarded-for'] || req.ip || 'unknown';
|
|
21
|
+
const userAgent = req.headers['user-agent'] || 'unknown';
|
|
22
|
+
return { ip, userAgent };
|
|
23
|
+
}
|
|
@@ -413,6 +413,7 @@ class FraimLocalMCPServer {
|
|
|
413
413
|
this.otlpServer = null;
|
|
414
414
|
this.isShutdown = false;
|
|
415
415
|
this.mentoringResponseCache = null;
|
|
416
|
+
this.jobStartCache = new Map();
|
|
416
417
|
this.connectSyncInFlight = null;
|
|
417
418
|
this.latestConnectSyncWarning = null;
|
|
418
419
|
this.orgCacheRefreshInFlight = false;
|
|
@@ -1996,7 +1997,7 @@ class FraimLocalMCPServer {
|
|
|
1996
1997
|
if (!this.mentoringResponseCache) {
|
|
1997
1998
|
this.mentoringResponseCache = new Map();
|
|
1998
1999
|
}
|
|
1999
|
-
this.mentoringResponseCache.set(
|
|
2000
|
+
this.mentoringResponseCache.set(request.id, {
|
|
2000
2001
|
nextPhase: tutoringResponse.nextPhase,
|
|
2001
2002
|
jobId: args.jobId || requestSessionId // Use jobId from args, fallback to sessionId
|
|
2002
2003
|
});
|
|
@@ -2063,10 +2064,13 @@ class FraimLocalMCPServer {
|
|
|
2063
2064
|
const overview = await mentor.getJobOverview(name);
|
|
2064
2065
|
if (overview) {
|
|
2065
2066
|
this.log(`✅ Local override found for get_fraim_job: ${name}`);
|
|
2067
|
+
const jobId = (0, crypto_1.randomUUID)();
|
|
2068
|
+
this.jobStartCache.set(request.id, { jobId, jobName: name });
|
|
2069
|
+
this.log(`📊 Generated jobId=${jobId} for job=${name}`);
|
|
2066
2070
|
let responseText = overview.overview;
|
|
2067
2071
|
if (!overview.isSimple) {
|
|
2068
2072
|
responseText = `${mentor.getCompactPhaseAuthority()}\n\n${responseText}`;
|
|
2069
|
-
responseText += `\n\n---\n\n**This job has phases.** Use \`seekMentoring\` to get phase-specific instructions.`;
|
|
2073
|
+
responseText += `\n\n---\n\n**Job ID:** \`${jobId}\`\n\n**This job has phases.** Use \`seekMentoring\` with the jobId above to get phase-specific instructions.`;
|
|
2070
2074
|
}
|
|
2071
2075
|
// Inject local learning context for job requests (RFC 177).
|
|
2072
2076
|
const userEmail = this.ensureEngine().getUserEmail();
|
|
@@ -2356,7 +2360,7 @@ class FraimLocalMCPServer {
|
|
|
2356
2360
|
if (!this.repoInfo) {
|
|
2357
2361
|
this.detectRepoInfo();
|
|
2358
2362
|
}
|
|
2359
|
-
//
|
|
2363
|
+
// Enrich seekMentoring args with response data (nextPhase, jobId from cache)
|
|
2360
2364
|
if (toolName === 'seekMentoring' && this.mentoringResponseCache) {
|
|
2361
2365
|
const requestId = request.id;
|
|
2362
2366
|
const cachedResponse = this.mentoringResponseCache.get(requestId);
|
|
@@ -2366,10 +2370,20 @@ class FraimLocalMCPServer {
|
|
|
2366
2370
|
nextPhase: cachedResponse.nextPhase,
|
|
2367
2371
|
jobId: cachedResponse.jobId
|
|
2368
2372
|
};
|
|
2369
|
-
this.mentoringResponseCache.delete(requestId);
|
|
2373
|
+
this.mentoringResponseCache.delete(requestId);
|
|
2370
2374
|
this.log(`📊 Enriched seekMentoring args with nextPhase=${cachedResponse.nextPhase}, jobId=${cachedResponse.jobId}`);
|
|
2371
2375
|
}
|
|
2372
2376
|
}
|
|
2377
|
+
// Enrich get_fraim_job args with the UUID jobId and action:start from jobStartCache
|
|
2378
|
+
if (toolName === 'get_fraim_job') {
|
|
2379
|
+
const requestId = request.id;
|
|
2380
|
+
const cached = this.jobStartCache.get(requestId);
|
|
2381
|
+
if (cached) {
|
|
2382
|
+
args = { ...args, jobId: cached.jobId, action: 'start' };
|
|
2383
|
+
this.jobStartCache.delete(requestId);
|
|
2384
|
+
this.log(`📊 Enriched get_fraim_job args with jobId=${cached.jobId}, action=start`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2373
2387
|
// Capture the current queue size before collection
|
|
2374
2388
|
const beforeCount = this.usageCollector.getEventCount();
|
|
2375
2389
|
try {
|
|
@@ -2410,6 +2424,16 @@ class FraimLocalMCPServer {
|
|
|
2410
2424
|
this.log(`📊 🔍 Debug: get_fraim_file path="${args.path}"`);
|
|
2411
2425
|
}
|
|
2412
2426
|
}
|
|
2427
|
+
// Emit job-complete event when seekMentoring reaches the final phase
|
|
2428
|
+
if (toolName === 'seekMentoring' && args.nextPhase === null && args.jobName && args.jobId) {
|
|
2429
|
+
try {
|
|
2430
|
+
this.usageCollector.collectJobComplete(args.jobName, args.jobId, requestSessionId);
|
|
2431
|
+
this.log(`📊 ✅ Job complete event emitted: ${args.jobName} (jobId: ${args.jobId})`);
|
|
2432
|
+
}
|
|
2433
|
+
catch (err) {
|
|
2434
|
+
this.log(`📊 ⚠️ Job complete event failed (non-blocking): ${err.message}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2413
2437
|
}
|
|
2414
2438
|
catch (error) {
|
|
2415
2439
|
this.log(`📊 ❌ Usage collection error: ${error.message}`);
|
|
@@ -142,6 +142,8 @@ class UsageCollector {
|
|
|
142
142
|
case 'get_fraim_job':
|
|
143
143
|
if (args.job)
|
|
144
144
|
analyticsArgs.job = args.job;
|
|
145
|
+
if (args.action)
|
|
146
|
+
analyticsArgs.action = args.action;
|
|
145
147
|
break;
|
|
146
148
|
case 'fraim_connect':
|
|
147
149
|
if (args.agent?.name)
|
|
@@ -179,6 +181,28 @@ class UsageCollector {
|
|
|
179
181
|
return null;
|
|
180
182
|
}
|
|
181
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Emit a job-completion event when seekMentoring returns nextPhase === null.
|
|
186
|
+
* Called by the local proxy after the final mentoring event is queued.
|
|
187
|
+
*/
|
|
188
|
+
collectJobComplete(jobName, jobId, sessionId) {
|
|
189
|
+
const event = {
|
|
190
|
+
type: 'job',
|
|
191
|
+
name: jobName,
|
|
192
|
+
userId: this.userId || 'unknown',
|
|
193
|
+
sessionId,
|
|
194
|
+
success: true,
|
|
195
|
+
jobId,
|
|
196
|
+
repoIdentifier: this.repoIdentifier || undefined,
|
|
197
|
+
agentName: this.agentName || undefined,
|
|
198
|
+
agentModel: this.agentModel || undefined,
|
|
199
|
+
args: { action: 'complete', jobId },
|
|
200
|
+
};
|
|
201
|
+
this.events.push(event);
|
|
202
|
+
const msg = `[UsageCollector] ✅ Job complete event: ${jobName} (jobId: ${jobId}, queue: ${this.events.length})`;
|
|
203
|
+
console.error(msg);
|
|
204
|
+
process.stderr.write(msg + '\n');
|
|
205
|
+
}
|
|
182
206
|
/**
|
|
183
207
|
* Get collected events for upload and clear the queue
|
|
184
208
|
*/
|