fraim-framework 2.0.47 → 2.0.48
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/registry/providers/ado.json +19 -0
- package/dist/registry/providers/github.json +19 -0
- package/dist/src/ai-manager/ai-manager.js +8 -1
- package/dist/src/cli/commands/init-project.js +5 -4
- package/dist/src/cli/commands/init.js +8 -7
- package/dist/src/cli/setup/first-run.js +116 -29
- package/dist/src/fraim/config-loader.js +58 -23
- package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
- package/dist/src/fraim/issue-tracking/factory.js +63 -0
- package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
- package/dist/src/fraim/issue-tracking/types.js +7 -0
- package/dist/src/fraim/issue-tracking-config.js +83 -0
- package/dist/src/fraim/issues.js +25 -23
- package/dist/src/fraim/setup-wizard.js +5 -3
- package/dist/src/fraim/template-processor.js +156 -30
- package/dist/src/fraim/types.js +21 -23
- package/dist/src/fraim-mcp-server.js +192 -31
- package/dist/src/utils/git-utils.js +38 -3
- package/dist/src/utils/platform-detection.js +213 -0
- package/dist/tests/test-cli.js +6 -10
- package/dist/tests/test-debug-session.js +130 -0
- package/dist/tests/test-enhanced-session-init.js +184 -0
- package/dist/tests/test-first-run-interactive.js +1 -0
- package/dist/tests/test-first-run-journey.js +274 -54
- package/dist/tests/test-fraim-issues.js +1 -1
- package/dist/tests/test-genericization.js +5 -25
- package/dist/tests/test-mcp-issue-integration.js +6 -2
- package/dist/tests/test-mcp-template-processing.js +156 -0
- package/dist/tests/test-modular-issue-tracking.js +161 -0
- package/dist/tests/test-package-size.js +7 -0
- package/dist/tests/test-workflow-discovery.js +242 -0
- package/package.json +1 -1
|
@@ -43,10 +43,10 @@ const cors_1 = __importDefault(require("cors"));
|
|
|
43
43
|
const fs_1 = require("fs");
|
|
44
44
|
const path_1 = require("path");
|
|
45
45
|
const git_utils_1 = require("./utils/git-utils");
|
|
46
|
-
const config_loader_1 = require("./fraim/config-loader");
|
|
47
46
|
const db_service_1 = require("./fraim/db-service");
|
|
48
47
|
const ai_manager_1 = require("./ai-manager/ai-manager");
|
|
49
48
|
const issues_1 = require("./fraim/issues");
|
|
49
|
+
const template_processor_1 = require("./fraim/template-processor");
|
|
50
50
|
const crypto_1 = require("crypto");
|
|
51
51
|
const dotenv = __importStar(require("dotenv"));
|
|
52
52
|
// Load environment variables
|
|
@@ -170,8 +170,6 @@ class FraimMCPServer {
|
|
|
170
170
|
// Initialize database service
|
|
171
171
|
this.dbService = new db_service_1.FraimDbService();
|
|
172
172
|
this.sessionManager = new SessionManager(this.dbService);
|
|
173
|
-
// Load FRAIM configuration
|
|
174
|
-
this.config = (0, config_loader_1.loadFraimConfig)();
|
|
175
173
|
// Find registry directory (check dist first for production, then source)
|
|
176
174
|
this.registryPath = this.findRegistryPath();
|
|
177
175
|
// Build file index from filesystem (includes registry and .fraim)
|
|
@@ -238,7 +236,7 @@ class FraimMCPServer {
|
|
|
238
236
|
const clientVersion = this.getClientVersion();
|
|
239
237
|
if (clientVersion && clientVersion !== this.serverVersion) {
|
|
240
238
|
// Add a notice header for agents/developers to see
|
|
241
|
-
res.setHeader('X-FRAIM-Version-Notice', `
|
|
239
|
+
res.setHeader('X-FRAIM-Version-Notice', `Update available: Project has ${clientVersion}, Server has ${this.serverVersion}. Run 'fraim sync' to get latest workflows and features. This will not cause issues, but you're missing all the latest capabilities of FRAIM :)`);
|
|
242
240
|
// If it's an /mcp request, we can inject it into the response later in the handler
|
|
243
241
|
// For now, storing it in the request object
|
|
244
242
|
req.versionMismatch = {
|
|
@@ -283,11 +281,20 @@ class FraimMCPServer {
|
|
|
283
281
|
if (req.path === '/health' || req.path.startsWith('/admin')) {
|
|
284
282
|
return next();
|
|
285
283
|
}
|
|
286
|
-
|
|
284
|
+
const apiKey = req.headers['x-api-key'] || req.query['api-key'];
|
|
285
|
+
// In test mode, still extract API key if provided but don't validate it
|
|
287
286
|
if (process.env.NODE_ENV === 'test') {
|
|
287
|
+
if (apiKey) {
|
|
288
|
+
// Set mock API key data for testing
|
|
289
|
+
req.apiKeyData = {
|
|
290
|
+
key: apiKey,
|
|
291
|
+
userId: 'test-user',
|
|
292
|
+
orgId: 'test-org',
|
|
293
|
+
isActive: true
|
|
294
|
+
};
|
|
295
|
+
}
|
|
288
296
|
return next();
|
|
289
297
|
}
|
|
290
|
-
const apiKey = req.headers['x-api-key'] || req.query['api-key'];
|
|
291
298
|
if (!apiKey) {
|
|
292
299
|
console.error(`[FRAIM AUTH] Missing API key for ${req.method} ${req.path}`);
|
|
293
300
|
res.status(401).json({ error: 'Unauthorized', message: 'Missing API key' });
|
|
@@ -412,8 +419,12 @@ class FraimMCPServer {
|
|
|
412
419
|
buildFileIndex() {
|
|
413
420
|
this.fileIndex.clear();
|
|
414
421
|
// 1. Index Global registry (packaged with the server)
|
|
415
|
-
//
|
|
416
|
-
|
|
422
|
+
// In development, prefer source registry over dist registry
|
|
423
|
+
let globalRegistryPath = (0, path_1.join)(__dirname, '..', 'registry');
|
|
424
|
+
if (!(0, fs_1.existsSync)(globalRegistryPath)) {
|
|
425
|
+
// Fallback to dist registry for production
|
|
426
|
+
globalRegistryPath = (0, path_1.join)(__dirname, '..', '..', 'registry');
|
|
427
|
+
}
|
|
417
428
|
if ((0, fs_1.existsSync)(globalRegistryPath)) {
|
|
418
429
|
console.log(`🌍 Indexing global registry from ${globalRegistryPath}`);
|
|
419
430
|
this.indexDirectory(globalRegistryPath, '');
|
|
@@ -428,15 +439,13 @@ class FraimMCPServer {
|
|
|
428
439
|
const fraimBasePath = (0, path_1.join)(process.cwd(), '.fraim');
|
|
429
440
|
if ((0, fs_1.existsSync)(fraimBasePath)) {
|
|
430
441
|
console.log(`🎨 Indexing project-specific .fraim customizations`);
|
|
431
|
-
const customizations = this.config.customizations || {};
|
|
432
442
|
// Index custom rules
|
|
433
443
|
const rulesPath = (0, path_1.join)(process.cwd(), '.fraim/rules');
|
|
434
444
|
if ((0, fs_1.existsSync)(rulesPath)) {
|
|
435
445
|
this.indexDirectory(rulesPath, 'fraim/rules');
|
|
436
446
|
}
|
|
437
|
-
// Index custom workflows
|
|
438
|
-
const
|
|
439
|
-
const workflowsPath = (0, path_1.join)(process.cwd(), workflowsPathStr);
|
|
447
|
+
// Index custom workflows (use default path)
|
|
448
|
+
const workflowsPath = (0, path_1.join)(process.cwd(), '.fraim/workflows');
|
|
440
449
|
if ((0, fs_1.existsSync)(workflowsPath)) {
|
|
441
450
|
this.indexDirectory(workflowsPath, 'fraim/workflows');
|
|
442
451
|
}
|
|
@@ -797,7 +806,7 @@ class FraimMCPServer {
|
|
|
797
806
|
}
|
|
798
807
|
if (req.versionMismatch) {
|
|
799
808
|
const { client, server } = req.versionMismatch;
|
|
800
|
-
const notice =
|
|
809
|
+
const notice = `ℹ️ **FRAIM Update Available**\n\nYour project is using FRAIM ${client}, but version ${server} is available with new features and improvements.\n\n**To update:** Run \`fraim sync\` to get the latest workflows and templates.\n\n---\n\n`;
|
|
801
810
|
if (result && Array.isArray(result.content)) {
|
|
802
811
|
result.content.unshift({
|
|
803
812
|
type: 'text',
|
|
@@ -860,23 +869,87 @@ Call this before fetching any workflow or file to understand the system.`,
|
|
|
860
869
|
{
|
|
861
870
|
name: 'fraim_connect',
|
|
862
871
|
description: `Handshake to start a FRAIM session. MUST be called at the start of every session.
|
|
863
|
-
Registers your environment (machine/repo) for telemetry and unlocks other tools
|
|
872
|
+
Registers your environment (machine/repo/agent) for telemetry and unlocks other tools.
|
|
873
|
+
|
|
874
|
+
**Agent Information**: REQUIRED - Provide your actual agent name and model (validated against known agents).
|
|
875
|
+
**Machine Information**: Use os.hostname() and process.platform - required for telemetry.
|
|
876
|
+
**Repository Information**: FRAIM will auto-detect from .fraim/config.json if available, or provide git remote URL.`,
|
|
864
877
|
inputSchema: {
|
|
865
878
|
type: 'object',
|
|
866
879
|
properties: {
|
|
880
|
+
agent: {
|
|
881
|
+
type: 'object',
|
|
882
|
+
description: 'Agent identification and capabilities',
|
|
883
|
+
properties: {
|
|
884
|
+
name: {
|
|
885
|
+
type: 'string',
|
|
886
|
+
description: 'Agent name (e.g., "Claude", "Cursor", "Kiro", "Windsurf", "Antigravity")',
|
|
887
|
+
examples: ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity']
|
|
888
|
+
},
|
|
889
|
+
model: {
|
|
890
|
+
type: 'string',
|
|
891
|
+
description: 'Model name/version (e.g., "claude-3.5-sonnet", "gpt-4", "cursor-small")',
|
|
892
|
+
examples: ['claude-3.5-sonnet', 'gpt-4', 'cursor-small', 'kiro-agent']
|
|
893
|
+
},
|
|
894
|
+
version: {
|
|
895
|
+
type: 'string',
|
|
896
|
+
description: 'Agent version if available',
|
|
897
|
+
examples: ['1.0.0', '2024.12.1']
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
required: ['name', 'model'],
|
|
901
|
+
additionalProperties: true
|
|
902
|
+
},
|
|
867
903
|
machine: {
|
|
868
904
|
type: 'object',
|
|
869
|
-
description: 'Machine specs (
|
|
905
|
+
description: 'Machine specs - use os.hostname(), process.platform, os.totalmem(), os.cpus().length',
|
|
906
|
+
properties: {
|
|
907
|
+
hostname: {
|
|
908
|
+
type: 'string',
|
|
909
|
+
description: 'Machine hostname from os.hostname()'
|
|
910
|
+
},
|
|
911
|
+
platform: {
|
|
912
|
+
type: 'string',
|
|
913
|
+
description: 'Platform from process.platform (win32, darwin, linux)'
|
|
914
|
+
},
|
|
915
|
+
memory: {
|
|
916
|
+
type: 'number',
|
|
917
|
+
description: 'Total memory in bytes from os.totalmem()'
|
|
918
|
+
},
|
|
919
|
+
cpus: {
|
|
920
|
+
type: 'number',
|
|
921
|
+
description: 'CPU count from os.cpus().length'
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
required: ['hostname', 'platform'],
|
|
870
925
|
additionalProperties: true
|
|
871
926
|
},
|
|
872
927
|
repo: {
|
|
873
928
|
type: 'object',
|
|
874
|
-
description: 'Repository context
|
|
929
|
+
description: 'Repository context - use git remote -v or check .git/config for URL',
|
|
930
|
+
properties: {
|
|
931
|
+
url: {
|
|
932
|
+
type: 'string',
|
|
933
|
+
description: 'Git repository URL from git remote -v'
|
|
934
|
+
},
|
|
935
|
+
owner: {
|
|
936
|
+
type: 'string',
|
|
937
|
+
description: 'Repository owner (extracted from URL)'
|
|
938
|
+
},
|
|
939
|
+
name: {
|
|
940
|
+
type: 'string',
|
|
941
|
+
description: 'Repository name (extracted from URL)'
|
|
942
|
+
},
|
|
943
|
+
branch: {
|
|
944
|
+
type: 'string',
|
|
945
|
+
description: 'Current branch from git branch --show-current'
|
|
946
|
+
}
|
|
947
|
+
},
|
|
875
948
|
required: ['url'],
|
|
876
949
|
additionalProperties: true
|
|
877
950
|
}
|
|
878
951
|
},
|
|
879
|
-
required: ['machine', 'repo']
|
|
952
|
+
required: ['agent', 'machine', 'repo']
|
|
880
953
|
}
|
|
881
954
|
},
|
|
882
955
|
{
|
|
@@ -1054,13 +1127,14 @@ This is your single point of contact for workflow guidance with evidence validat
|
|
|
1054
1127
|
}
|
|
1055
1128
|
async handleToolCall(params, context = {}) {
|
|
1056
1129
|
const { name: toolName, arguments: toolArgs } = params;
|
|
1130
|
+
console.log(`🔧 MCP Server: handleToolCall called with tool: ${toolName}, args:`, JSON.stringify(toolArgs, null, 2));
|
|
1057
1131
|
switch (toolName) {
|
|
1058
1132
|
case 'get_fraim_init':
|
|
1059
1133
|
return await this.handleGetInit();
|
|
1060
1134
|
case 'get_fraim_file':
|
|
1061
1135
|
return await this.handleGetFile(toolArgs.path);
|
|
1062
1136
|
case 'get_fraim_workflow':
|
|
1063
|
-
return await this.handleGetWorkflow(toolArgs.workflow);
|
|
1137
|
+
return await this.handleGetWorkflow(toolArgs.workflow, context.apiKey);
|
|
1064
1138
|
case 'list_fraim_workflows':
|
|
1065
1139
|
return await this.handleListWorkflows();
|
|
1066
1140
|
case 'fraim_get_local_config':
|
|
@@ -1246,20 +1320,30 @@ This is your single point of contact for workflow guidance with evidence validat
|
|
|
1246
1320
|
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1247
1321
|
}
|
|
1248
1322
|
}
|
|
1249
|
-
async handleGetWorkflow(workflowName) {
|
|
1323
|
+
async handleGetWorkflow(workflowName, apiKey) {
|
|
1250
1324
|
// Normalize workflow name (remove .md if present)
|
|
1251
1325
|
const normalizedName = workflowName.replace(/\.md$/, '');
|
|
1252
1326
|
// Try to find the workflow file
|
|
1253
1327
|
const workflowPath = `workflows/${normalizedName}.md`;
|
|
1254
1328
|
const metadata = this.fileIndex.get(workflowPath);
|
|
1255
1329
|
if (!metadata) {
|
|
1256
|
-
// Try alternative category paths
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1330
|
+
// Try alternative category paths - dynamically discover categories from file index
|
|
1331
|
+
const workflowCategories = new Set();
|
|
1332
|
+
// Extract all unique workflow categories from the file index
|
|
1333
|
+
for (const [path, fileMetadata] of this.fileIndex) {
|
|
1334
|
+
if (fileMetadata.type === 'workflow' && path.startsWith('workflows/') && path.includes('/')) {
|
|
1335
|
+
const pathParts = path.split('/');
|
|
1336
|
+
if (pathParts.length >= 3) { // workflows/category/file.md
|
|
1337
|
+
workflowCategories.add(pathParts[1]);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// Search through all discovered categories
|
|
1342
|
+
for (const cat of workflowCategories) {
|
|
1259
1343
|
const altPath = `workflows/${cat}/${normalizedName}.md`;
|
|
1260
1344
|
const altMetadata = this.fileIndex.get(altPath);
|
|
1261
1345
|
if (altMetadata) {
|
|
1262
|
-
return this.returnWorkflowFile(altMetadata);
|
|
1346
|
+
return this.returnWorkflowFile(altMetadata, apiKey);
|
|
1263
1347
|
}
|
|
1264
1348
|
}
|
|
1265
1349
|
// Workflow not found
|
|
@@ -1273,22 +1357,48 @@ This is your single point of contact for workflow guidance with evidence validat
|
|
|
1273
1357
|
}]
|
|
1274
1358
|
};
|
|
1275
1359
|
}
|
|
1276
|
-
return this.returnWorkflowFile(metadata);
|
|
1360
|
+
return this.returnWorkflowFile(metadata, apiKey);
|
|
1277
1361
|
}
|
|
1278
1362
|
/**
|
|
1279
1363
|
* Return a workflow file with its content
|
|
1280
1364
|
*/
|
|
1281
|
-
async returnWorkflowFile(metadata) {
|
|
1365
|
+
async returnWorkflowFile(metadata, apiKey) {
|
|
1282
1366
|
try {
|
|
1283
|
-
const
|
|
1367
|
+
const rawContent = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
|
|
1368
|
+
// Get repository info from session if available
|
|
1369
|
+
let repositoryInfo = null;
|
|
1370
|
+
if (apiKey) {
|
|
1371
|
+
try {
|
|
1372
|
+
const session = await this.dbService.getActiveSessionByApiKey(apiKey);
|
|
1373
|
+
if (session?.repo) {
|
|
1374
|
+
repositoryInfo = {
|
|
1375
|
+
owner: session.repo.owner,
|
|
1376
|
+
name: session.repo.name,
|
|
1377
|
+
organization: session.repo.organization,
|
|
1378
|
+
project: session.repo.project,
|
|
1379
|
+
url: session.repo.url,
|
|
1380
|
+
provider: this.detectProviderFromUrl(session.repo.url)
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
catch (e) {
|
|
1385
|
+
// Ignore session lookup errors, continue without repository info
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// Process templates with provider-specific actions
|
|
1389
|
+
const templateEngine = (0, template_processor_1.getTemplateEngine)(this.registryPath);
|
|
1390
|
+
const processedContent = templateEngine.processWorkflow(rawContent, repositoryInfo);
|
|
1284
1391
|
// Build response
|
|
1285
1392
|
let response = `# Workflow: ${metadata.name.replace('.md', '')}\n\n`;
|
|
1286
1393
|
response += `**Path:** \`${metadata.path}\`\n`;
|
|
1287
1394
|
if (metadata.category) {
|
|
1288
1395
|
response += `**Category:** ${metadata.category}\n`;
|
|
1289
1396
|
}
|
|
1397
|
+
// Add provider info if available
|
|
1398
|
+
const provider = repositoryInfo?.provider || 'GITHUB';
|
|
1399
|
+
response += `**Platform:** ${provider.toUpperCase()}\n`;
|
|
1290
1400
|
response += `\n---\n\n`;
|
|
1291
|
-
response +=
|
|
1401
|
+
response += processedContent;
|
|
1292
1402
|
return {
|
|
1293
1403
|
content: [{
|
|
1294
1404
|
type: 'text',
|
|
@@ -1484,16 +1594,41 @@ If \`.fraim/config.json\` doesn't exist:
|
|
|
1484
1594
|
if (!apiKey) {
|
|
1485
1595
|
throw new Error('No API Key found in context for fraim_connect');
|
|
1486
1596
|
}
|
|
1597
|
+
// Validate required agent information
|
|
1598
|
+
if (!args.agent?.name || !args.agent?.model) {
|
|
1599
|
+
throw new Error('Agent information is required. Please provide agent.name and agent.model in your fraim_connect call.');
|
|
1600
|
+
}
|
|
1601
|
+
// Validate agent information to prevent BS
|
|
1602
|
+
const validAgentNames = ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity', 'ChatGPT', 'Copilot'];
|
|
1603
|
+
if (!validAgentNames.some(valid => args.agent.name.toLowerCase().includes(valid.toLowerCase()))) {
|
|
1604
|
+
throw new Error(`Invalid agent name "${args.agent.name}". Please use one of: ${validAgentNames.join(', ')}`);
|
|
1605
|
+
}
|
|
1606
|
+
// Validate machine information
|
|
1607
|
+
if (!args.machine?.hostname || !args.machine?.platform) {
|
|
1608
|
+
throw new Error('Machine information is required. Please provide machine.hostname and machine.platform. Use os.hostname() and process.platform.');
|
|
1609
|
+
}
|
|
1610
|
+
// Validate repo information - check if .fraim/config.json exists and use it
|
|
1611
|
+
let repoInfo = args.repo || {};
|
|
1612
|
+
// Note: MCP server cannot access client-side .fraim/config.json
|
|
1613
|
+
// Repository info must be provided by the client in the fraim_connect call
|
|
1614
|
+
if (!repoInfo.url) {
|
|
1615
|
+
throw new Error('Repository information is required. Please provide repo.url in the fraim_connect call.');
|
|
1616
|
+
}
|
|
1487
1617
|
// 1. Generate Session ID
|
|
1488
1618
|
const sessionId = (0, crypto_1.randomUUID)();
|
|
1489
1619
|
// userId passed from context
|
|
1490
1620
|
const finalUserId = userId || 'unknown';
|
|
1491
|
-
// 2. Create Session in DB
|
|
1621
|
+
// 2. Create Session in DB with enhanced information
|
|
1492
1622
|
const session = {
|
|
1493
1623
|
sessionId,
|
|
1494
1624
|
userId: finalUserId,
|
|
1495
|
-
|
|
1496
|
-
|
|
1625
|
+
agent: {
|
|
1626
|
+
name: args.agent.name,
|
|
1627
|
+
model: args.agent.model,
|
|
1628
|
+
version: args.agent.version
|
|
1629
|
+
},
|
|
1630
|
+
machine: args.machine,
|
|
1631
|
+
repo: repoInfo,
|
|
1497
1632
|
startTime: new Date(),
|
|
1498
1633
|
lastActive: new Date()
|
|
1499
1634
|
};
|
|
@@ -1503,15 +1638,32 @@ If \`.fraim/config.json\` doesn't exist:
|
|
|
1503
1638
|
await this.dbService.createSession(session);
|
|
1504
1639
|
// 3. Register in SessionManager
|
|
1505
1640
|
this.sessionManager.registerSession(apiKey, sessionId);
|
|
1641
|
+
// 4. Generate informative response
|
|
1642
|
+
const agentInfo = `${args.agent.name}${args.agent.model ? ` (${args.agent.model})` : ''}${args.agent.version ? ` v${args.agent.version}` : ''}`;
|
|
1643
|
+
const machineInfo = `${args.machine.hostname || 'unknown'} (${args.machine.platform || 'unknown'})`;
|
|
1644
|
+
const repoInfoDisplay = repoInfo.name || repoInfo.url || 'unknown';
|
|
1506
1645
|
return {
|
|
1507
1646
|
content: [{
|
|
1508
1647
|
type: 'text',
|
|
1509
|
-
text: `✅
|
|
1648
|
+
text: `✅ **FRAIM Session Connected!**
|
|
1649
|
+
|
|
1650
|
+
**Session ID**: ${sessionId}
|
|
1651
|
+
**Agent**: ${agentInfo}
|
|
1652
|
+
**Machine**: ${machineInfo}
|
|
1653
|
+
**Repository**: ${repoInfoDisplay}
|
|
1654
|
+
|
|
1655
|
+
🔄 Telemetry active. All FRAIM tools are now unlocked.
|
|
1656
|
+
|
|
1657
|
+
**Next Steps**:
|
|
1658
|
+
- Use \`get_fraim_init\` to see available workflows
|
|
1659
|
+
- Use \`list_fraim_workflows\` to browse by category
|
|
1660
|
+
- Use \`get_fraim_workflow({ workflow: "name" })\` to start working`
|
|
1510
1661
|
}],
|
|
1511
1662
|
sessionId: sessionId
|
|
1512
1663
|
};
|
|
1513
1664
|
}
|
|
1514
1665
|
async handleSeekCoachingOnNextStep(args) {
|
|
1666
|
+
console.log('🔧 MCP Server: handleSeekCoachingOnNextStep called with args:', JSON.stringify(args, null, 2));
|
|
1515
1667
|
try {
|
|
1516
1668
|
const response = await this.aiCoach.handleCoachingRequest({
|
|
1517
1669
|
workflowType: args.workflowType,
|
|
@@ -1521,6 +1673,7 @@ If \`.fraim/config.json\` doesn't exist:
|
|
|
1521
1673
|
evidence: args.evidence,
|
|
1522
1674
|
findings: args.findings
|
|
1523
1675
|
});
|
|
1676
|
+
console.log('✅ MCP Server: AI Coach returned response, length:', response.length);
|
|
1524
1677
|
return {
|
|
1525
1678
|
content: [{
|
|
1526
1679
|
type: 'text',
|
|
@@ -1557,6 +1710,14 @@ If \`.fraim/config.json\` doesn't exist:
|
|
|
1557
1710
|
throw error;
|
|
1558
1711
|
}
|
|
1559
1712
|
}
|
|
1713
|
+
detectProviderFromUrl(url) {
|
|
1714
|
+
if (!url)
|
|
1715
|
+
return 'github';
|
|
1716
|
+
if (url.includes('dev.azure.com') || url.includes('visualstudio.com')) {
|
|
1717
|
+
return 'ado';
|
|
1718
|
+
}
|
|
1719
|
+
return 'github';
|
|
1720
|
+
}
|
|
1560
1721
|
}
|
|
1561
1722
|
exports.FraimMCPServer = FraimMCPServer;
|
|
1562
1723
|
// Start the server if this file is run directly
|
|
@@ -4,6 +4,7 @@ exports.getPort = getPort;
|
|
|
4
4
|
exports.determineDatabaseName = determineDatabaseName;
|
|
5
5
|
exports.getCurrentGitBranch = getCurrentGitBranch;
|
|
6
6
|
exports.determineSchema = determineSchema;
|
|
7
|
+
exports.getDefaultBranch = getDefaultBranch;
|
|
7
8
|
exports.getGitRemoteInfo = getGitRemoteInfo;
|
|
8
9
|
const child_process_1 = require("child_process");
|
|
9
10
|
/**
|
|
@@ -47,7 +48,10 @@ function determineDatabaseName() {
|
|
|
47
48
|
*/
|
|
48
49
|
function getCurrentGitBranch() {
|
|
49
50
|
try {
|
|
50
|
-
return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD'
|
|
51
|
+
return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
52
|
+
timeout: 2000, // 2 second timeout
|
|
53
|
+
stdio: 'pipe'
|
|
54
|
+
}).toString().trim();
|
|
51
55
|
}
|
|
52
56
|
catch (e) {
|
|
53
57
|
return 'master';
|
|
@@ -63,12 +67,42 @@ function determineSchema(branchName) {
|
|
|
63
67
|
}
|
|
64
68
|
return 'prod';
|
|
65
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Gets the default branch name from git remote
|
|
72
|
+
*/
|
|
73
|
+
function getDefaultBranch() {
|
|
74
|
+
try {
|
|
75
|
+
// Try to get the default branch from remote HEAD
|
|
76
|
+
const remoteHead = (0, child_process_1.execSync)('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
77
|
+
timeout: 2000, // 2 second timeout
|
|
78
|
+
stdio: 'pipe'
|
|
79
|
+
}).toString().trim();
|
|
80
|
+
const match = remoteHead.match(/refs\/remotes\/origin\/(.+)$/);
|
|
81
|
+
if (match) {
|
|
82
|
+
return match[1];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
// If that fails, try to get it from the current branch
|
|
87
|
+
try {
|
|
88
|
+
return getCurrentGitBranch();
|
|
89
|
+
}
|
|
90
|
+
catch (e2) {
|
|
91
|
+
// Fall back to common defaults
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Default fallback
|
|
95
|
+
return 'main';
|
|
96
|
+
}
|
|
66
97
|
/**
|
|
67
98
|
* Gets the GitHub remote info (owner and repo name)
|
|
68
99
|
*/
|
|
69
100
|
function getGitRemoteInfo() {
|
|
70
101
|
try {
|
|
71
|
-
const remoteUrl = (0, child_process_1.execSync)('git remote get-url origin'
|
|
102
|
+
const remoteUrl = (0, child_process_1.execSync)('git remote get-url origin', {
|
|
103
|
+
timeout: 2000, // 2 second timeout
|
|
104
|
+
stdio: 'pipe'
|
|
105
|
+
}).toString().trim();
|
|
72
106
|
// Match both HTTPS and SSH formats
|
|
73
107
|
// HTTPS: https://github.com/owner/repo.git OR https://github.com/owner/repo
|
|
74
108
|
// SSH: git@github.com:owner/repo.git OR git@github.com:owner/repo
|
|
@@ -76,7 +110,8 @@ function getGitRemoteInfo() {
|
|
|
76
110
|
if (match) {
|
|
77
111
|
return {
|
|
78
112
|
owner: match[1],
|
|
79
|
-
repo: match[2]
|
|
113
|
+
repo: match[2],
|
|
114
|
+
defaultBranch: getDefaultBranch()
|
|
80
115
|
};
|
|
81
116
|
}
|
|
82
117
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform Detection Utilities
|
|
4
|
+
*
|
|
5
|
+
* Detects development platform (GitHub, ADO) from git remote URLs
|
|
6
|
+
* and provides repository information extraction.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.detectPlatformFromGit = detectPlatformFromGit;
|
|
10
|
+
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
11
|
+
exports.detectPlatformFromUrl = detectPlatformFromUrl;
|
|
12
|
+
exports.validateRepositoryConfig = validateRepositoryConfig;
|
|
13
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
14
|
+
exports.isGitRepository = isGitRepository;
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
/**
|
|
17
|
+
* Detect platform from git remote URL
|
|
18
|
+
*/
|
|
19
|
+
function detectPlatformFromGit() {
|
|
20
|
+
try {
|
|
21
|
+
const remoteUrl = getGitRemoteUrl();
|
|
22
|
+
if (!remoteUrl) {
|
|
23
|
+
return { provider: 'unknown', confidence: 'low' };
|
|
24
|
+
}
|
|
25
|
+
return detectPlatformFromUrl(remoteUrl);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.warn('⚠️ Failed to detect platform from git:', error);
|
|
29
|
+
return { provider: 'unknown', confidence: 'low' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get git remote URL
|
|
34
|
+
*/
|
|
35
|
+
function getGitRemoteUrl() {
|
|
36
|
+
try {
|
|
37
|
+
// Try origin first
|
|
38
|
+
const originUrl = (0, child_process_1.execSync)('git remote get-url origin', {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
41
|
+
}).trim();
|
|
42
|
+
if (originUrl)
|
|
43
|
+
return originUrl;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
// Ignore error, try alternative methods
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Try any remote
|
|
50
|
+
const remotes = (0, child_process_1.execSync)('git remote', {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
53
|
+
}).trim().split('\n');
|
|
54
|
+
if (remotes.length > 0 && remotes[0]) {
|
|
55
|
+
const firstRemoteUrl = (0, child_process_1.execSync)(`git remote get-url ${remotes[0]}`, {
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
58
|
+
}).trim();
|
|
59
|
+
return firstRemoteUrl;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// Ignore error
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Detect platform from URL
|
|
69
|
+
*/
|
|
70
|
+
function detectPlatformFromUrl(url) {
|
|
71
|
+
const normalizedUrl = url.toLowerCase();
|
|
72
|
+
// GitHub detection
|
|
73
|
+
if (normalizedUrl.includes('github.com')) {
|
|
74
|
+
const repository = extractGitHubInfo(url);
|
|
75
|
+
return {
|
|
76
|
+
provider: 'github',
|
|
77
|
+
repository,
|
|
78
|
+
confidence: 'high'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// ADO detection
|
|
82
|
+
if (normalizedUrl.includes('dev.azure.com') ||
|
|
83
|
+
normalizedUrl.includes('visualstudio.com') ||
|
|
84
|
+
normalizedUrl.includes('azure.com')) {
|
|
85
|
+
const repository = extractAdoInfo(url);
|
|
86
|
+
return {
|
|
87
|
+
provider: 'ado',
|
|
88
|
+
repository,
|
|
89
|
+
confidence: 'high'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { provider: 'unknown', confidence: 'low' };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract GitHub repository information from URL
|
|
96
|
+
*/
|
|
97
|
+
function extractGitHubInfo(url) {
|
|
98
|
+
// Handle both HTTPS and SSH URLs
|
|
99
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
100
|
+
// SSH: git@github.com:owner/repo.git
|
|
101
|
+
let match;
|
|
102
|
+
// HTTPS format
|
|
103
|
+
match = url.match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)/i);
|
|
104
|
+
if (match) {
|
|
105
|
+
return {
|
|
106
|
+
provider: 'github',
|
|
107
|
+
owner: match[1],
|
|
108
|
+
name: match[2],
|
|
109
|
+
url: url,
|
|
110
|
+
defaultBranch: 'main'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Fallback - just mark as GitHub
|
|
114
|
+
return {
|
|
115
|
+
provider: 'github',
|
|
116
|
+
url: url,
|
|
117
|
+
defaultBranch: 'main'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract ADO repository information from URL
|
|
122
|
+
*/
|
|
123
|
+
function extractAdoInfo(url) {
|
|
124
|
+
// ADO URL formats:
|
|
125
|
+
// https://dev.azure.com/organization/project/_git/repository
|
|
126
|
+
// https://organization.visualstudio.com/project/_git/repository
|
|
127
|
+
let match;
|
|
128
|
+
// dev.azure.com format
|
|
129
|
+
match = url.match(/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/\.]+)/i);
|
|
130
|
+
if (match) {
|
|
131
|
+
return {
|
|
132
|
+
provider: 'ado',
|
|
133
|
+
organization: match[1],
|
|
134
|
+
project: match[2],
|
|
135
|
+
name: match[3],
|
|
136
|
+
url: url,
|
|
137
|
+
defaultBranch: 'main'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// visualstudio.com format
|
|
141
|
+
match = url.match(/([^\.]+)\.visualstudio\.com\/([^\/]+)\/_git\/([^\/\.]+)/i);
|
|
142
|
+
if (match) {
|
|
143
|
+
return {
|
|
144
|
+
provider: 'ado',
|
|
145
|
+
organization: match[1],
|
|
146
|
+
project: match[2],
|
|
147
|
+
name: match[3],
|
|
148
|
+
url: url,
|
|
149
|
+
defaultBranch: 'main'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Fallback - just mark as ADO
|
|
153
|
+
return {
|
|
154
|
+
provider: 'ado',
|
|
155
|
+
url: url,
|
|
156
|
+
defaultBranch: 'main'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Validate repository configuration
|
|
161
|
+
*/
|
|
162
|
+
function validateRepositoryConfig(config) {
|
|
163
|
+
const errors = [];
|
|
164
|
+
if (!config.provider) {
|
|
165
|
+
errors.push('Provider is required');
|
|
166
|
+
}
|
|
167
|
+
if (config.provider === 'github') {
|
|
168
|
+
if (!config.owner)
|
|
169
|
+
errors.push('GitHub owner is required');
|
|
170
|
+
if (!config.name)
|
|
171
|
+
errors.push('GitHub repository name is required');
|
|
172
|
+
}
|
|
173
|
+
if (config.provider === 'ado') {
|
|
174
|
+
if (!config.organization)
|
|
175
|
+
errors.push('ADO organization is required');
|
|
176
|
+
if (!config.project)
|
|
177
|
+
errors.push('ADO project is required');
|
|
178
|
+
if (!config.name)
|
|
179
|
+
errors.push('ADO repository name is required');
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
valid: errors.length === 0,
|
|
183
|
+
errors
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get current git branch
|
|
188
|
+
*/
|
|
189
|
+
function getCurrentBranch() {
|
|
190
|
+
try {
|
|
191
|
+
return (0, child_process_1.execSync)('git branch --show-current', {
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
194
|
+
}).trim();
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if we're in a git repository
|
|
202
|
+
*/
|
|
203
|
+
function isGitRepository() {
|
|
204
|
+
try {
|
|
205
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', {
|
|
206
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
207
|
+
});
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|