fraim-framework 2.0.62 → 2.0.64
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/bin/fraim-mcp.js +25 -9
- package/dist/src/cli/commands/init-project.js +98 -26
- package/dist/src/cli/commands/init.js +81 -23
- package/dist/src/cli/commands/setup.js +275 -47
- package/dist/src/local-mcp-server/stdio-server.js +412 -27
- package/dist/src/utils/enforcement-utils.js +239 -0
- package/dist/src/utils/git-utils.js +0 -27
- package/dist/src/utils/platform-detection.js +1 -1
- package/dist/src/utils/script-sync-utils.js +6 -1
- package/dist/src/utils/validate-workflows.js +101 -0
- package/package.json +5 -4
- package/registry/stubs/workflows/azure/cost-optimization.md +11 -0
- package/bin/fraim.js +0 -23
- package/dist/src/cli/commands/mcp.js +0 -65
- package/dist/src/fraim/issue-tracking/ado-provider.js +0 -304
- package/dist/src/fraim/issue-tracking/factory.js +0 -63
- package/dist/src/fraim/issue-tracking/github-provider.js +0 -200
- package/dist/src/fraim/issue-tracking/types.js +0 -7
- package/dist/src/fraim/issue-tracking-config.js +0 -83
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* FRAIM Local MCP Server - STDIO Version
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Proxy that:
|
|
7
7
|
* 1. Accepts MCP requests via stdin/stdout
|
|
8
8
|
* 2. Proxies to remote FRAIM server
|
|
9
|
-
* 3. Performs template substitution
|
|
10
|
-
*
|
|
9
|
+
* 3. Performs template substitution:
|
|
10
|
+
* - Config variables: {{config.path.to.value}}
|
|
11
|
+
* - Platform-specific actions: {{get_issue}}, {{create_pr}}, etc.
|
|
12
|
+
* 4. Automatically detects and injects machine/repo info for fraim_connect
|
|
13
|
+
* 5. Substitutes {{delivery.*}} templates based on user's workingStyle
|
|
14
|
+
* (PR or Conversation from ~/.fraim/config.json). Delivery phases live
|
|
15
|
+
* server-side in the workflow; the proxy just fills in mode-specific content.
|
|
11
16
|
*/
|
|
12
17
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -16,6 +21,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
21
|
exports.FraimLocalMCPServer = void 0;
|
|
17
22
|
const fs_1 = require("fs");
|
|
18
23
|
const path_1 = require("path");
|
|
24
|
+
const os_1 = require("os");
|
|
25
|
+
const child_process_1 = require("child_process");
|
|
26
|
+
const crypto_1 = require("crypto");
|
|
19
27
|
const axios_1 = __importDefault(require("axios"));
|
|
20
28
|
class FraimLocalMCPServer {
|
|
21
29
|
constructor() {
|
|
@@ -23,8 +31,11 @@ class FraimLocalMCPServer {
|
|
|
23
31
|
this.clientSupportsRoots = false;
|
|
24
32
|
this.workspaceRoot = null;
|
|
25
33
|
this.pendingRootsRequest = false;
|
|
34
|
+
this.machineInfo = null;
|
|
35
|
+
this.repoInfo = null;
|
|
26
36
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
27
37
|
this.apiKey = process.env.FRAIM_API_KEY || '';
|
|
38
|
+
this.localVersion = this.detectLocalVersion();
|
|
28
39
|
if (!this.apiKey) {
|
|
29
40
|
this.logError('❌ FRAIM_API_KEY environment variable is required');
|
|
30
41
|
process.exit(1);
|
|
@@ -32,13 +43,36 @@ class FraimLocalMCPServer {
|
|
|
32
43
|
this.log('🚀 FRAIM Local MCP Server starting...');
|
|
33
44
|
this.log(`📡 Remote server: ${this.remoteUrl}`);
|
|
34
45
|
this.log(`🔑 API key: ${this.apiKey.substring(0, 10)}...`);
|
|
46
|
+
this.log(`Local MCP version: ${this.localVersion}`);
|
|
35
47
|
}
|
|
36
48
|
log(message) {
|
|
37
49
|
// Log to stderr (stdout is reserved for MCP protocol)
|
|
38
|
-
|
|
50
|
+
const key = this.apiKey || 'MISSING_API_KEY';
|
|
51
|
+
console.error(`[FRAIM key:${key}] ${message}`);
|
|
39
52
|
}
|
|
40
53
|
logError(message) {
|
|
41
|
-
|
|
54
|
+
const key = this.apiKey || 'MISSING_API_KEY';
|
|
55
|
+
console.error(`[FRAIM ERROR key:${key}] ${message}`);
|
|
56
|
+
}
|
|
57
|
+
detectLocalVersion() {
|
|
58
|
+
const candidates = [
|
|
59
|
+
(0, path_1.join)(__dirname, '..', '..', '..', 'package.json'),
|
|
60
|
+
(0, path_1.join)(__dirname, '..', '..', 'package.json')
|
|
61
|
+
];
|
|
62
|
+
for (const pkgPath of candidates) {
|
|
63
|
+
try {
|
|
64
|
+
if (!(0, fs_1.existsSync)(pkgPath))
|
|
65
|
+
continue;
|
|
66
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf8'));
|
|
67
|
+
if (typeof pkg.version === 'string' && pkg.version.trim().length > 0) {
|
|
68
|
+
return pkg.version;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Ignore and try the next candidate
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 'unknown';
|
|
42
76
|
}
|
|
43
77
|
findProjectRoot() {
|
|
44
78
|
// If we already have workspace root from MCP roots, use it
|
|
@@ -122,40 +156,325 @@ class FraimLocalMCPServer {
|
|
|
122
156
|
}
|
|
123
157
|
}
|
|
124
158
|
/**
|
|
125
|
-
*
|
|
126
|
-
* Replaces {{config.path.to.value}} with actual values from config
|
|
159
|
+
* Automatically detect machine information
|
|
127
160
|
*/
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
return
|
|
131
|
-
|
|
161
|
+
detectMachineInfo() {
|
|
162
|
+
if (this.machineInfo) {
|
|
163
|
+
return this.machineInfo;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
this.machineInfo = {
|
|
167
|
+
hostname: (0, os_1.hostname)(),
|
|
168
|
+
platform: (0, os_1.platform)(),
|
|
169
|
+
memory: (0, os_1.totalmem)(),
|
|
170
|
+
cpus: (0, os_1.cpus)().length
|
|
171
|
+
};
|
|
172
|
+
this.log(`✅ Detected machine info: ${this.machineInfo.hostname} (${this.machineInfo.platform})`);
|
|
173
|
+
return this.machineInfo;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
this.logError(`Failed to detect machine info: ${error}`);
|
|
177
|
+
return {
|
|
178
|
+
hostname: 'unknown',
|
|
179
|
+
platform: 'unknown'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Automatically detect repository information from git
|
|
185
|
+
*/
|
|
186
|
+
detectRepoInfo() {
|
|
187
|
+
if (this.repoInfo) {
|
|
188
|
+
return this.repoInfo;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const projectDir = this.findProjectRoot() || process.cwd();
|
|
192
|
+
// Try to get git remote URL
|
|
193
|
+
let repoUrl = '';
|
|
132
194
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
195
|
+
repoUrl = (0, child_process_1.execSync)('git remote get-url origin', {
|
|
196
|
+
cwd: projectDir,
|
|
197
|
+
encoding: 'utf8',
|
|
198
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
199
|
+
}).trim();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// If git command fails, construct URL from config if available
|
|
203
|
+
if (this.config?.repository?.owner && this.config?.repository?.name) {
|
|
204
|
+
const provider = this.config.repository.provider || 'github';
|
|
205
|
+
if (provider === 'github') {
|
|
206
|
+
repoUrl = `https://github.com/${this.config.repository.owner}/${this.config.repository.name}.git`;
|
|
138
207
|
}
|
|
139
|
-
else {
|
|
140
|
-
//
|
|
141
|
-
|
|
208
|
+
else if (provider === 'ado') {
|
|
209
|
+
// Azure DevOps URL format
|
|
210
|
+
repoUrl = `https://dev.azure.com/${this.config.repository.owner}/${this.config.repository.name}/_git/${this.config.repository.name}`;
|
|
142
211
|
}
|
|
212
|
+
this.log(`📋 Constructed repo URL from config: ${repoUrl}`);
|
|
143
213
|
}
|
|
144
|
-
|
|
214
|
+
}
|
|
215
|
+
if (!repoUrl) {
|
|
216
|
+
this.log('⚠️ No git repository found and no config available');
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
// Parse owner and name from URL
|
|
220
|
+
let owner = '';
|
|
221
|
+
let name = '';
|
|
222
|
+
let organization = '';
|
|
223
|
+
let project = '';
|
|
224
|
+
// Handle GitHub URLs: https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
225
|
+
const githubHttpsMatch = repoUrl.match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)/);
|
|
226
|
+
const adoMatch = repoUrl.match(/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/]+)/);
|
|
227
|
+
if (githubHttpsMatch) {
|
|
228
|
+
owner = githubHttpsMatch[1];
|
|
229
|
+
name = githubHttpsMatch[2];
|
|
230
|
+
}
|
|
231
|
+
else if (adoMatch) {
|
|
232
|
+
// Azure DevOps: organization and project are separate fields
|
|
233
|
+
organization = adoMatch[1];
|
|
234
|
+
project = adoMatch[2];
|
|
235
|
+
owner = organization; // For compatibility
|
|
236
|
+
name = adoMatch[3];
|
|
237
|
+
}
|
|
238
|
+
else if (this.config?.repository) {
|
|
239
|
+
// Fall back to config if URL parsing fails
|
|
240
|
+
owner = this.config.repository.owner || '';
|
|
241
|
+
name = this.config.repository.name || '';
|
|
242
|
+
organization = this.config.repository.organization || '';
|
|
243
|
+
project = this.config.repository.project || '';
|
|
244
|
+
}
|
|
245
|
+
// Get current branch
|
|
246
|
+
let branch = '';
|
|
247
|
+
try {
|
|
248
|
+
branch = (0, child_process_1.execSync)('git branch --show-current', {
|
|
249
|
+
cwd: projectDir,
|
|
250
|
+
encoding: 'utf8',
|
|
251
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
252
|
+
}).trim();
|
|
145
253
|
}
|
|
146
254
|
catch (error) {
|
|
147
|
-
//
|
|
148
|
-
|
|
255
|
+
// Fall back to config default branch if available
|
|
256
|
+
if (this.config?.repository?.defaultBranch) {
|
|
257
|
+
branch = this.config.repository.defaultBranch;
|
|
258
|
+
}
|
|
149
259
|
}
|
|
260
|
+
this.repoInfo = {
|
|
261
|
+
url: repoUrl,
|
|
262
|
+
owner: owner || 'unknown',
|
|
263
|
+
name: name || 'unknown',
|
|
264
|
+
...(organization && { organization }),
|
|
265
|
+
...(project && { project }),
|
|
266
|
+
...(branch && { branch })
|
|
267
|
+
};
|
|
268
|
+
this.log(`✅ Detected repo info: ${this.repoInfo.owner}/${this.repoInfo.name}`);
|
|
269
|
+
return this.repoInfo;
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
this.logError(`Failed to detect repo info: ${error}`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get the user's working style preference from ~/.fraim/config.json
|
|
278
|
+
*/
|
|
279
|
+
getWorkingStyle() {
|
|
280
|
+
try {
|
|
281
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
282
|
+
const configPath = (0, path_1.join)(homeDir, '.fraim', 'config.json');
|
|
283
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
284
|
+
const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
285
|
+
return config.workingStyle || 'PR';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Ignore errors, default to PR
|
|
290
|
+
}
|
|
291
|
+
return 'PR';
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Substitute template variables in content
|
|
295
|
+
* Supports:
|
|
296
|
+
* 1. {{config.path.to.value}} - Config substitution
|
|
297
|
+
* 2. {{config.path.to.value | "fallback instruction"}} - With fallback
|
|
298
|
+
* 3. {{delivery.*}} - Working-style-specific delivery templates (from registry/providers/delivery-*.json)
|
|
299
|
+
* 4. Platform-specific action templates (GitHub vs ADO, from registry/providers/*.json)
|
|
300
|
+
*/
|
|
301
|
+
substituteTemplates(content) {
|
|
302
|
+
let result = content;
|
|
303
|
+
// First, substitute config variables with fallback support (only if config exists)
|
|
304
|
+
if (this.config) {
|
|
305
|
+
result = result.replace(/\{\{config\.([^}|]+)(?:\s*\|\s*"([^"]+)")?\}\}/g, (match, path, fallback) => {
|
|
306
|
+
try {
|
|
307
|
+
const value = this.getNestedValue(this.config, path.trim());
|
|
308
|
+
if (value !== undefined) {
|
|
309
|
+
// Config value exists - substitute it
|
|
310
|
+
return typeof value === 'object'
|
|
311
|
+
? JSON.stringify(value)
|
|
312
|
+
: String(value);
|
|
313
|
+
}
|
|
314
|
+
if (fallback !== undefined) {
|
|
315
|
+
// Config value missing - use fallback instruction
|
|
316
|
+
return fallback;
|
|
317
|
+
}
|
|
318
|
+
// No fallback provided - keep placeholder
|
|
319
|
+
return match;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// On error, use fallback if provided, otherwise keep placeholder
|
|
323
|
+
return fallback !== undefined ? fallback : match;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Second, substitute {{delivery.*}} templates based on workingStyle
|
|
328
|
+
// Loaded from registry/providers/delivery-{mode}.json (same pattern as platform templates)
|
|
329
|
+
const deliveryValues = this.loadDeliveryTemplates();
|
|
330
|
+
if (deliveryValues) {
|
|
331
|
+
result = result.replace(/\{\{delivery\.([^}]+)\}\}/g, (match, key) => {
|
|
332
|
+
const value = deliveryValues[`delivery.${key.trim()}`];
|
|
333
|
+
return value !== undefined ? value : match;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Third, substitute platform-specific action templates
|
|
337
|
+
// This works independently of config - only needs repo info
|
|
338
|
+
result = this.substitutePlatformActions(result);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Load delivery templates from registry based on workingStyle.
|
|
343
|
+
* Follows the same pattern as loadProviderTemplates (registry/providers/*.json).
|
|
344
|
+
*/
|
|
345
|
+
loadDeliveryTemplates() {
|
|
346
|
+
const workingStyle = this.getWorkingStyle();
|
|
347
|
+
const filename = workingStyle === 'Conversation' ? 'delivery-conversation.json' : 'delivery-pr.json';
|
|
348
|
+
try {
|
|
349
|
+
const projectRoot = this.findProjectRoot();
|
|
350
|
+
if (projectRoot) {
|
|
351
|
+
const deliveryPath = (0, path_1.join)(projectRoot, 'registry', 'providers', filename);
|
|
352
|
+
if ((0, fs_1.existsSync)(deliveryPath)) {
|
|
353
|
+
return JSON.parse((0, fs_1.readFileSync)(deliveryPath, 'utf-8'));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Fallback: Try node_modules/@fraim/framework
|
|
357
|
+
const nodeModulesPath = (0, path_1.join)(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry', 'providers', filename);
|
|
358
|
+
if ((0, fs_1.existsSync)(nodeModulesPath)) {
|
|
359
|
+
return JSON.parse((0, fs_1.readFileSync)(nodeModulesPath, 'utf-8'));
|
|
360
|
+
}
|
|
361
|
+
this.log(`⚠️ Could not find delivery templates: ${filename}`);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
this.log(`⚠️ Failed to load delivery templates: ${error.message}`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Substitute platform-specific action templates
|
|
371
|
+
* Replaces {{action}} with provider-specific MCP tool calls
|
|
372
|
+
*/
|
|
373
|
+
substitutePlatformActions(content) {
|
|
374
|
+
// Detect provider from repo info
|
|
375
|
+
const provider = this.detectProvider();
|
|
376
|
+
// Load provider templates
|
|
377
|
+
const templates = this.loadProviderTemplates(provider);
|
|
378
|
+
if (!templates) {
|
|
379
|
+
return content; // No templates available, return unchanged
|
|
380
|
+
}
|
|
381
|
+
let result = content;
|
|
382
|
+
// Replace {{action}} with provider-specific implementations
|
|
383
|
+
for (const [action, template] of Object.entries(templates)) {
|
|
384
|
+
const regex = new RegExp(`\\{\\{${action}\\}\\}`, 'g');
|
|
385
|
+
// Substitute repository variables in the template
|
|
386
|
+
const renderedTemplate = this.renderActionTemplate(template, provider);
|
|
387
|
+
result = result.replace(regex, renderedTemplate);
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Detect provider from repository info
|
|
393
|
+
*/
|
|
394
|
+
detectProvider() {
|
|
395
|
+
if (!this.repoInfo) {
|
|
396
|
+
return 'github'; // Default
|
|
397
|
+
}
|
|
398
|
+
const url = this.repoInfo.url || '';
|
|
399
|
+
if (url.includes('dev.azure.com') || url.includes('visualstudio.com')) {
|
|
400
|
+
return 'ado';
|
|
401
|
+
}
|
|
402
|
+
// Check config for explicit provider
|
|
403
|
+
if (this.config?.repository?.provider) {
|
|
404
|
+
return this.config.repository.provider;
|
|
405
|
+
}
|
|
406
|
+
return 'github'; // Default
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Load provider templates from registry
|
|
410
|
+
*/
|
|
411
|
+
loadProviderTemplates(provider) {
|
|
412
|
+
try {
|
|
413
|
+
// Try to load from project root first
|
|
414
|
+
const projectRoot = this.findProjectRoot();
|
|
415
|
+
if (projectRoot) {
|
|
416
|
+
const providerPath = (0, path_1.join)(projectRoot, 'registry', 'providers', `${provider}.json`);
|
|
417
|
+
if ((0, fs_1.existsSync)(providerPath)) {
|
|
418
|
+
return JSON.parse((0, fs_1.readFileSync)(providerPath, 'utf-8'));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Fallback: Try to load from node_modules/@fraim/framework
|
|
422
|
+
const nodeModulesPath = (0, path_1.join)(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry', 'providers', `${provider}.json`);
|
|
423
|
+
if ((0, fs_1.existsSync)(nodeModulesPath)) {
|
|
424
|
+
return JSON.parse((0, fs_1.readFileSync)(nodeModulesPath, 'utf-8'));
|
|
425
|
+
}
|
|
426
|
+
this.log(`⚠️ Could not find provider templates for: ${provider}`);
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
this.log(`⚠️ Failed to load provider templates: ${error.message}`);
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Render action template with repository variables
|
|
436
|
+
*/
|
|
437
|
+
renderActionTemplate(template, provider) {
|
|
438
|
+
if (!this.repoInfo && !this.config?.repository) {
|
|
439
|
+
return template; // No repo info available
|
|
440
|
+
}
|
|
441
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
442
|
+
const trimmedPath = path.trim();
|
|
443
|
+
// Handle repository.* variables
|
|
444
|
+
if (trimmedPath.startsWith('repository.')) {
|
|
445
|
+
const repoPath = trimmedPath.substring('repository.'.length);
|
|
446
|
+
// Try repoInfo first (from git detection)
|
|
447
|
+
if (this.repoInfo) {
|
|
448
|
+
const value = this.getNestedValue(this.repoInfo, repoPath);
|
|
449
|
+
if (value !== undefined)
|
|
450
|
+
return String(value);
|
|
451
|
+
}
|
|
452
|
+
// Fallback to config
|
|
453
|
+
if (this.config?.repository) {
|
|
454
|
+
const value = this.getNestedValue(this.config.repository, repoPath);
|
|
455
|
+
if (value !== undefined)
|
|
456
|
+
return String(value);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Keep original placeholder if not found
|
|
460
|
+
return match;
|
|
150
461
|
});
|
|
151
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Get nested value from object using dot notation
|
|
465
|
+
*/
|
|
466
|
+
getNestedValue(obj, path) {
|
|
467
|
+
return path.split('.').reduce((current, key) => {
|
|
468
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
469
|
+
}, obj);
|
|
470
|
+
}
|
|
152
471
|
/**
|
|
153
472
|
* Process template substitution in MCP response
|
|
154
473
|
*/
|
|
155
474
|
processResponse(response) {
|
|
156
475
|
if (!response.result)
|
|
157
476
|
return response;
|
|
158
|
-
//
|
|
477
|
+
// Recursively substitute templates in all string values
|
|
159
478
|
const processValue = (value) => {
|
|
160
479
|
if (typeof value === 'string') {
|
|
161
480
|
return this.substituteTemplates(value);
|
|
@@ -181,24 +500,90 @@ class FraimLocalMCPServer {
|
|
|
181
500
|
* Proxy request to remote FRAIM server
|
|
182
501
|
*/
|
|
183
502
|
async proxyToRemote(request) {
|
|
503
|
+
const requestId = (0, crypto_1.randomUUID)();
|
|
184
504
|
try {
|
|
505
|
+
// Special handling for fraim_connect - automatically inject machine and repo info
|
|
506
|
+
if (request.method === 'tools/call' && request.params?.name === 'fraim_connect') {
|
|
507
|
+
this.log(`[req:${requestId}] Intercepting fraim_connect to inject machine/repo info`);
|
|
508
|
+
const args = request.params.arguments || {};
|
|
509
|
+
// REQUIRED: Auto-detect and inject machine info
|
|
510
|
+
const detectedMachine = this.detectMachineInfo();
|
|
511
|
+
args.machine = {
|
|
512
|
+
...args.machine, // Agent values as fallback
|
|
513
|
+
...detectedMachine // Detected values override (always win)
|
|
514
|
+
};
|
|
515
|
+
this.log(`[req:${requestId}] Auto-detected and injected machine info: ${args.machine.hostname} (${args.machine.platform}), ${Math.round(args.machine.memory / 1024 / 1024 / 1024)}GB RAM, ${args.machine.cpus} CPUs`);
|
|
516
|
+
// REQUIRED: Auto-detect and inject repo info
|
|
517
|
+
const detectedRepo = this.detectRepoInfo();
|
|
518
|
+
if (detectedRepo) {
|
|
519
|
+
args.repo = {
|
|
520
|
+
...args.repo, // Agent values as fallback
|
|
521
|
+
...detectedRepo // Detected values override (always win)
|
|
522
|
+
};
|
|
523
|
+
this.log(`[req:${requestId}] Auto-detected and injected repo info: ${args.repo.owner}/${args.repo.name}`);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// If detection fails completely, return error instead of sending garbage
|
|
527
|
+
this.logError(`[req:${requestId}] Could not detect repo info and no config available`);
|
|
528
|
+
return {
|
|
529
|
+
jsonrpc: '2.0',
|
|
530
|
+
id: request.id,
|
|
531
|
+
error: {
|
|
532
|
+
code: -32603,
|
|
533
|
+
message: 'Failed to detect repository information. Please ensure you are in a git repository or have .fraim/config.json configured with repository details.'
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
// Update the request with injected info
|
|
538
|
+
request.params.arguments = args;
|
|
539
|
+
}
|
|
540
|
+
this.log(`[req:${requestId}] Proxying ${request.method} to ${this.remoteUrl}/mcp`);
|
|
185
541
|
const response = await axios_1.default.post(`${this.remoteUrl}/mcp`, request, {
|
|
186
542
|
headers: {
|
|
187
543
|
'Content-Type': 'application/json',
|
|
188
|
-
'x-api-key': this.apiKey
|
|
544
|
+
'x-api-key': this.apiKey,
|
|
545
|
+
'x-fraim-request-id': requestId,
|
|
546
|
+
'x-fraim-local-version': this.localVersion
|
|
189
547
|
},
|
|
190
548
|
timeout: 30000
|
|
191
549
|
});
|
|
192
550
|
return response.data;
|
|
193
551
|
}
|
|
194
552
|
catch (error) {
|
|
195
|
-
|
|
553
|
+
const status = error?.response?.status;
|
|
554
|
+
const remoteData = error?.response?.data;
|
|
555
|
+
this.logError(`[req:${requestId}] Remote request failed (${status || 'no-status'}): ${error.message}`);
|
|
556
|
+
if (remoteData && typeof remoteData === 'object') {
|
|
557
|
+
const forwarded = {
|
|
558
|
+
jsonrpc: typeof remoteData.jsonrpc === 'string' ? remoteData.jsonrpc : '2.0',
|
|
559
|
+
id: remoteData.id ?? request.id,
|
|
560
|
+
error: remoteData.error ?? {
|
|
561
|
+
code: -32603,
|
|
562
|
+
message: `Remote server error (${status || 'unknown status'}): ${error.message}`
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
if (forwarded.error && typeof forwarded.error === 'object') {
|
|
566
|
+
const existingData = forwarded.error.data;
|
|
567
|
+
forwarded.error.data = {
|
|
568
|
+
...(existingData && typeof existingData === 'object' ? existingData : {}),
|
|
569
|
+
fraimRequestId: requestId,
|
|
570
|
+
remoteStatus: status ?? null,
|
|
571
|
+
localMcpVersion: this.localVersion
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return forwarded;
|
|
575
|
+
}
|
|
196
576
|
return {
|
|
197
577
|
jsonrpc: '2.0',
|
|
198
578
|
id: request.id,
|
|
199
579
|
error: {
|
|
200
580
|
code: -32603,
|
|
201
|
-
message: `Remote server error: ${error.message}
|
|
581
|
+
message: `Remote server error: ${error.message}`,
|
|
582
|
+
data: {
|
|
583
|
+
fraimRequestId: requestId,
|
|
584
|
+
remoteStatus: status ?? null,
|
|
585
|
+
localMcpVersion: this.localVersion
|
|
586
|
+
}
|
|
202
587
|
}
|
|
203
588
|
};
|
|
204
589
|
}
|
|
@@ -257,7 +642,7 @@ class FraimLocalMCPServer {
|
|
|
257
642
|
}
|
|
258
643
|
// Proxy to remote server
|
|
259
644
|
const response = await this.proxyToRemote(request);
|
|
260
|
-
// Process template substitution
|
|
645
|
+
// Process template substitution (config vars, platform actions, delivery templates)
|
|
261
646
|
const processedResponse = this.processResponse(response);
|
|
262
647
|
this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
|
|
263
648
|
return processedResponse;
|