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.
@@ -3,11 +3,16 @@
3
3
  /**
4
4
  * FRAIM Local MCP Server - STDIO Version
5
5
  *
6
- * Simple proxy that:
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 using local .fraim/config.json
10
- * 4. Returns processed content to agent
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
- console.error(`[FRAIM] ${message}`);
50
+ const key = this.apiKey || 'MISSING_API_KEY';
51
+ console.error(`[FRAIM key:${key}] ${message}`);
39
52
  }
40
53
  logError(message) {
41
- console.error(`[FRAIM ERROR] ${message}`);
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
- * Substitute template variables in content
126
- * Replaces {{config.path.to.value}} with actual values from config
159
+ * Automatically detect machine information
127
160
  */
128
- substituteTemplates(content) {
129
- if (!this.config)
130
- return content;
131
- return content.replace(/\{\{config\.([^}]+)\}\}/g, (match, path) => {
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
- const keys = path.split('.');
134
- let value = this.config;
135
- for (const key of keys) {
136
- if (value && typeof value === 'object' && key in value) {
137
- value = value[key];
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
- // Keep original placeholder if path not found
141
- return match;
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
- return String(value);
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
- // Keep original placeholder on error
148
- return match;
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
- // Process content fields that might contain templates
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
- this.logError(`Remote request failed: ${error.message}`);
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;