fraim-framework 2.0.55 → 2.0.57
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/CHANGELOG.md +10 -0
- package/dist/src/cli/commands/init-project.js +10 -4
- package/dist/src/cli/setup/mcp-config-generator.js +23 -15
- package/dist/src/local-mcp-server/stdio-server.js +207 -0
- package/dist/src/utils/validate-workflows.js +101 -0
- package/dist/src/utils/workflow-parser.js +81 -0
- package/package.json +16 -11
- package/registry/scripts/pdf-styles.css +172 -0
- package/registry/scripts/prep-issue.sh +46 -4
- package/registry/scripts/profile-server.ts +131 -130
- package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +1 -1
- package/registry/stubs/workflows/customer-development/users-to-target.md +1 -1
- package/registry/stubs/workflows/product-building/design.md +1 -1
- package/registry/stubs/workflows/product-building/implement.md +1 -1
- package/Claude.md +0 -1
- package/dist/registry/ai-manager-rules/design-phases/design-completeness-review.md +0 -73
- package/dist/registry/ai-manager-rules/design-phases/design-design.md +0 -145
- package/dist/registry/ai-manager-rules/implement-phases/implement-code.md +0 -283
- package/dist/registry/ai-manager-rules/implement-phases/implement-completeness-review.md +0 -120
- package/dist/registry/ai-manager-rules/implement-phases/implement-regression.md +0 -173
- package/dist/registry/ai-manager-rules/implement-phases/implement-repro.md +0 -104
- package/dist/registry/ai-manager-rules/implement-phases/implement-scoping.md +0 -100
- package/dist/registry/ai-manager-rules/implement-phases/implement-smoke.md +0 -237
- package/dist/registry/ai-manager-rules/implement-phases/implement-spike.md +0 -121
- package/dist/registry/ai-manager-rules/implement-phases/implement-validate.md +0 -375
- package/dist/registry/ai-manager-rules/retrospective.md +0 -116
- package/dist/registry/ai-manager-rules/shared-phases/address-pr-feedback.md +0 -188
- package/dist/registry/ai-manager-rules/shared-phases/submit-pr.md +0 -202
- package/dist/registry/ai-manager-rules/shared-phases/wait-for-pr-review.md +0 -170
- package/dist/registry/ai-manager-rules/spec-phases/spec-competitor-analysis.md +0 -105
- package/dist/registry/ai-manager-rules/spec-phases/spec-completeness-review.md +0 -66
- package/dist/registry/ai-manager-rules/spec-phases/spec-spec.md +0 -139
- package/dist/registry/providers/ado.json +0 -19
- package/dist/registry/providers/github.json +0 -19
- package/dist/registry/scripts/cleanup-branch.js +0 -287
- package/dist/registry/scripts/evaluate-code-quality.js +0 -66
- package/dist/registry/scripts/exec-with-timeout.js +0 -142
- package/dist/registry/scripts/generate-engagement-emails.js +0 -705
- package/dist/registry/scripts/newsletter-helpers.js +0 -671
- package/dist/registry/scripts/profile-server.js +0 -388
- package/dist/registry/scripts/run-thank-you-workflow.js +0 -92
- package/dist/registry/scripts/send-newsletter-simple.js +0 -85
- package/dist/registry/scripts/send-thank-you-emails.js +0 -54
- package/dist/registry/scripts/validate-openapi-limits.js +0 -311
- package/dist/registry/scripts/validate-test-coverage.js +0 -262
- package/dist/registry/scripts/verify-test-coverage.js +0 -66
- package/dist/scripts/build-stub-registry.js +0 -108
- package/dist/src/ai-manager/ai-manager.js +0 -482
- package/dist/src/ai-manager/phase-flow.js +0 -357
- package/dist/src/ai-manager/types.js +0 -5
- package/dist/src/fraim-mcp-server.js +0 -1885
- package/dist/tests/debug-tools.js +0 -80
- package/dist/tests/shared-server-utils.js +0 -57
- package/dist/tests/test-add-ide.js +0 -283
- package/dist/tests/test-ai-coach-edge-cases.js +0 -420
- package/dist/tests/test-ai-coach-mcp-integration.js +0 -450
- package/dist/tests/test-ai-coach-performance.js +0 -328
- package/dist/tests/test-ai-coach-phase-content.js +0 -264
- package/dist/tests/test-ai-coach-workflows.js +0 -514
- package/dist/tests/test-cli.js +0 -228
- package/dist/tests/test-client-scripts-validation.js +0 -167
- package/dist/tests/test-complete-setup-flow.js +0 -110
- package/dist/tests/test-config-system.js +0 -279
- package/dist/tests/test-debug-session.js +0 -134
- package/dist/tests/test-end-to-end-hybrid-validation.js +0 -328
- package/dist/tests/test-enhanced-session-init.js +0 -188
- package/dist/tests/test-first-run-journey.js +0 -368
- package/dist/tests/test-fraim-issues.js +0 -59
- package/dist/tests/test-genericization.js +0 -44
- package/dist/tests/test-hybrid-script-execution.js +0 -340
- package/dist/tests/test-ide-detector.js +0 -46
- package/dist/tests/test-improved-setup.js +0 -121
- package/dist/tests/test-mcp-config-generator.js +0 -99
- package/dist/tests/test-mcp-connection.js +0 -107
- package/dist/tests/test-mcp-issue-integration.js +0 -156
- package/dist/tests/test-mcp-lifecycle-methods.js +0 -240
- package/dist/tests/test-mcp-shared-server.js +0 -308
- package/dist/tests/test-mcp-template-processing.js +0 -160
- package/dist/tests/test-modular-issue-tracking.js +0 -165
- package/dist/tests/test-node-compatibility.js +0 -95
- package/dist/tests/test-npm-install.js +0 -68
- package/dist/tests/test-package-size.js +0 -108
- package/dist/tests/test-pr-review-workflow.js +0 -307
- package/dist/tests/test-prep-issue.js +0 -129
- package/dist/tests/test-productivity-integration.js +0 -157
- package/dist/tests/test-script-location-independence.js +0 -198
- package/dist/tests/test-script-sync.js +0 -557
- package/dist/tests/test-server-utils.js +0 -32
- package/dist/tests/test-session-rehydration.js +0 -148
- package/dist/tests/test-setup-integration.js +0 -98
- package/dist/tests/test-setup-scenarios.js +0 -322
- package/dist/tests/test-standalone.js +0 -143
- package/dist/tests/test-stub-registry.js +0 -136
- package/dist/tests/test-sync-stubs.js +0 -143
- package/dist/tests/test-sync-version-update.js +0 -93
- package/dist/tests/test-telemetry.js +0 -193
- package/dist/tests/test-token-validator.js +0 -30
- package/dist/tests/test-user-journey.js +0 -236
- package/dist/tests/test-users-to-target-workflow.js +0 -253
- package/dist/tests/test-utils.js +0 -109
- package/dist/tests/test-wizard.js +0 -71
- package/dist/tests/test-workflow-discovery.js +0 -242
- package/labels.json +0 -52
- package/registry/agent-guardrails.md +0 -63
- package/registry/fraim.md +0 -48
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-customer-profiling.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-survey-scoping.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-platform-discovery.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-survey-build-linkedin.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-prospect-qualification.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-survey-build-reddit.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-inventory-compilation.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-survey-build-x.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase5-survey-build-facebook.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase6-survey-build-custom.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase7-survey-dispatch.md +0 -11
- package/registry/stubs/workflows/customer-development/templates/customer-persona-template.md +0 -11
- package/registry/stubs/workflows/customer-development/templates/search-strategy-template.md +0 -11
- package/setup.js +0 -171
- package/tsconfig.json +0 -23
|
@@ -1,1885 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
-
if (k2 === undefined) k2 = k;
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
-
var ownKeys = function(o) {
|
|
21
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
-
var ar = [];
|
|
23
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
-
return ar;
|
|
25
|
-
};
|
|
26
|
-
return ownKeys(o);
|
|
27
|
-
};
|
|
28
|
-
return function (mod) {
|
|
29
|
-
if (mod && mod.__esModule) return mod;
|
|
30
|
-
var result = {};
|
|
31
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
-
__setModuleDefault(result, mod);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
})();
|
|
36
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
-
};
|
|
39
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.FraimMCPServer = void 0;
|
|
41
|
-
const express_1 = __importDefault(require("express"));
|
|
42
|
-
const cors_1 = __importDefault(require("cors"));
|
|
43
|
-
const fs_1 = require("fs");
|
|
44
|
-
const path_1 = require("path");
|
|
45
|
-
const git_utils_1 = require("./utils/git-utils");
|
|
46
|
-
const db_service_1 = require("./fraim/db-service");
|
|
47
|
-
const ai_manager_1 = require("./ai-manager/ai-manager");
|
|
48
|
-
const issues_1 = require("./fraim/issues");
|
|
49
|
-
const template_processor_1 = require("./fraim/template-processor");
|
|
50
|
-
const crypto_1 = require("crypto");
|
|
51
|
-
const dotenv = __importStar(require("dotenv"));
|
|
52
|
-
// Load environment variables
|
|
53
|
-
dotenv.config();
|
|
54
|
-
// Global error handlers for better debugging in production
|
|
55
|
-
process.on('uncaughtException', (err) => {
|
|
56
|
-
console.error('❌ CRITICAL: Uncaught Exception:', err.message);
|
|
57
|
-
console.error(err.stack);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
});
|
|
60
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
61
|
-
console.error('❌ CRITICAL: Unhandled Rejection at:', promise, 'reason:', reason);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
});
|
|
64
|
-
console.log('🚀 FRAIM MCP Server starting up...');
|
|
65
|
-
console.log(`📂 Working Directory: ${process.cwd()}`);
|
|
66
|
-
console.log(`🏠 Dirname: ${__dirname}`);
|
|
67
|
-
try {
|
|
68
|
-
console.log('files in current dir:', (0, fs_1.readdirSync)(process.cwd()).join(', '));
|
|
69
|
-
const distPath = (0, path_1.join)(process.cwd(), 'dist');
|
|
70
|
-
if ((0, fs_1.existsSync)(distPath)) {
|
|
71
|
-
console.log('files in dist:', (0, fs_1.readdirSync)(distPath).join(', '));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch (e) {
|
|
75
|
-
console.warn('⚠️ Could not log directory structure:', e.message);
|
|
76
|
-
}
|
|
77
|
-
class SessionManager {
|
|
78
|
-
constructor(dbService) {
|
|
79
|
-
this.sessions = new Map();
|
|
80
|
-
this.dbService = dbService;
|
|
81
|
-
// Default 1m, configurable for testing
|
|
82
|
-
this.FLUSH_INTERVAL_MS = process.env.FRAIM_TELEMETRY_FLUSH_INTERVAL
|
|
83
|
-
? parseInt(process.env.FRAIM_TELEMETRY_FLUSH_INTERVAL)
|
|
84
|
-
: 60 * 1000;
|
|
85
|
-
// Flush on shutdown
|
|
86
|
-
const cleanup = async () => {
|
|
87
|
-
console.log('🛑 Flushing telemetry sessions before shutdown...');
|
|
88
|
-
// Race flush with a 2s timeout to guarantee exit
|
|
89
|
-
const timeout = new Promise(resolve => setTimeout(resolve, 2000));
|
|
90
|
-
await Promise.race([this.flushAll(), timeout]);
|
|
91
|
-
console.log('🛑 Shutdown flush complete (or timed out). Exiting.');
|
|
92
|
-
process.exit(0);
|
|
93
|
-
};
|
|
94
|
-
process.on('SIGTERM', cleanup);
|
|
95
|
-
process.on('SIGINT', cleanup);
|
|
96
|
-
}
|
|
97
|
-
registerSession(apiKey, sessionId) {
|
|
98
|
-
const now = Date.now();
|
|
99
|
-
this.sessions.set(apiKey, { sessionId, lastWrite: now, lastActive: now });
|
|
100
|
-
}
|
|
101
|
-
async updateActivity(apiKey) {
|
|
102
|
-
let session = this.sessions.get(apiKey);
|
|
103
|
-
const now = Date.now();
|
|
104
|
-
if (!session) {
|
|
105
|
-
// Re-hydration: Check DB for an active session for this API Key
|
|
106
|
-
try {
|
|
107
|
-
const dbSession = await this.dbService.getActiveSessionByApiKey(apiKey);
|
|
108
|
-
if (dbSession) {
|
|
109
|
-
console.log(`🔄 Re-hydrating session for API Key: ${apiKey}`);
|
|
110
|
-
session = {
|
|
111
|
-
sessionId: dbSession.sessionId,
|
|
112
|
-
lastWrite: now,
|
|
113
|
-
lastActive: now
|
|
114
|
-
};
|
|
115
|
-
this.sessions.set(apiKey, session);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (e) {
|
|
122
|
-
console.error('⚠️ Failed to re-hydrate session from DB:', e);
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
session.lastActive = now;
|
|
127
|
-
// Write-behind: Only flush to DB if > Interval elapsed
|
|
128
|
-
if (now - session.lastWrite > this.FLUSH_INTERVAL_MS) {
|
|
129
|
-
// Async write
|
|
130
|
-
this.dbService.updateSessionActivity(session.sessionId, new Date(now)).catch(e => console.error('⚠️ Failed to flush session activity:', e));
|
|
131
|
-
session.lastWrite = now;
|
|
132
|
-
}
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
async flushAll() {
|
|
136
|
-
const promises = [];
|
|
137
|
-
for (const [_, data] of this.sessions) {
|
|
138
|
-
// Flush the memory lastActive time
|
|
139
|
-
promises.push(this.dbService.updateSessionActivity(data.sessionId, new Date(data.lastActive)));
|
|
140
|
-
}
|
|
141
|
-
await Promise.allSettled(promises);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
class FraimMCPServer {
|
|
145
|
-
constructor() {
|
|
146
|
-
this.fileIndex = new Map();
|
|
147
|
-
this.workflowKeywords = new Map();
|
|
148
|
-
this.app = (0, express_1.default)();
|
|
149
|
-
this.app.use((0, cors_1.default)());
|
|
150
|
-
this.app.use(express_1.default.json());
|
|
151
|
-
// Load server version
|
|
152
|
-
try {
|
|
153
|
-
// Try process.cwd() first as it's the root in most deployments
|
|
154
|
-
let pkgPath = (0, path_1.join)(process.cwd(), 'package.json');
|
|
155
|
-
if (!(0, fs_1.existsSync)(pkgPath)) {
|
|
156
|
-
// Fallback to relative to __dirname (dist/src/ -> ../../package.json)
|
|
157
|
-
pkgPath = (0, path_1.join)(__dirname, '..', '..', 'package.json');
|
|
158
|
-
}
|
|
159
|
-
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
160
|
-
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf8'));
|
|
161
|
-
this.serverVersion = pkg.version;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
this.serverVersion = 'unknown';
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
catch (e) {
|
|
168
|
-
this.serverVersion = 'unknown';
|
|
169
|
-
}
|
|
170
|
-
// Initialize database service
|
|
171
|
-
this.dbService = new db_service_1.FraimDbService();
|
|
172
|
-
this.sessionManager = new SessionManager(this.dbService);
|
|
173
|
-
// Find registry directory (check dist first for production, then source)
|
|
174
|
-
this.registryPath = this.findRegistryPath();
|
|
175
|
-
// Build file index from filesystem (includes registry and .fraim)
|
|
176
|
-
this.buildFileIndex();
|
|
177
|
-
this.buildWorkflowKeywords();
|
|
178
|
-
// Initialize AI Coach with file index
|
|
179
|
-
this.aiCoach = new ai_manager_1.AICoach(this.fileIndex);
|
|
180
|
-
// Apply core middleware first (logging and auth)
|
|
181
|
-
this.app.use(this.requestLogger.bind(this));
|
|
182
|
-
this.app.use(this.versionCheck.bind(this));
|
|
183
|
-
// Public health check (before auth)
|
|
184
|
-
this.app.get('/health', (req, res) => {
|
|
185
|
-
res.status(200).json({
|
|
186
|
-
status: 'ok',
|
|
187
|
-
version: this.serverVersion,
|
|
188
|
-
timestamp: new Date().toISOString(),
|
|
189
|
-
service: 'fraim-mcp-standalone'
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
this.app.use(this.authMiddleware.bind(this));
|
|
193
|
-
this.app.use(this.telemetryMiddleware.bind(this));
|
|
194
|
-
this.setupRoutes();
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Middleware to log all requests
|
|
198
|
-
*/
|
|
199
|
-
requestLogger(req, res, next) {
|
|
200
|
-
const start = Date.now();
|
|
201
|
-
const { method, path } = req;
|
|
202
|
-
const params = method === 'POST' ? JSON.stringify(req.body) : '';
|
|
203
|
-
res.on('finish', () => {
|
|
204
|
-
const duration = Date.now() - start;
|
|
205
|
-
const status = res.statusCode;
|
|
206
|
-
const apiKey = req.apiKeyData;
|
|
207
|
-
const keyStr = apiKey ? apiKey.key : 'ANONYMOUS';
|
|
208
|
-
if (params && params.length > 0) {
|
|
209
|
-
console.info(`${keyStr} ${method} ${path} ${status} ${duration}ms ${params}`);
|
|
210
|
-
}
|
|
211
|
-
// Log usage if authenticated
|
|
212
|
-
// DISABLE: Granular logging disabled in favor of Session Keep-Alive
|
|
213
|
-
/*
|
|
214
|
-
if (apiKey) {
|
|
215
|
-
this.dbService.logUsage({
|
|
216
|
-
keyId: apiKey.key,
|
|
217
|
-
userId: apiKey.userId,
|
|
218
|
-
method,
|
|
219
|
-
path,
|
|
220
|
-
status,
|
|
221
|
-
duration
|
|
222
|
-
}).catch(e => console.error('⚠️ Failed to log usage:', (e as Error).message));
|
|
223
|
-
}
|
|
224
|
-
*/
|
|
225
|
-
});
|
|
226
|
-
next();
|
|
227
|
-
}
|
|
228
|
-
async versionCheck(req, res, next) {
|
|
229
|
-
// Skip version check in test environment
|
|
230
|
-
if (process.env.NODE_ENV === 'test') {
|
|
231
|
-
return next();
|
|
232
|
-
}
|
|
233
|
-
// Only check for /mcp, /files routes, or root POST (MCP) where AI agents are active
|
|
234
|
-
const isMcpTraffic = req.path.startsWith('/mcp') ||
|
|
235
|
-
req.path.startsWith('/files') ||
|
|
236
|
-
(req.path === '/' && req.method === 'POST');
|
|
237
|
-
if (!isMcpTraffic) {
|
|
238
|
-
return next();
|
|
239
|
-
}
|
|
240
|
-
const clientVersion = this.getClientVersion();
|
|
241
|
-
if (clientVersion && clientVersion !== this.serverVersion) {
|
|
242
|
-
// Add a notice header for agents/developers to see
|
|
243
|
-
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 :)`);
|
|
244
|
-
// If it's an /mcp request, we can inject it into the response later in the handler
|
|
245
|
-
// For now, storing it in the request object
|
|
246
|
-
req.versionMismatch = {
|
|
247
|
-
client: clientVersion,
|
|
248
|
-
server: this.serverVersion
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
next();
|
|
252
|
-
}
|
|
253
|
-
getClientVersion() {
|
|
254
|
-
try {
|
|
255
|
-
// Try to find FRAIM framework version in the project's node_modules or package.json
|
|
256
|
-
const projectPkgPath = (0, path_1.join)(process.cwd(), 'package.json');
|
|
257
|
-
if ((0, fs_1.existsSync)(projectPkgPath)) {
|
|
258
|
-
const pkg = JSON.parse((0, fs_1.readFileSync)(projectPkgPath, 'utf8'));
|
|
259
|
-
// Check dependencies or devDependencies
|
|
260
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
261
|
-
const version = deps['@fraim/framework'];
|
|
262
|
-
if (version) {
|
|
263
|
-
// Clean version string (remove ^, ~)
|
|
264
|
-
return version.replace(/[\^~]/, '');
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// Fallback: check .fraim/config.json if we implement versioning there
|
|
268
|
-
const configPath = (0, path_1.join)(process.cwd(), '.fraim', 'config.json');
|
|
269
|
-
if ((0, fs_1.existsSync)(configPath)) {
|
|
270
|
-
const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
271
|
-
if (config.version)
|
|
272
|
-
return config.version;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch (e) {
|
|
276
|
-
// Ignore errors
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Middleware to authenticate requests via API key
|
|
282
|
-
*/
|
|
283
|
-
async authMiddleware(req, res, next) {
|
|
284
|
-
// Skip auth for public health check, admin routes, website signup, and sales inquiries
|
|
285
|
-
if (req.path === '/health' || req.path.startsWith('/admin') || req.path === '/api/signup' || req.path === '/api/sales') {
|
|
286
|
-
return next();
|
|
287
|
-
}
|
|
288
|
-
const apiKey = req.headers['x-api-key'] || req.query['api-key'];
|
|
289
|
-
// In test mode, still extract API key if provided but don't validate it
|
|
290
|
-
if (process.env.NODE_ENV === 'test') {
|
|
291
|
-
if (apiKey) {
|
|
292
|
-
// Set mock API key data for testing
|
|
293
|
-
req.apiKeyData = {
|
|
294
|
-
key: apiKey,
|
|
295
|
-
userId: 'test-user',
|
|
296
|
-
orgId: 'test-org',
|
|
297
|
-
isActive: true
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
return next();
|
|
301
|
-
}
|
|
302
|
-
if (!apiKey) {
|
|
303
|
-
console.error(`[FRAIM AUTH] Missing API key for ${req.method} ${req.path}`);
|
|
304
|
-
res.status(401).json({ error: 'Unauthorized', message: 'Missing API key' });
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
try {
|
|
308
|
-
const apiKeyData = await this.dbService.verifyApiKey(apiKey);
|
|
309
|
-
if (apiKeyData) {
|
|
310
|
-
req.apiKeyData = apiKeyData;
|
|
311
|
-
return next();
|
|
312
|
-
}
|
|
313
|
-
console.error(`❌ FRAIM AUTH: Invalid API key: ${apiKey}`);
|
|
314
|
-
res.status(401).json({
|
|
315
|
-
error: 'Unauthorized',
|
|
316
|
-
message: 'Invalid x-api-key'
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
321
|
-
console.error('❌ FRAIM AUTH: Error during verification:', msg);
|
|
322
|
-
if (error instanceof Error && error.stack)
|
|
323
|
-
console.error(error.stack);
|
|
324
|
-
res.status(500).json({ error: 'Internal Server Error', details: msg });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
adminAuthMiddleware(req, res, next) {
|
|
328
|
-
const adminKey = process.env.FRAIM_ADMIN_KEY;
|
|
329
|
-
if (!adminKey) {
|
|
330
|
-
console.error('⚠️ FRAIM_ADMIN_KEY not configured on server');
|
|
331
|
-
return res.status(503).json({ error: 'Management API disabled (key not set)' });
|
|
332
|
-
}
|
|
333
|
-
const providedKey = req.headers['x-admin-key'];
|
|
334
|
-
if (providedKey !== adminKey) {
|
|
335
|
-
return res.status(403).json({ error: 'Forbidden: Invalid admin key' });
|
|
336
|
-
}
|
|
337
|
-
return next();
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Middleware to enforce Session Handshake and track activity
|
|
341
|
-
*/
|
|
342
|
-
async telemetryMiddleware(req, res, next) {
|
|
343
|
-
// Skip for non-authenticated or exempt paths
|
|
344
|
-
const exemptPaths = ['/health', '/admin', '/mcp'];
|
|
345
|
-
if (exemptPaths.some(p => req.path.startsWith(p)) && req.method === 'GET') {
|
|
346
|
-
return next();
|
|
347
|
-
}
|
|
348
|
-
// 1. Standard MCP Lifecycle & Discovery methods (Initialize, List Tools/Resources/Prompts, etc.)
|
|
349
|
-
const exemptMethods = [
|
|
350
|
-
'initialize',
|
|
351
|
-
'notifications/initialized',
|
|
352
|
-
'tools/list',
|
|
353
|
-
'resources/list',
|
|
354
|
-
'prompts/list',
|
|
355
|
-
'logging/setLevel'
|
|
356
|
-
];
|
|
357
|
-
if (req.body && exemptMethods.includes(req.body.method)) {
|
|
358
|
-
return next();
|
|
359
|
-
}
|
|
360
|
-
// 2. FRAIM Bootstrap/Discovery Tools that don't need an active session
|
|
361
|
-
if (req.body && req.body.method === 'tools/call') {
|
|
362
|
-
const toolName = req.body.params?.name;
|
|
363
|
-
const safeTools = [
|
|
364
|
-
'fraim_connect',
|
|
365
|
-
'get_fraim_init',
|
|
366
|
-
'list_fraim_workflows',
|
|
367
|
-
'fraim_get_local_config'
|
|
368
|
-
];
|
|
369
|
-
if (safeTools.includes(toolName))
|
|
370
|
-
return next();
|
|
371
|
-
}
|
|
372
|
-
const apiKey = req.apiKeyData?.key;
|
|
373
|
-
if (!apiKey)
|
|
374
|
-
return next(); // Auth middleware handles 401 earlier
|
|
375
|
-
// Check Session
|
|
376
|
-
const isActive = await this.sessionManager.updateActivity(apiKey);
|
|
377
|
-
if (!isActive) {
|
|
378
|
-
console.log(`⛔ Telemetry blocked request: ${req.method} ${req.path} (No Session for ${apiKey})`);
|
|
379
|
-
// Enforce Handshake with clear instructions for agents
|
|
380
|
-
res.status(400).json({
|
|
381
|
-
jsonrpc: '2.0',
|
|
382
|
-
error: {
|
|
383
|
-
code: -32600,
|
|
384
|
-
message: `⛔ Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first with these parameters:
|
|
385
|
-
|
|
386
|
-
{
|
|
387
|
-
"agent": {
|
|
388
|
-
"name": "YourAgentName",
|
|
389
|
-
"model": "your-model-name"
|
|
390
|
-
},
|
|
391
|
-
"machine": {
|
|
392
|
-
"hostname": "use os.hostname()",
|
|
393
|
-
"platform": "use process.platform"
|
|
394
|
-
},
|
|
395
|
-
"repo": {
|
|
396
|
-
"url": "git remote URL",
|
|
397
|
-
"owner": "repo owner",
|
|
398
|
-
"name": "repo name"
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
After successful fraim_connect, all other FRAIM tools will work. This is required for telemetry and session management.`
|
|
403
|
-
},
|
|
404
|
-
id: req.body?.id || null
|
|
405
|
-
});
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
console.log(`✅ Telemetry active for ${apiKey}`);
|
|
409
|
-
return next();
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Get the Express application for mounting in another server
|
|
413
|
-
* This allows FRAIM routes to be accessible on the main server port (e.g., on Azure)
|
|
414
|
-
*/
|
|
415
|
-
getApp() {
|
|
416
|
-
return this.app;
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Find registry directory
|
|
420
|
-
* Checks dist/registry (production) first, then registry (development)
|
|
421
|
-
*/
|
|
422
|
-
findRegistryPath() {
|
|
423
|
-
const distPath = (0, path_1.join)(process.cwd(), 'dist', 'registry');
|
|
424
|
-
const sourcePath = (0, path_1.join)(process.cwd(), 'registry');
|
|
425
|
-
if ((0, fs_1.existsSync)(distPath)) {
|
|
426
|
-
console.log(`📁 Using registry from dist(production)`);
|
|
427
|
-
return distPath;
|
|
428
|
-
}
|
|
429
|
-
else if ((0, fs_1.existsSync)(sourcePath)) {
|
|
430
|
-
console.log(`📁 Using registry from source(development)`);
|
|
431
|
-
return sourcePath;
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
console.warn(`⚠️ registry directory not found at ${distPath} or ${sourcePath} `);
|
|
435
|
-
return sourcePath; // Default to source path
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Build an index of all files in registry and .fraim directories
|
|
440
|
-
*/
|
|
441
|
-
buildFileIndex() {
|
|
442
|
-
this.fileIndex.clear();
|
|
443
|
-
// 1. Index Global registry (packaged with the server)
|
|
444
|
-
// In development, prefer source registry over dist registry
|
|
445
|
-
let globalRegistryPath = (0, path_1.join)(__dirname, '..', 'registry');
|
|
446
|
-
if (!(0, fs_1.existsSync)(globalRegistryPath)) {
|
|
447
|
-
// Fallback to dist registry for production
|
|
448
|
-
globalRegistryPath = (0, path_1.join)(__dirname, '..', '..', 'registry');
|
|
449
|
-
}
|
|
450
|
-
if ((0, fs_1.existsSync)(globalRegistryPath)) {
|
|
451
|
-
console.log(`🌍 Indexing global registry from ${globalRegistryPath}`);
|
|
452
|
-
this.indexDirectory(globalRegistryPath, '');
|
|
453
|
-
}
|
|
454
|
-
// 2. Index Local registry (project-specific framework files)
|
|
455
|
-
const localRegistryPath = (0, path_1.join)(process.cwd(), 'registry');
|
|
456
|
-
if ((0, fs_1.existsSync)(localRegistryPath) && localRegistryPath !== globalRegistryPath) {
|
|
457
|
-
console.log(`🏠 Indexing local registry overrides from ${localRegistryPath}`);
|
|
458
|
-
this.indexDirectory(localRegistryPath, '');
|
|
459
|
-
}
|
|
460
|
-
// 3. Index .fraim custom directories (project-specific customizations)
|
|
461
|
-
const fraimBasePath = (0, path_1.join)(process.cwd(), '.fraim');
|
|
462
|
-
if ((0, fs_1.existsSync)(fraimBasePath)) {
|
|
463
|
-
console.log(`🎨 Indexing project-specific .fraim customizations`);
|
|
464
|
-
// Index custom rules
|
|
465
|
-
const rulesPath = (0, path_1.join)(process.cwd(), '.fraim/rules');
|
|
466
|
-
if ((0, fs_1.existsSync)(rulesPath)) {
|
|
467
|
-
this.indexDirectory(rulesPath, 'fraim/rules');
|
|
468
|
-
}
|
|
469
|
-
// Index custom workflows (use default path)
|
|
470
|
-
const workflowsPath = (0, path_1.join)(process.cwd(), '.fraim/workflows');
|
|
471
|
-
if ((0, fs_1.existsSync)(workflowsPath)) {
|
|
472
|
-
this.indexDirectory(workflowsPath, 'fraim/workflows');
|
|
473
|
-
}
|
|
474
|
-
// Index custom templates
|
|
475
|
-
const templatesPath = (0, path_1.join)(process.cwd(), '.fraim/templates');
|
|
476
|
-
if ((0, fs_1.existsSync)(templatesPath)) {
|
|
477
|
-
this.indexDirectory(templatesPath, 'fraim/templates');
|
|
478
|
-
}
|
|
479
|
-
// Index custom scripts
|
|
480
|
-
const scriptsPath = (0, path_1.join)(process.cwd(), '.fraim/scripts');
|
|
481
|
-
if ((0, fs_1.existsSync)(scriptsPath)) {
|
|
482
|
-
this.indexDirectory(scriptsPath, 'fraim/scripts');
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
console.log(`📚 Indexed ${this.fileIndex.size} files total (Global + Local)`);
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Index files in a directory
|
|
489
|
-
*/
|
|
490
|
-
indexDirectory(dir, basePath = '') {
|
|
491
|
-
const indexFile = (dirPath, relativeBase = '') => {
|
|
492
|
-
if (!(0, fs_1.existsSync)(dirPath)) {
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
496
|
-
for (const entry of entries) {
|
|
497
|
-
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
498
|
-
const relativePath = relativeBase ? (0, path_1.join)(relativeBase, entry.name) : entry.name;
|
|
499
|
-
const normalizedPath = relativePath.replace(/\\/g, '/'); // Normalize path separators
|
|
500
|
-
if (entry.isDirectory()) {
|
|
501
|
-
// Skip nested workspace directories
|
|
502
|
-
if (entry.name.includes('Issue ') || entry.name.includes('Master')) {
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
indexFile(fullPath, normalizedPath);
|
|
506
|
-
}
|
|
507
|
-
else if (entry.isFile()) {
|
|
508
|
-
// Include .md, .ts, .sh, .html files
|
|
509
|
-
const ext = (0, path_1.extname)(entry.name);
|
|
510
|
-
if (['.md', '.ts', '.sh', '.html'].includes(ext)) {
|
|
511
|
-
// Determine file type based on path
|
|
512
|
-
let type = 'other';
|
|
513
|
-
let category;
|
|
514
|
-
const pathLower = normalizedPath.toLowerCase();
|
|
515
|
-
if (pathLower.includes('workflow') || normalizedPath.startsWith('workflows/') || normalizedPath.startsWith('fraim/workflows/')) {
|
|
516
|
-
type = 'workflow';
|
|
517
|
-
const parts = normalizedPath.split('/');
|
|
518
|
-
if (parts.length > 2) {
|
|
519
|
-
category = parts[parts.length - 2]; // e.g., 'customer-development'
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
else if (pathLower.includes('rule') || normalizedPath.startsWith('rules/') || normalizedPath.startsWith('fraim/rules/')) {
|
|
523
|
-
type = 'rule';
|
|
524
|
-
}
|
|
525
|
-
else if (pathLower.includes('template') || normalizedPath.startsWith('templates/') || normalizedPath.startsWith('fraim/templates/')) {
|
|
526
|
-
type = 'template';
|
|
527
|
-
const parts = normalizedPath.split('/');
|
|
528
|
-
if (parts.length > 2) {
|
|
529
|
-
category = parts[parts.length - 2]; // e.g., 'specs', 'evidence'
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
else if (pathLower.includes('script') || normalizedPath.startsWith('scripts/') || normalizedPath.startsWith('fraim/scripts/')) {
|
|
533
|
-
type = 'script';
|
|
534
|
-
}
|
|
535
|
-
else if (normalizedPath.includes('ai-manager-rules/') && normalizedPath.includes('-phases/')) {
|
|
536
|
-
type = 'phase';
|
|
537
|
-
const parts = normalizedPath.split('/');
|
|
538
|
-
if (parts.length > 2) {
|
|
539
|
-
category = parts[parts.length - 2]; // e.g., 'implement-phases', 'spec-phases'
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
// Extract keywords from filename
|
|
543
|
-
const keywords = this.extractKeywords(entry.name, normalizedPath);
|
|
544
|
-
const metadata = {
|
|
545
|
-
path: normalizedPath,
|
|
546
|
-
name: entry.name,
|
|
547
|
-
type,
|
|
548
|
-
category,
|
|
549
|
-
keywords,
|
|
550
|
-
fullPath: fullPath
|
|
551
|
-
};
|
|
552
|
-
// Use normalized path as key (e.g., 'workflows/spec.md' or 'fraim/rules/custom-rule.md')
|
|
553
|
-
// Custom files override generic ones if they have the same path
|
|
554
|
-
this.fileIndex.set(normalizedPath, metadata);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
indexFile(dir, basePath);
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Extract keywords from filename and path
|
|
563
|
-
*/
|
|
564
|
-
extractKeywords(filename, path) {
|
|
565
|
-
const keywords = [];
|
|
566
|
-
// Add filename without extension
|
|
567
|
-
const nameWithoutExt = (0, path_1.basename)(filename, (0, path_1.extname)(filename));
|
|
568
|
-
keywords.push(nameWithoutExt.toLowerCase());
|
|
569
|
-
// Split on hyphens/underscores
|
|
570
|
-
const parts = nameWithoutExt.split(/[-_]/);
|
|
571
|
-
keywords.push(...parts.map(p => p.toLowerCase()));
|
|
572
|
-
// Add path components
|
|
573
|
-
const pathParts = path.split('/');
|
|
574
|
-
keywords.push(...pathParts.map(p => p.toLowerCase()));
|
|
575
|
-
return [...new Set(keywords)]; // Remove duplicates
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Build keyword mappings for workflows to enable context-aware selection
|
|
579
|
-
*/
|
|
580
|
-
buildWorkflowKeywords() {
|
|
581
|
-
// Map common user intents to workflow files
|
|
582
|
-
this.workflowKeywords.set('spec', ['spec', 'specification', 'write spec', 'create spec', 'specification phase']);
|
|
583
|
-
this.workflowKeywords.set('design', ['design', 'rfc', 'technical design', 'architecture', 'design phase']);
|
|
584
|
-
this.workflowKeywords.set('implement', ['implement', 'implementation', 'code', 'develop', 'implementation phase']);
|
|
585
|
-
this.workflowKeywords.set('test', ['test', 'testing', 'write tests', 'test phase']);
|
|
586
|
-
this.workflowKeywords.set('resolve', ['resolve', 'merge', 'close issue', 'resolution']);
|
|
587
|
-
this.workflowKeywords.set('retrospect', ['retrospect', 'retrospective', 'review']);
|
|
588
|
-
// Customer development workflows
|
|
589
|
-
this.workflowKeywords.set('linkedin-outreach', ['linkedin', 'outreach', 'customer development']);
|
|
590
|
-
this.workflowKeywords.set('interview-preparation', ['interview', 'customer interview', 'preparation']);
|
|
591
|
-
this.workflowKeywords.set('insight-analysis', ['insight', 'analysis', 'customer insight']);
|
|
592
|
-
this.workflowKeywords.set('strategic-brainstorming', ['brainstorm', 'strategy', 'strategic']);
|
|
593
|
-
this.workflowKeywords.set('thank-customers', ['thank', 'gratitude', 'appreciation']);
|
|
594
|
-
this.workflowKeywords.set('weekly-newsletter', ['newsletter', 'weekly update']);
|
|
595
|
-
// New lifecycle workflows
|
|
596
|
-
this.workflowKeywords.set('prototype', ['prototype', 'poc', 'spike', 'proof of concept']);
|
|
597
|
-
this.workflowKeywords.set('marketing-strategy', ['marketing', 'promotion', 'launch strategy', 'gtm']);
|
|
598
|
-
this.workflowKeywords.set('content-creation', ['content', 'blog', 'social media', 'docs']);
|
|
599
|
-
this.workflowKeywords.set('launch-checklist', ['launch', 'release', 'go live']);
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Find workflows and related files based on user query
|
|
603
|
-
*/
|
|
604
|
-
findRelevantFiles(query) {
|
|
605
|
-
const queryLower = query.toLowerCase();
|
|
606
|
-
const results = [];
|
|
607
|
-
const matched = new Set();
|
|
608
|
-
// First, check for exact workflow matches
|
|
609
|
-
for (const [workflowKey, keywords] of this.workflowKeywords.entries()) {
|
|
610
|
-
if (keywords.some(keyword => queryLower.includes(keyword))) {
|
|
611
|
-
// Find the workflow file
|
|
612
|
-
const workflowPath = `workflows / ${workflowKey}.md`;
|
|
613
|
-
const metadata = this.fileIndex.get(workflowPath);
|
|
614
|
-
if (metadata && !matched.has(workflowPath)) {
|
|
615
|
-
results.push(metadata);
|
|
616
|
-
matched.add(workflowPath);
|
|
617
|
-
}
|
|
618
|
-
// Add related files based on workflow type
|
|
619
|
-
this.addRelatedFiles(workflowKey, results, matched);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
// If no exact match, search by keywords in file index
|
|
623
|
-
if (results.length === 0) {
|
|
624
|
-
for (const [path, metadata] of this.fileIndex.entries()) {
|
|
625
|
-
if (metadata.keywords.some(keyword => queryLower.includes(keyword))) {
|
|
626
|
-
if (!matched.has(path)) {
|
|
627
|
-
results.push(metadata);
|
|
628
|
-
matched.add(path);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return results;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Add related files based on workflow type
|
|
637
|
-
*/
|
|
638
|
-
addRelatedFiles(workflowKey, results, matched) {
|
|
639
|
-
// For spec workflow, include spec template
|
|
640
|
-
if (workflowKey === 'spec') {
|
|
641
|
-
const specTemplate = this.fileIndex.get('templates/specs/FEATURESPEC-TEMPLATE.md');
|
|
642
|
-
if (specTemplate && !matched.has('templates/specs/FEATURESPEC-TEMPLATE.md')) {
|
|
643
|
-
results.push(specTemplate);
|
|
644
|
-
matched.add('templates/specs/FEATURESPEC-TEMPLATE.md');
|
|
645
|
-
}
|
|
646
|
-
// Add spec evidence template
|
|
647
|
-
const specEvidence = this.fileIndex.get('templates/evidence/Spec-Evidence.md');
|
|
648
|
-
if (specEvidence && !matched.has('templates/evidence/Spec-Evidence.md')) {
|
|
649
|
-
results.push(specEvidence);
|
|
650
|
-
matched.add('templates/evidence/Spec-Evidence.md');
|
|
651
|
-
}
|
|
652
|
-
// Add relevant rules
|
|
653
|
-
this.addRuleFiles(['communication', 'pr-workflow-completeness'], results, matched);
|
|
654
|
-
}
|
|
655
|
-
// For design workflow, include design templates and rules
|
|
656
|
-
if (workflowKey === 'design') {
|
|
657
|
-
const techSpecTemplate = this.fileIndex.get('templates/specs/TECHSPEC-TEMPLATE.md');
|
|
658
|
-
if (techSpecTemplate && !matched.has('templates/specs/TECHSPEC-TEMPLATE.md')) {
|
|
659
|
-
results.push(techSpecTemplate);
|
|
660
|
-
matched.add('templates/specs/TECHSPEC-TEMPLATE.md');
|
|
661
|
-
}
|
|
662
|
-
const bugSpecTemplate = this.fileIndex.get('templates/specs/BUGSPEC-TEMPLATE.md');
|
|
663
|
-
if (bugSpecTemplate && !matched.has('templates/specs/BUGSPEC-TEMPLATE.md')) {
|
|
664
|
-
results.push(bugSpecTemplate);
|
|
665
|
-
matched.add('templates/specs/BUGSPEC-TEMPLATE.md');
|
|
666
|
-
}
|
|
667
|
-
const designEvidence = this.fileIndex.get('templates/evidence/Design-Evidence.md');
|
|
668
|
-
if (designEvidence && !matched.has('templates/evidence/Design-Evidence.md')) {
|
|
669
|
-
results.push(designEvidence);
|
|
670
|
-
matched.add('templates/evidence/Design-Evidence.md');
|
|
671
|
-
}
|
|
672
|
-
this.addRuleFiles(['spike-first-development', 'communication', 'pr-workflow-completeness'], results, matched);
|
|
673
|
-
}
|
|
674
|
-
// For implement workflow, include implementation templates and rules
|
|
675
|
-
if (workflowKey === 'implement') {
|
|
676
|
-
const implEvidence = this.fileIndex.get('templates/evidence/Implementation-FeatureEvidence.md');
|
|
677
|
-
if (implEvidence && !matched.has('templates/evidence/Implementation-FeatureEvidence.md')) {
|
|
678
|
-
results.push(implEvidence);
|
|
679
|
-
matched.add('templates/evidence/Implementation-FeatureEvidence.md');
|
|
680
|
-
}
|
|
681
|
-
const bugEvidence = this.fileIndex.get('templates/evidence/Implementation-BugEvidence.md');
|
|
682
|
-
if (bugEvidence && !matched.has('templates/evidence/Implementation-BugEvidence.md')) {
|
|
683
|
-
results.push(bugEvidence);
|
|
684
|
-
matched.add('templates/evidence/Implementation-BugEvidence.md');
|
|
685
|
-
}
|
|
686
|
-
this.addRuleFiles([
|
|
687
|
-
'mandatory-pre-completion-reflection',
|
|
688
|
-
'agent-testing-guidelines',
|
|
689
|
-
'code-quality-and-debugging-patterns',
|
|
690
|
-
'spike-first-development',
|
|
691
|
-
'pr-workflow-completeness'
|
|
692
|
-
], results, matched);
|
|
693
|
-
}
|
|
694
|
-
// For test workflow, include test rules
|
|
695
|
-
if (workflowKey === 'test') {
|
|
696
|
-
this.addRuleFiles(['agent-testing-guidelines', 'integrity-and-test-ethics'], results, matched);
|
|
697
|
-
}
|
|
698
|
-
// New workflows related files
|
|
699
|
-
if (workflowKey === 'prototype') {
|
|
700
|
-
this.addRuleFiles(['spike-first-development'], results, matched);
|
|
701
|
-
}
|
|
702
|
-
if (workflowKey === 'marketing-strategy') {
|
|
703
|
-
const contentCreation = this.fileIndex.get('workflows/marketing/content-creation.md');
|
|
704
|
-
if (contentCreation && !matched.has('workflows/marketing/content-creation.md')) {
|
|
705
|
-
results.push(contentCreation);
|
|
706
|
-
matched.add('workflows/marketing/content-creation.md');
|
|
707
|
-
}
|
|
708
|
-
const launchChecklist = this.fileIndex.get('workflows/marketing/launch-checklist.md');
|
|
709
|
-
if (launchChecklist && !matched.has('workflows/marketing/launch-checklist.md')) {
|
|
710
|
-
results.push(launchChecklist);
|
|
711
|
-
matched.add('workflows/marketing/launch-checklist.md');
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Add rule files to results
|
|
717
|
-
*/
|
|
718
|
-
addRuleFiles(ruleNames, results, matched) {
|
|
719
|
-
for (const ruleName of ruleNames) {
|
|
720
|
-
// Check for project-specific override first
|
|
721
|
-
const customRulePath = `fraim/rules/${ruleName}.md`;
|
|
722
|
-
const genericRulePath = `rules/${ruleName}.md`;
|
|
723
|
-
const rule = this.fileIndex.get(customRulePath) || this.fileIndex.get(genericRulePath);
|
|
724
|
-
if (rule && !matched.has(rule.path)) {
|
|
725
|
-
results.push(rule);
|
|
726
|
-
matched.add(rule.path);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
setupRoutes() {
|
|
731
|
-
// Files indexing endpoint
|
|
732
|
-
this.app.get('/', (req, res) => {
|
|
733
|
-
res.json({
|
|
734
|
-
name: 'FRAIM MCP Server',
|
|
735
|
-
version: '1.0.0',
|
|
736
|
-
description: 'MCP server for .ai-agents files with context-aware workflow selection',
|
|
737
|
-
endpoints: {
|
|
738
|
-
health: '/health',
|
|
739
|
-
mcp: '/mcp',
|
|
740
|
-
resources: '/resources'
|
|
741
|
-
},
|
|
742
|
-
files_indexed: this.fileIndex.size
|
|
743
|
-
});
|
|
744
|
-
});
|
|
745
|
-
// MCP endpoint - GET for SSE connections (optional, some clients use this)
|
|
746
|
-
this.app.get('/mcp', (req, res) => {
|
|
747
|
-
// Allow GET requests without strict Accept header check
|
|
748
|
-
// Some MCP clients may use GET for health checks
|
|
749
|
-
const acceptHeader = req.headers.accept || '';
|
|
750
|
-
if (acceptHeader.includes('text/event-stream')) {
|
|
751
|
-
// SSE connection
|
|
752
|
-
res.writeHead(200, {
|
|
753
|
-
'Content-Type': 'text/event-stream',
|
|
754
|
-
'Cache-Control': 'no-cache',
|
|
755
|
-
'Connection': 'keep-alive',
|
|
756
|
-
'Access-Control-Allow-Origin': '*'
|
|
757
|
-
});
|
|
758
|
-
// Handle client disconnects to prevent server crash
|
|
759
|
-
res.on('error', (err) => {
|
|
760
|
-
console.error('⚠️ SSE stream error:', err);
|
|
761
|
-
});
|
|
762
|
-
res.write('event: endpoint\n');
|
|
763
|
-
// Use absolute URL to be safe
|
|
764
|
-
const protocol = req.protocol;
|
|
765
|
-
const host = req.get('host');
|
|
766
|
-
res.write(`data: ${protocol}://${host}/mcp\n\n`);
|
|
767
|
-
// Remove confusing "connected" message that might interfere with protocol
|
|
768
|
-
// res.write(': FRAIM MCP Server\n\n');
|
|
769
|
-
// res.write('data: connected\n\n');
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
// Regular GET request
|
|
773
|
-
res.json({
|
|
774
|
-
message: 'FRAIM MCP Server is running. Use POST /mcp for MCP requests.'
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
// Admin routes (protected by admin key)
|
|
778
|
-
this.app.use('/admin', this.adminAuthMiddleware.bind(this));
|
|
779
|
-
this.app.post('/admin/keys', async (req, res) => {
|
|
780
|
-
const { userId, orgId } = req.body;
|
|
781
|
-
if (!userId || !orgId) {
|
|
782
|
-
return res.status(400).json({ error: 'userId and orgId are required' });
|
|
783
|
-
}
|
|
784
|
-
const apiKey = this.dbService.generateApiKey(userId, orgId);
|
|
785
|
-
await this.dbService.createApiKey({ key: apiKey, userId, orgId });
|
|
786
|
-
res.json({ key: apiKey });
|
|
787
|
-
});
|
|
788
|
-
this.app.get('/admin/keys', async (req, res) => {
|
|
789
|
-
const keys = await this.dbService.listApiKeys();
|
|
790
|
-
res.json(keys);
|
|
791
|
-
});
|
|
792
|
-
this.app.post('/admin/keys/revoke', async (req, res) => {
|
|
793
|
-
const { key } = req.body;
|
|
794
|
-
if (!key) {
|
|
795
|
-
return res.status(400).json({ error: 'key is required' });
|
|
796
|
-
}
|
|
797
|
-
const success = await this.dbService.revokeApiKey(key);
|
|
798
|
-
res.json({ revoked: success });
|
|
799
|
-
});
|
|
800
|
-
// Website signup endpoint (public, no auth required)
|
|
801
|
-
this.app.post('/api/signup', async (req, res) => {
|
|
802
|
-
try {
|
|
803
|
-
const { email, company, useCase, source } = req.body;
|
|
804
|
-
if (!email) {
|
|
805
|
-
return res.status(400).json({ error: 'Email is required' });
|
|
806
|
-
}
|
|
807
|
-
// Basic email validation
|
|
808
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
809
|
-
if (!emailRegex.test(email)) {
|
|
810
|
-
return res.status(400).json({ error: 'Invalid email format' });
|
|
811
|
-
}
|
|
812
|
-
// Get client IP and user agent for analytics
|
|
813
|
-
const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
|
|
814
|
-
const userAgent = req.headers['user-agent'];
|
|
815
|
-
const signup = {
|
|
816
|
-
email: email.toLowerCase().trim(),
|
|
817
|
-
company: company?.trim() || undefined,
|
|
818
|
-
useCase: useCase || undefined,
|
|
819
|
-
source: source || 'website',
|
|
820
|
-
timestamp: new Date(),
|
|
821
|
-
ipAddress,
|
|
822
|
-
userAgent
|
|
823
|
-
};
|
|
824
|
-
await this.dbService.createWebsiteSignup(signup);
|
|
825
|
-
console.log(`✅ New website signup: ${email} (${company || 'No company'}) - ${useCase || 'No use case'}`);
|
|
826
|
-
res.json({
|
|
827
|
-
success: true,
|
|
828
|
-
message: 'Successfully joined the waitlist!'
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
console.error('❌ Website signup error:', error);
|
|
833
|
-
if (error.message === 'Email already registered') {
|
|
834
|
-
return res.status(409).json({
|
|
835
|
-
error: 'Email already registered',
|
|
836
|
-
message: 'This email is already on our waitlist.'
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
res.status(500).json({
|
|
840
|
-
error: 'Internal server error',
|
|
841
|
-
message: 'Failed to process signup. Please try again.'
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
// Sales inquiry endpoint (public, no auth required)
|
|
846
|
-
this.app.post('/api/sales', async (req, res) => {
|
|
847
|
-
try {
|
|
848
|
-
const { email, company, projectDetails, teamSize, timeline, budget, source } = req.body;
|
|
849
|
-
if (!email || !company || !projectDetails) {
|
|
850
|
-
return res.status(400).json({
|
|
851
|
-
error: 'Missing required fields',
|
|
852
|
-
message: 'Email, company, project details are required'
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
// Basic email validation
|
|
856
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
857
|
-
if (!emailRegex.test(email)) {
|
|
858
|
-
return res.status(400).json({ error: 'Invalid email format' });
|
|
859
|
-
}
|
|
860
|
-
// Get client IP and user agent for analytics
|
|
861
|
-
const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
|
|
862
|
-
const userAgent = req.headers['user-agent'];
|
|
863
|
-
const inquiry = {
|
|
864
|
-
email: email.toLowerCase().trim(),
|
|
865
|
-
company: company.trim(),
|
|
866
|
-
projectDetails: projectDetails.trim(),
|
|
867
|
-
teamSize: teamSize?.trim() || undefined,
|
|
868
|
-
timeline: timeline?.trim() || undefined,
|
|
869
|
-
budget: budget?.trim() || undefined,
|
|
870
|
-
source: source || 'website',
|
|
871
|
-
timestamp: new Date(),
|
|
872
|
-
ipAddress,
|
|
873
|
-
userAgent
|
|
874
|
-
};
|
|
875
|
-
await this.dbService.createSalesInquiry(inquiry);
|
|
876
|
-
console.log(`💼 New sales inquiry: ${email} from ${company} - Team: ${teamSize}`);
|
|
877
|
-
res.json({
|
|
878
|
-
success: true,
|
|
879
|
-
message: 'Sales inquiry submitted successfully! We\'ll be in touch soon.'
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
catch (error) {
|
|
883
|
-
console.error('❌ Sales inquiry error:', error);
|
|
884
|
-
res.status(500).json({
|
|
885
|
-
error: 'Internal server error',
|
|
886
|
-
message: 'Failed to process sales inquiry. Please try again.'
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
});
|
|
890
|
-
// Get signups endpoint (admin only)
|
|
891
|
-
this.app.get('/admin/signups', async (req, res) => {
|
|
892
|
-
try {
|
|
893
|
-
const limit = parseInt(req.query.limit) || 100;
|
|
894
|
-
const signups = await this.dbService.getWebsiteSignups(limit);
|
|
895
|
-
res.json({ signups, total: signups.length });
|
|
896
|
-
}
|
|
897
|
-
catch (error) {
|
|
898
|
-
console.error('❌ Failed to get signups:', error);
|
|
899
|
-
res.status(500).json({ error: 'Failed to retrieve signups' });
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
// Get sales inquiries endpoint (admin only)
|
|
903
|
-
this.app.get('/admin/sales', async (req, res) => {
|
|
904
|
-
try {
|
|
905
|
-
const limit = parseInt(req.query.limit) || 100;
|
|
906
|
-
const inquiries = await this.dbService.getSalesInquiries(limit);
|
|
907
|
-
res.json({ inquiries, total: inquiries.length });
|
|
908
|
-
}
|
|
909
|
-
catch (error) {
|
|
910
|
-
console.error('❌ Failed to get sales inquiries:', error);
|
|
911
|
-
res.status(500).json({ error: 'Failed to retrieve sales inquiries' });
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
// MCP endpoint - POST for actual MCP requests
|
|
915
|
-
// Handle MCP requests on both /mcp (standard) and / (easier config)
|
|
916
|
-
this.app.post(['/', '/mcp'], async (req, res) => {
|
|
917
|
-
const { jsonrpc, method, params, id } = req.body;
|
|
918
|
-
if (jsonrpc !== '2.0') {
|
|
919
|
-
return res.status(400).json({ error: 'Invalid JSON-RPC version' });
|
|
920
|
-
}
|
|
921
|
-
try {
|
|
922
|
-
let result;
|
|
923
|
-
if (method === 'initialize') {
|
|
924
|
-
result = this.handleInitialize();
|
|
925
|
-
}
|
|
926
|
-
else if (method === 'tools/list') {
|
|
927
|
-
result = this.handleListTools();
|
|
928
|
-
}
|
|
929
|
-
else if (method === 'resources/list') {
|
|
930
|
-
result = this.handleListResources();
|
|
931
|
-
}
|
|
932
|
-
else if (method === 'prompts/list') {
|
|
933
|
-
result = this.handleListPrompts();
|
|
934
|
-
}
|
|
935
|
-
else if (method === 'tools/call') {
|
|
936
|
-
// Pass API Key context to handleToolCall
|
|
937
|
-
const context = {
|
|
938
|
-
apiKey: req.apiKeyData?.key,
|
|
939
|
-
userId: req.apiKeyData?.userId
|
|
940
|
-
};
|
|
941
|
-
result = await this.handleToolCall(params, context);
|
|
942
|
-
}
|
|
943
|
-
else if (method === 'notifications/initialized') {
|
|
944
|
-
// Client initialized notification - no response needed but must not error
|
|
945
|
-
return res.json({ jsonrpc: '2.0', id: id || null, result: true });
|
|
946
|
-
}
|
|
947
|
-
else {
|
|
948
|
-
throw new Error(`Unknown method: ${method}`);
|
|
949
|
-
}
|
|
950
|
-
if (req.versionMismatch) {
|
|
951
|
-
const { client, server } = req.versionMismatch;
|
|
952
|
-
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`;
|
|
953
|
-
if (result && Array.isArray(result.content)) {
|
|
954
|
-
result.content.unshift({
|
|
955
|
-
type: 'text',
|
|
956
|
-
text: notice
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
res.json({
|
|
961
|
-
jsonrpc: '2.0',
|
|
962
|
-
result,
|
|
963
|
-
id
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
catch (error) {
|
|
967
|
-
res.status(500).json({
|
|
968
|
-
jsonrpc: '2.0',
|
|
969
|
-
error: {
|
|
970
|
-
code: -32603,
|
|
971
|
-
message: error instanceof Error ? error.message : 'Internal error'
|
|
972
|
-
},
|
|
973
|
-
id
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
handleInitialize() {
|
|
979
|
-
return {
|
|
980
|
-
protocolVersion: '2024-11-05',
|
|
981
|
-
serverInfo: {
|
|
982
|
-
name: 'fraim-mcp-server',
|
|
983
|
-
version: this.serverVersion
|
|
984
|
-
},
|
|
985
|
-
capabilities: {
|
|
986
|
-
tools: {},
|
|
987
|
-
resources: {}
|
|
988
|
-
}
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
handleListTools() {
|
|
992
|
-
return {
|
|
993
|
-
tools: [
|
|
994
|
-
{
|
|
995
|
-
name: 'get_fraim_init',
|
|
996
|
-
description: `Initialize FRAIM with all available rules, workflows, and usage instructions.
|
|
997
|
-
|
|
998
|
-
This should be the FIRST call to FRAIM in any new session.
|
|
999
|
-
Returns:
|
|
1000
|
-
- All rules (organized by category)
|
|
1001
|
-
- All workflows (organized by category)
|
|
1002
|
-
- Usage instructions and best practices
|
|
1003
|
-
- Workflow execution guidance
|
|
1004
|
-
|
|
1005
|
-
Call this before fetching any workflow or file to understand the system.`,
|
|
1006
|
-
inputSchema: {
|
|
1007
|
-
type: 'object',
|
|
1008
|
-
properties: {},
|
|
1009
|
-
required: []
|
|
1010
|
-
}
|
|
1011
|
-
},
|
|
1012
|
-
{
|
|
1013
|
-
name: 'fraim_connect',
|
|
1014
|
-
description: `🚨 REQUIRED FIRST CALL: Handshake to start a FRAIM session. MUST be called at the start of every session.
|
|
1015
|
-
|
|
1016
|
-
⚠️ CRITICAL: If you get a "Session Not Started" error from any FRAIM tool, call this tool first.
|
|
1017
|
-
|
|
1018
|
-
Registers your environment (machine/repo/agent) for telemetry and unlocks other tools.
|
|
1019
|
-
|
|
1020
|
-
**Agent Information**: REQUIRED - Provide your actual agent name and model (validated against known agents).
|
|
1021
|
-
**Machine Information**: Use os.hostname() and process.platform - required for telemetry.
|
|
1022
|
-
**Repository Information**: Extract and send this information from .fraim/config.json.
|
|
1023
|
-
|
|
1024
|
-
Example call:
|
|
1025
|
-
{
|
|
1026
|
-
"agent": {"name": "Claude", "model": "claude-3.5-sonnet"},
|
|
1027
|
-
"machine": {"hostname": "my-machine", "platform": "darwin"},
|
|
1028
|
-
"repo": {"url": "https://github.com/owner/repo", "owner": "owner", "name": "repo"}
|
|
1029
|
-
}`,
|
|
1030
|
-
inputSchema: {
|
|
1031
|
-
type: 'object',
|
|
1032
|
-
properties: {
|
|
1033
|
-
agent: {
|
|
1034
|
-
type: 'object',
|
|
1035
|
-
description: 'Agent identification and capabilities',
|
|
1036
|
-
properties: {
|
|
1037
|
-
name: {
|
|
1038
|
-
type: 'string',
|
|
1039
|
-
description: 'Agent name (e.g., "Claude", "Cursor", "Kiro", "Windsurf", "Antigravity")',
|
|
1040
|
-
examples: ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity']
|
|
1041
|
-
},
|
|
1042
|
-
model: {
|
|
1043
|
-
type: 'string',
|
|
1044
|
-
description: 'Model name/version (e.g., "claude-3.5-sonnet", "gpt-4", "cursor-small")',
|
|
1045
|
-
examples: ['claude-3.5-sonnet', 'gpt-4', 'cursor-small', 'kiro-agent']
|
|
1046
|
-
},
|
|
1047
|
-
version: {
|
|
1048
|
-
type: 'string',
|
|
1049
|
-
description: 'Agent version if available',
|
|
1050
|
-
examples: ['1.0.0', '2024.12.1']
|
|
1051
|
-
}
|
|
1052
|
-
},
|
|
1053
|
-
required: ['name', 'model'],
|
|
1054
|
-
additionalProperties: true
|
|
1055
|
-
},
|
|
1056
|
-
machine: {
|
|
1057
|
-
type: 'object',
|
|
1058
|
-
description: 'Machine specs - use os.hostname(), process.platform, os.totalmem(), os.cpus().length',
|
|
1059
|
-
properties: {
|
|
1060
|
-
hostname: {
|
|
1061
|
-
type: 'string',
|
|
1062
|
-
description: 'Machine hostname from os.hostname()'
|
|
1063
|
-
},
|
|
1064
|
-
platform: {
|
|
1065
|
-
type: 'string',
|
|
1066
|
-
description: 'Platform from process.platform (win32, darwin, linux)'
|
|
1067
|
-
},
|
|
1068
|
-
memory: {
|
|
1069
|
-
type: 'number',
|
|
1070
|
-
description: 'Total memory in bytes from os.totalmem()'
|
|
1071
|
-
},
|
|
1072
|
-
cpus: {
|
|
1073
|
-
type: 'number',
|
|
1074
|
-
description: 'CPU count from os.cpus().length'
|
|
1075
|
-
}
|
|
1076
|
-
},
|
|
1077
|
-
required: ['hostname', 'platform'],
|
|
1078
|
-
additionalProperties: true
|
|
1079
|
-
},
|
|
1080
|
-
repo: {
|
|
1081
|
-
type: 'object',
|
|
1082
|
-
description: 'Repository context - use git remote -v or check .git/config for URL',
|
|
1083
|
-
properties: {
|
|
1084
|
-
url: {
|
|
1085
|
-
type: 'string',
|
|
1086
|
-
description: 'Git repository URL from git remote -v'
|
|
1087
|
-
},
|
|
1088
|
-
owner: {
|
|
1089
|
-
type: 'string',
|
|
1090
|
-
description: 'Repository owner (extracted from URL)'
|
|
1091
|
-
},
|
|
1092
|
-
name: {
|
|
1093
|
-
type: 'string',
|
|
1094
|
-
description: 'Repository name (extracted from URL)'
|
|
1095
|
-
},
|
|
1096
|
-
branch: {
|
|
1097
|
-
type: 'string',
|
|
1098
|
-
description: 'Current branch from git branch --show-current'
|
|
1099
|
-
}
|
|
1100
|
-
},
|
|
1101
|
-
required: ['url'],
|
|
1102
|
-
additionalProperties: true
|
|
1103
|
-
}
|
|
1104
|
-
},
|
|
1105
|
-
required: ['agent', 'machine', 'repo']
|
|
1106
|
-
}
|
|
1107
|
-
},
|
|
1108
|
-
{
|
|
1109
|
-
name: 'get_fraim_file',
|
|
1110
|
-
description: `Get a specific file from FRAIM by path.
|
|
1111
|
-
|
|
1112
|
-
Examples:
|
|
1113
|
-
- get_fraim_file({ path: "rules/communication.md" })
|
|
1114
|
-
- get_fraim_file({ path: "workflows/spec.md" })
|
|
1115
|
-
- get_fraim_file({ path: "templates/specs/FEATURESPEC-TEMPLATE.md" })
|
|
1116
|
-
|
|
1117
|
-
NOTE: If a .fraim override exists for a rule, workflow, template, or script,
|
|
1118
|
-
FRAIM will return the project-specific version instead of the generic one.`,
|
|
1119
|
-
inputSchema: {
|
|
1120
|
-
type: 'object',
|
|
1121
|
-
properties: {
|
|
1122
|
-
path: {
|
|
1123
|
-
type: 'string',
|
|
1124
|
-
description: 'Path to the file (e.g., rules/communication.md, workflows/spec.md)'
|
|
1125
|
-
}
|
|
1126
|
-
},
|
|
1127
|
-
required: ['path']
|
|
1128
|
-
}
|
|
1129
|
-
},
|
|
1130
|
-
{
|
|
1131
|
-
name: 'get_fraim_workflow',
|
|
1132
|
-
description: `Get a workflow file by name. Returns the workflow content.
|
|
1133
|
-
|
|
1134
|
-
Examples:
|
|
1135
|
-
- get_fraim_workflow({ workflow: "spec" })
|
|
1136
|
-
- get_fraim_workflow({ workflow: "design" })
|
|
1137
|
-
- get_fraim_workflow({ workflow: "implement" })
|
|
1138
|
-
|
|
1139
|
-
After receiving the workflow, follow any ACTION items to fetch related files.
|
|
1140
|
-
|
|
1141
|
-
Common workflows:
|
|
1142
|
-
- "prep-issue" - Prepare environment for working on an issue
|
|
1143
|
-
- "spec" - Write feature specifications
|
|
1144
|
-
- "design" - Create technical designs
|
|
1145
|
-
- "implement" - Implement features / fixes
|
|
1146
|
-
- "test" - Write tests
|
|
1147
|
-
- "resolve" - Merge to master
|
|
1148
|
-
|
|
1149
|
-
Call get_fraim_init first to see all available workflows organized by category.`,
|
|
1150
|
-
inputSchema: {
|
|
1151
|
-
type: 'object',
|
|
1152
|
-
properties: {
|
|
1153
|
-
workflow: {
|
|
1154
|
-
type: 'string',
|
|
1155
|
-
description: 'Workflow name (e.g., "spec", "design", "implement", "prep-issue")'
|
|
1156
|
-
}
|
|
1157
|
-
},
|
|
1158
|
-
required: ['workflow']
|
|
1159
|
-
}
|
|
1160
|
-
},
|
|
1161
|
-
{
|
|
1162
|
-
name: 'list_fraim_workflows',
|
|
1163
|
-
description: `List all available workflows organized by category.
|
|
1164
|
-
|
|
1165
|
-
Returns workflows grouped by:
|
|
1166
|
-
- Product Development (prep-issue, spec, design, implement, test, resolve)
|
|
1167
|
-
- Customer Development (linkedin-outreach, customer-interview, etc.)
|
|
1168
|
-
- Business Development (partnership-outreach, investor-pitch, etc.)
|
|
1169
|
-
|
|
1170
|
-
Use this to discover which workflow to call with get_fraim_workflow.`,
|
|
1171
|
-
inputSchema: {
|
|
1172
|
-
type: 'object',
|
|
1173
|
-
properties: {},
|
|
1174
|
-
required: []
|
|
1175
|
-
}
|
|
1176
|
-
},
|
|
1177
|
-
{
|
|
1178
|
-
name: 'fraim_get_local_config',
|
|
1179
|
-
description: 'Get instructions for reading and using local .fraim/config.json from your workspace. Use this to understand project-specific settings and replace template variables.',
|
|
1180
|
-
inputSchema: {
|
|
1181
|
-
type: 'object',
|
|
1182
|
-
properties: {},
|
|
1183
|
-
required: []
|
|
1184
|
-
}
|
|
1185
|
-
},
|
|
1186
|
-
{
|
|
1187
|
-
name: 'file_issue',
|
|
1188
|
-
description: `Create a GitHub issue in the FRAIM repository.
|
|
1189
|
-
|
|
1190
|
-
Use this tool when you need to report a bug, request a feature, or track a task.
|
|
1191
|
-
Supports dry-run mode to preview the operation.`,
|
|
1192
|
-
inputSchema: {
|
|
1193
|
-
type: 'object',
|
|
1194
|
-
properties: {
|
|
1195
|
-
title: {
|
|
1196
|
-
type: 'string',
|
|
1197
|
-
description: 'Title of the issue'
|
|
1198
|
-
},
|
|
1199
|
-
body: {
|
|
1200
|
-
type: 'string',
|
|
1201
|
-
description: 'Body/Content of the issue'
|
|
1202
|
-
},
|
|
1203
|
-
labels: {
|
|
1204
|
-
type: 'array',
|
|
1205
|
-
items: { type: 'string' },
|
|
1206
|
-
description: 'List of labels to apply'
|
|
1207
|
-
},
|
|
1208
|
-
dryRun: {
|
|
1209
|
-
type: 'boolean',
|
|
1210
|
-
description: 'If true, simulates the creation without actually notifying GitHub'
|
|
1211
|
-
}
|
|
1212
|
-
},
|
|
1213
|
-
required: ['title', 'body']
|
|
1214
|
-
}
|
|
1215
|
-
},
|
|
1216
|
-
{
|
|
1217
|
-
name: 'seekCoachingOnNextStep',
|
|
1218
|
-
description: `Get coaching on what to do next in your implementation workflow.
|
|
1219
|
-
|
|
1220
|
-
The AI Coach provides phase-specific instructions based on your current progress:
|
|
1221
|
-
- If starting: Get initial phase instructions (usually scoping)
|
|
1222
|
-
- If completed phase: Provide evidence for validation, get next phase instructions
|
|
1223
|
-
- If stuck/incomplete: Get guidance to continue current phase
|
|
1224
|
-
|
|
1225
|
-
This is your single point of contact for workflow guidance with evidence validation.`,
|
|
1226
|
-
inputSchema: {
|
|
1227
|
-
type: 'object',
|
|
1228
|
-
properties: {
|
|
1229
|
-
workflowType: {
|
|
1230
|
-
type: 'string',
|
|
1231
|
-
description: 'Type of workflow you are following',
|
|
1232
|
-
enum: ['implement', 'spec', 'design', 'test']
|
|
1233
|
-
},
|
|
1234
|
-
issueNumber: {
|
|
1235
|
-
type: 'string',
|
|
1236
|
-
description: 'Issue number you are working on'
|
|
1237
|
-
},
|
|
1238
|
-
currentPhase: {
|
|
1239
|
-
type: 'string',
|
|
1240
|
-
description: 'Phase you just completed or are working on (e.g., "scoping", "repro", "code")'
|
|
1241
|
-
},
|
|
1242
|
-
status: {
|
|
1243
|
-
type: 'string',
|
|
1244
|
-
description: 'Status of your current phase',
|
|
1245
|
-
enum: ['starting', 'complete', 'incomplete', 'failure']
|
|
1246
|
-
},
|
|
1247
|
-
findings: {
|
|
1248
|
-
type: 'object',
|
|
1249
|
-
description: 'Your findings/results from the current phase (varies by phase)',
|
|
1250
|
-
properties: {
|
|
1251
|
-
uncertainties: {
|
|
1252
|
-
type: 'array',
|
|
1253
|
-
items: { type: 'string' },
|
|
1254
|
-
description: 'Any unclear aspects that need clarification (used for help messages)'
|
|
1255
|
-
}
|
|
1256
|
-
},
|
|
1257
|
-
additionalProperties: true
|
|
1258
|
-
},
|
|
1259
|
-
evidence: {
|
|
1260
|
-
type: 'object',
|
|
1261
|
-
description: 'Evidence submission for validation (varies by phase - can contain any relevant evidence)',
|
|
1262
|
-
additionalProperties: true
|
|
1263
|
-
}
|
|
1264
|
-
},
|
|
1265
|
-
required: ['workflowType', 'issueNumber', 'currentPhase', 'status']
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
]
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
handleListResources() {
|
|
1272
|
-
return {
|
|
1273
|
-
resources: []
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
handleListPrompts() {
|
|
1277
|
-
return {
|
|
1278
|
-
prompts: []
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
async handleToolCall(params, context = {}) {
|
|
1282
|
-
const { name: toolName, arguments: toolArgs } = params;
|
|
1283
|
-
console.log(`🔧 MCP Server: handleToolCall called with tool: ${toolName}, args:`, JSON.stringify(toolArgs, null, 2));
|
|
1284
|
-
switch (toolName) {
|
|
1285
|
-
case 'get_fraim_init':
|
|
1286
|
-
return await this.handleGetInit();
|
|
1287
|
-
case 'get_fraim_file':
|
|
1288
|
-
return await this.handleGetFile(toolArgs.path);
|
|
1289
|
-
case 'get_fraim_workflow':
|
|
1290
|
-
return await this.handleGetWorkflow(toolArgs.workflow, context.apiKey);
|
|
1291
|
-
case 'list_fraim_workflows':
|
|
1292
|
-
return await this.handleListWorkflows();
|
|
1293
|
-
case 'fraim_get_local_config':
|
|
1294
|
-
return await this.handleGetLocalConfig();
|
|
1295
|
-
case 'file_issue':
|
|
1296
|
-
const result = await (0, issues_1.fileFraimIssue)(toolArgs);
|
|
1297
|
-
return {
|
|
1298
|
-
content: [{
|
|
1299
|
-
type: 'text',
|
|
1300
|
-
text: JSON.stringify(result, null, 2)
|
|
1301
|
-
}]
|
|
1302
|
-
};
|
|
1303
|
-
case 'fraim_connect':
|
|
1304
|
-
return await this.handleFraimConnect(toolArgs, context.apiKey, context.userId);
|
|
1305
|
-
case 'seekCoachingOnNextStep':
|
|
1306
|
-
return await this.handleSeekCoachingOnNextStep(toolArgs);
|
|
1307
|
-
default:
|
|
1308
|
-
throw new Error(`Unknown tool: ${toolName} `);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
async handleGetInit() {
|
|
1312
|
-
// Get all workflows organized by category
|
|
1313
|
-
const workflows = Array.from(this.fileIndex.values()).filter(f => f.type === 'workflow');
|
|
1314
|
-
// Organize workflows by category
|
|
1315
|
-
const categorized = {};
|
|
1316
|
-
for (const wf of workflows) {
|
|
1317
|
-
const category = wf.category || 'Other';
|
|
1318
|
-
if (!categorized[category]) {
|
|
1319
|
-
categorized[category] = [];
|
|
1320
|
-
}
|
|
1321
|
-
// Extract workflow name without .md extension
|
|
1322
|
-
const workflowName = wf.name.replace('.md', '');
|
|
1323
|
-
categorized[category].push({
|
|
1324
|
-
name: workflowName,
|
|
1325
|
-
path: wf.path,
|
|
1326
|
-
description: this.getWorkflowDescription(workflowName)
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
// Build initialization content
|
|
1330
|
-
let initContent = `# FRAIM - Your Development Workflow Assistant\n\n`;
|
|
1331
|
-
initContent += `## How to Use FRAIM\n\n`;
|
|
1332
|
-
initContent += `When the user says: \n`;
|
|
1333
|
-
initContent += `- "I need to prototype a PoC" → Call \`get_fraim_workflow({ workflow: "prototype" })\`\n`;
|
|
1334
|
-
initContent += `- "I want to write a spec" → Call \`get_fraim_workflow({ workflow: "spec" })\`\n`;
|
|
1335
|
-
initContent += `- "I need to design X" → Call \`get_fraim_workflow({ workflow: "design" })\`\n`;
|
|
1336
|
-
initContent += `- "Implement issue #123" → Call \`get_fraim_workflow({ workflow: "prep-issue" })\`, then \`implement\`\n`;
|
|
1337
|
-
initContent += `- "Write tests" → Call \`get_fraim_workflow({ workflow: "test" })\`\n`;
|
|
1338
|
-
initContent += `- "Marketing/Launch a feature" → Call \`get_fraim_workflow({ workflow: "marketing-strategy" })\`\n`;
|
|
1339
|
-
initContent += `- "Merge this PR" → Call \`get_fraim_workflow({ workflow: "resolve" })\`\n\n`;
|
|
1340
|
-
initContent += `## Available Workflows\n\n`;
|
|
1341
|
-
for (const [category, wfs] of Object.entries(categorized)) {
|
|
1342
|
-
initContent += `### ${category}\n\n`;
|
|
1343
|
-
for (const wf of wfs) {
|
|
1344
|
-
initContent += `- **${wf.name}**: ${wf.description}\n`;
|
|
1345
|
-
initContent += ` - Call: \`get_fraim_workflow({ workflow: "${wf.name}" })\`\n\n`;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
initContent += `## Workflow Execution Pattern\n\n`;
|
|
1349
|
-
initContent += `1. Call \`get_fraim_workflow({ workflow: "<name>" })\` to get the workflow file\n`;
|
|
1350
|
-
initContent += `2. The workflow will contain **ACTION** items like:\n`;
|
|
1351
|
-
initContent += ` - \`ACTION: Call get_fraim_file({ path: "rules/X.md" })\`\n`;
|
|
1352
|
-
initContent += ` - \`ACTION: Call get_fraim_file({ path: "templates/Y.md" })\`\n`;
|
|
1353
|
-
initContent += `3. Execute each ACTION item to fetch referenced files\n`;
|
|
1354
|
-
initContent += `4. Follow the workflow steps with the fetched context\n\n`;
|
|
1355
|
-
initContent += `## Important Notes\n\n`;
|
|
1356
|
-
initContent += `- Files are NOT sent automatically - you must fetch them via ACTION items\n`;
|
|
1357
|
-
initContent += `- This keeps context minimal and focused\n`;
|
|
1358
|
-
initContent += `- Use \`get_fraim_file({ path: "..." })\` to fetch any referenced file\n\n`;
|
|
1359
|
-
initContent += `## 📊 Proactive Codebase Analysis\n\n`;
|
|
1360
|
-
initContent += `Would you like a quick analysis of your codebase for recommendations on test coverage, quality, and architecture? If yes, you can run the following tools:\n`;
|
|
1361
|
-
initContent += `- **Test Coverage**: Run \`npm run validate-coverage\` and report gaps.\n`;
|
|
1362
|
-
initContent += `- **Code Quality**: Run \`./.ai-agents/scripts/code-quality-check.sh\` to identify anti-patterns.\n`;
|
|
1363
|
-
initContent += `If you find issues, offer to create GitHub bugs to address them.\n\n`;
|
|
1364
|
-
initContent += `## 🚀 FRAIM Value Proposition\n\n`;
|
|
1365
|
-
initContent += `**For Engineers**:\n`;
|
|
1366
|
-
initContent += `- Deterministic coding via BAML and clear rule enforcement.\n`;
|
|
1367
|
-
initContent += `- Automated repository setup and indexing.\n`;
|
|
1368
|
-
initContent += `- Continuous learning from project retrospectives.\n\n`;
|
|
1369
|
-
initContent += `**For Product/Marketing**:\n`;
|
|
1370
|
-
initContent += `- End-to-end lifecycle support from rapid Prototyping to Go-To-Market.\n`;
|
|
1371
|
-
initContent += `- Spec-first development with high-fidelity HTML/CSS mocks.\n`;
|
|
1372
|
-
initContent += `- Automated customer research and outreach workflows.\n`;
|
|
1373
|
-
initContent += `- Consistent documentation and evidence gathering.\n`;
|
|
1374
|
-
return {
|
|
1375
|
-
content: [
|
|
1376
|
-
{
|
|
1377
|
-
type: 'text',
|
|
1378
|
-
text: initContent
|
|
1379
|
-
}
|
|
1380
|
-
]
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
/**
|
|
1384
|
-
* Get description for a workflow
|
|
1385
|
-
*/
|
|
1386
|
-
getWorkflowDescription(workflowName) {
|
|
1387
|
-
const descriptions = {
|
|
1388
|
-
// Core Development Workflows
|
|
1389
|
-
'prep-issue': 'Prepare environment for working on an issue',
|
|
1390
|
-
'spec': 'Write feature specifications',
|
|
1391
|
-
'design': 'Create technical designs',
|
|
1392
|
-
'implement': 'Implement features/fixes',
|
|
1393
|
-
'test': 'Write tests',
|
|
1394
|
-
'resolve': 'Merge to master',
|
|
1395
|
-
'retrospect': 'Conduct retrospectives after task completion',
|
|
1396
|
-
// Review Workflows
|
|
1397
|
-
'review-implementation-vs-design-spec': 'Review implementation against design spec',
|
|
1398
|
-
'review-implementation-vs-feature-spec': 'Review implementation against feature spec',
|
|
1399
|
-
// Quality Assurance
|
|
1400
|
-
'browser-validation': 'Validate in browser',
|
|
1401
|
-
'iterative-improvement-cycle': 'Iterative improvement cycle',
|
|
1402
|
-
// Customer Development
|
|
1403
|
-
'interview-preparation': 'Prepare for customer interviews',
|
|
1404
|
-
'linkedin-outreach': 'LinkedIn customer outreach',
|
|
1405
|
-
'insight-analysis': 'Analyze customer insights',
|
|
1406
|
-
'insight-triage': 'Triage customer insights',
|
|
1407
|
-
'strategic-brainstorming': 'Strategic brainstorming sessions',
|
|
1408
|
-
'weekly-newsletter': 'Create weekly newsletter',
|
|
1409
|
-
'thank-customers': 'Thank customers workflow',
|
|
1410
|
-
// Replicate / Reverse Engineering
|
|
1411
|
-
'visual-analysis': 'Analyze visual designs',
|
|
1412
|
-
'website-discovery-analysis': 'Discover and analyze websites',
|
|
1413
|
-
'use-case-extraction': 'Extract use cases',
|
|
1414
|
-
're-implementation-strategy': 'Plan re-implementation strategy',
|
|
1415
|
-
// New Lifecycle Workflows
|
|
1416
|
-
'prototype': 'Quickly explore ideas and prove concepts (spike code)',
|
|
1417
|
-
'marketing-strategy': 'Define value prop, target audience, and launch plans',
|
|
1418
|
-
'content-creation': 'Generate high-quality blog posts, social media, and docs',
|
|
1419
|
-
'launch-checklist': 'Final technical and marketing sign-off before release'
|
|
1420
|
-
};
|
|
1421
|
-
return descriptions[workflowName] || 'Workflow';
|
|
1422
|
-
}
|
|
1423
|
-
async handleGetFile(path) {
|
|
1424
|
-
const metadata = this.fileIndex.get(path);
|
|
1425
|
-
if (!metadata) {
|
|
1426
|
-
throw new Error(`File not found: ${path}. Use list_fraim_files to see available files.`);
|
|
1427
|
-
}
|
|
1428
|
-
try {
|
|
1429
|
-
let content = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
|
|
1430
|
-
// Check if file contains template variables
|
|
1431
|
-
const hasTemplateVars = /\{\{config\.[^}]+\}\}/.test(content);
|
|
1432
|
-
// Don't process template variables here - let agent do it with local config
|
|
1433
|
-
// content = processTemplateVariables(content, this.config);
|
|
1434
|
-
let header = `# ${metadata.name}\n\n`;
|
|
1435
|
-
header += `**Path:** ${path}\n`;
|
|
1436
|
-
header += `**Type:** ${metadata.type}\n`;
|
|
1437
|
-
if (metadata.category) {
|
|
1438
|
-
header += `**Category:** ${metadata.category}\n`;
|
|
1439
|
-
}
|
|
1440
|
-
if (hasTemplateVars) {
|
|
1441
|
-
header += `\n## ⚠️ Template Variables Detected\n\n`;
|
|
1442
|
-
header += `This file contains template variables like \`{{config.project.name}}\`.\n\n`;
|
|
1443
|
-
header += `**You must replace these with values from your local \`.fraim/config.json\`:**\n\n`;
|
|
1444
|
-
header += `1. Read \`.fraim/config.json\` from your workspace\n`;
|
|
1445
|
-
header += `2. Find the config path (e.g., for \`{{config.project.name}}\`, read \`config.project.name\`)\n`;
|
|
1446
|
-
header += `3. Replace the variable with the actual value\n\n`;
|
|
1447
|
-
header += `**Example:**\n`;
|
|
1448
|
-
header += `- Template: "Project: {{config.project.name}}"`;
|
|
1449
|
-
header += `- Your config: \`{ "project": { "name": "My App" } }\`\n`;
|
|
1450
|
-
header += `- Result: "Project: My App"\n\n`;
|
|
1451
|
-
header += `Use \`fraim_get_local_config\` tool for detailed instructions.\n\n`;
|
|
1452
|
-
}
|
|
1453
|
-
if (metadata.type === 'script') {
|
|
1454
|
-
header += `\n## 🛡️ Ephemeral Execution Mandatory (IP Protection)\n\n`;
|
|
1455
|
-
header += `This script MUST be executed ephemerally:\n\n`;
|
|
1456
|
-
header += `1. **Fetch** this content from the MCP.\n`;
|
|
1457
|
-
header += `2. **Save** to a system temporary directory (e.g., \`/tmp\` or \`%TEMP%\`), NOT the local repository.\n`;
|
|
1458
|
-
header += `3. **Execute** from the temporary location.\n`;
|
|
1459
|
-
header += `4. **Delete** the temporary file immediately after execution.\n\n`;
|
|
1460
|
-
header += `Refer to \`rules/ephemeral-execution.md\` for the full policy.\n\n`;
|
|
1461
|
-
}
|
|
1462
|
-
header += `---\n\n`;
|
|
1463
|
-
return {
|
|
1464
|
-
content: [
|
|
1465
|
-
{
|
|
1466
|
-
type: 'text',
|
|
1467
|
-
text: header + content
|
|
1468
|
-
}
|
|
1469
|
-
]
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
catch (error) {
|
|
1473
|
-
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
async handleGetWorkflow(workflowName, apiKey) {
|
|
1477
|
-
// Normalize workflow name (remove .md if present)
|
|
1478
|
-
const normalizedName = workflowName.replace(/\.md$/, '');
|
|
1479
|
-
// Try to find the workflow file
|
|
1480
|
-
const workflowPath = `workflows/${normalizedName}.md`;
|
|
1481
|
-
const metadata = this.fileIndex.get(workflowPath);
|
|
1482
|
-
if (!metadata) {
|
|
1483
|
-
// Try alternative category paths - dynamically discover categories from file index
|
|
1484
|
-
const workflowCategories = new Set();
|
|
1485
|
-
// Extract all unique workflow categories from the file index
|
|
1486
|
-
for (const [path, fileMetadata] of this.fileIndex) {
|
|
1487
|
-
if (fileMetadata.type === 'workflow' && path.startsWith('workflows/') && path.includes('/')) {
|
|
1488
|
-
const pathParts = path.split('/');
|
|
1489
|
-
if (pathParts.length >= 3) { // workflows/category/file.md
|
|
1490
|
-
workflowCategories.add(pathParts[1]);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
// Search through all discovered categories
|
|
1495
|
-
for (const cat of workflowCategories) {
|
|
1496
|
-
const altPath = `workflows/${cat}/${normalizedName}.md`;
|
|
1497
|
-
const altMetadata = this.fileIndex.get(altPath);
|
|
1498
|
-
if (altMetadata) {
|
|
1499
|
-
return this.returnWorkflowFile(altMetadata, apiKey);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
// Workflow not found
|
|
1503
|
-
const availableWorkflows = Array.from(this.fileIndex.values())
|
|
1504
|
-
.filter(f => f.type === 'workflow')
|
|
1505
|
-
.map(f => f.name.replace('.md', ''));
|
|
1506
|
-
return {
|
|
1507
|
-
content: [{
|
|
1508
|
-
type: 'text',
|
|
1509
|
-
text: `Workflow "${workflowName}" not found.\n\nAvailable workflows:\n${availableWorkflows.map(w => `- ${w}`).join('\n')}\n\nUse list_fraim_workflows to see all workflows organized by category.`
|
|
1510
|
-
}]
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
return this.returnWorkflowFile(metadata, apiKey);
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Return a workflow file with its content
|
|
1517
|
-
*/
|
|
1518
|
-
async returnWorkflowFile(metadata, apiKey) {
|
|
1519
|
-
try {
|
|
1520
|
-
const rawContent = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
|
|
1521
|
-
// Get repository info from session if available
|
|
1522
|
-
let repositoryInfo = null;
|
|
1523
|
-
if (apiKey) {
|
|
1524
|
-
try {
|
|
1525
|
-
const session = await this.dbService.getActiveSessionByApiKey(apiKey);
|
|
1526
|
-
if (session?.repo) {
|
|
1527
|
-
repositoryInfo = {
|
|
1528
|
-
owner: session.repo.owner,
|
|
1529
|
-
name: session.repo.name,
|
|
1530
|
-
organization: session.repo.organization,
|
|
1531
|
-
project: session.repo.project,
|
|
1532
|
-
url: session.repo.url,
|
|
1533
|
-
provider: this.detectProviderFromUrl(session.repo.url)
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
catch (e) {
|
|
1538
|
-
// Ignore session lookup errors, continue without repository info
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
// Process templates with provider-specific actions
|
|
1542
|
-
const templateEngine = (0, template_processor_1.getTemplateEngine)(this.registryPath);
|
|
1543
|
-
const processedContent = templateEngine.processWorkflow(rawContent, repositoryInfo);
|
|
1544
|
-
// Build response
|
|
1545
|
-
let response = `# Workflow: ${metadata.name.replace('.md', '')}\n\n`;
|
|
1546
|
-
response += `**Path:** \`${metadata.path}\`\n`;
|
|
1547
|
-
if (metadata.category) {
|
|
1548
|
-
response += `**Category:** ${metadata.category}\n`;
|
|
1549
|
-
}
|
|
1550
|
-
// Add provider info if available
|
|
1551
|
-
const provider = repositoryInfo?.provider || 'GITHUB';
|
|
1552
|
-
response += `**Platform:** ${provider.toUpperCase()}\n`;
|
|
1553
|
-
response += `\n---\n\n`;
|
|
1554
|
-
response += processedContent;
|
|
1555
|
-
return {
|
|
1556
|
-
content: [{
|
|
1557
|
-
type: 'text',
|
|
1558
|
-
text: response
|
|
1559
|
-
}]
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
catch (error) {
|
|
1563
|
-
throw new Error(`Failed to read workflow: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* List all workflows organized by category
|
|
1568
|
-
*/
|
|
1569
|
-
async handleListWorkflows() {
|
|
1570
|
-
const workflows = Array.from(this.fileIndex.values()).filter(f => f.type === 'workflow');
|
|
1571
|
-
// Organize by category
|
|
1572
|
-
const categorized = {};
|
|
1573
|
-
for (const wf of workflows) {
|
|
1574
|
-
const category = wf.category || 'Other';
|
|
1575
|
-
if (!categorized[category]) {
|
|
1576
|
-
categorized[category] = [];
|
|
1577
|
-
}
|
|
1578
|
-
const workflowName = wf.name.replace('.md', '');
|
|
1579
|
-
categorized[category].push({
|
|
1580
|
-
name: workflowName,
|
|
1581
|
-
path: wf.path,
|
|
1582
|
-
description: this.getWorkflowDescription(workflowName)
|
|
1583
|
-
});
|
|
1584
|
-
}
|
|
1585
|
-
// Build response - cleaner format without file paths
|
|
1586
|
-
let response = `# Available FRAIM Workflows\n\n`;
|
|
1587
|
-
for (const [category, wfs] of Object.entries(categorized)) {
|
|
1588
|
-
response += `## ${category}\n\n`;
|
|
1589
|
-
for (const wf of wfs) {
|
|
1590
|
-
response += `- **${wf.name}**: ${wf.description}\n`;
|
|
1591
|
-
}
|
|
1592
|
-
response += `\n`;
|
|
1593
|
-
}
|
|
1594
|
-
response += `## How to Use\n\n`;
|
|
1595
|
-
response += `Call \`get_fraim_workflow({ workflow: "<name>" })\` with the workflow name.\n`;
|
|
1596
|
-
response += `Example: \`get_fraim_workflow({ workflow: "spec" })\`\n`;
|
|
1597
|
-
return {
|
|
1598
|
-
content: [{
|
|
1599
|
-
type: 'text',
|
|
1600
|
-
text: response
|
|
1601
|
-
}]
|
|
1602
|
-
};
|
|
1603
|
-
}
|
|
1604
|
-
async handleListFiles(type, category) {
|
|
1605
|
-
let files = Array.from(this.fileIndex.values());
|
|
1606
|
-
if (type !== 'all') {
|
|
1607
|
-
files = files.filter(f => f.type === type);
|
|
1608
|
-
}
|
|
1609
|
-
if (category) {
|
|
1610
|
-
files = files.filter(f => f.category === category);
|
|
1611
|
-
}
|
|
1612
|
-
// Sort by type, then by path
|
|
1613
|
-
files.sort((a, b) => {
|
|
1614
|
-
if (a.type !== b.type) {
|
|
1615
|
-
return a.type.localeCompare(b.type);
|
|
1616
|
-
}
|
|
1617
|
-
return a.path.localeCompare(b.path);
|
|
1618
|
-
});
|
|
1619
|
-
const filesList = files.map(f => {
|
|
1620
|
-
return `- **${f.type}**${f.category ? ` (${f.category})` : ''}: \`${f.path}\``;
|
|
1621
|
-
}).join('\n');
|
|
1622
|
-
return {
|
|
1623
|
-
content: [
|
|
1624
|
-
{
|
|
1625
|
-
type: 'text',
|
|
1626
|
-
text: `# FRAIM Files\n\n**Filter:** ${type}${category ? `, category: ${category}` : ''}\n\n**Total:** ${files.length} file(s)\n\n${filesList}`
|
|
1627
|
-
}
|
|
1628
|
-
]
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1631
|
-
async handleGetLocalConfig() {
|
|
1632
|
-
const instructions = `# Using Local .fraim/config.json
|
|
1633
|
-
|
|
1634
|
-
## Overview
|
|
1635
|
-
|
|
1636
|
-
FRAIM workflows and templates are generic and served from the cloud. However, **you must use your local project configuration** from \`.fraim/config.json\` in your workspace to customize them for your specific project.
|
|
1637
|
-
|
|
1638
|
-
## How to Read Local Config
|
|
1639
|
-
|
|
1640
|
-
1. **Locate the Config File**
|
|
1641
|
-
- Path: \`.fraim/config.json\` in your current workspace directory
|
|
1642
|
-
- If it doesn't exist, use the \`fraim_setup\` tool to create it
|
|
1643
|
-
|
|
1644
|
-
2. **Read the Config**
|
|
1645
|
-
- Use your file reading tools to read \`.fraim/config.json\`
|
|
1646
|
-
- Parse it as JSON
|
|
1647
|
-
- Access nested values using dot notation (e.g., \`config.project.name\`)
|
|
1648
|
-
|
|
1649
|
-
3. **Use Config Values**
|
|
1650
|
-
- Replace template variables like \`{{config.project.name}}\` with actual values
|
|
1651
|
-
- Use config values to customize commands, file paths, and workflows
|
|
1652
|
-
- Example: If config says \`testing.framework: "pytest"\`, use \`pytest\` instead of generic test commands
|
|
1653
|
-
|
|
1654
|
-
## Template Variable Replacement
|
|
1655
|
-
|
|
1656
|
-
When you see variables like \`{{config.project.name}}\` in workflows or templates:
|
|
1657
|
-
|
|
1658
|
-
1. **Extract the path**: \`{{config.project.name}}\` → path is \`project.name\`
|
|
1659
|
-
2. **Read your local config**: \`.fraim/config.json\`
|
|
1660
|
-
3. **Navigate to the value**: \`config.project.name\` → \`"My Project"\`
|
|
1661
|
-
4. **Replace**: \`{{config.project.name}}\` → \`"My Project"\`
|
|
1662
|
-
|
|
1663
|
-
### Example
|
|
1664
|
-
|
|
1665
|
-
**Template:**
|
|
1666
|
-
\`\`\`
|
|
1667
|
-
Project: {{config.project.name}}
|
|
1668
|
-
Language: {{config.project.primaryLanguage}}
|
|
1669
|
-
Test Framework: {{config.testing.framework}}
|
|
1670
|
-
\`\`\`
|
|
1671
|
-
|
|
1672
|
-
**Your .fraim/config.json:**
|
|
1673
|
-
\`\`\`json
|
|
1674
|
-
{
|
|
1675
|
-
"project": {
|
|
1676
|
-
"name": "My Awesome App",
|
|
1677
|
-
"primaryLanguage": "python"
|
|
1678
|
-
},
|
|
1679
|
-
"testing": {
|
|
1680
|
-
"framework": "pytest"
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
\`\`\`
|
|
1684
|
-
|
|
1685
|
-
**Result:**
|
|
1686
|
-
\`\`\`
|
|
1687
|
-
Project: My Awesome App
|
|
1688
|
-
Language: python
|
|
1689
|
-
Test Framework: pytest
|
|
1690
|
-
\`\`\`
|
|
1691
|
-
|
|
1692
|
-
## Local Customizations
|
|
1693
|
-
|
|
1694
|
-
Your project may have customizations in:
|
|
1695
|
-
|
|
1696
|
-
- **\`.fraim/rules/\`** - Custom rules that override generic ones
|
|
1697
|
-
- **\`.fraim/workflows/\`** - Custom workflows that override generic ones
|
|
1698
|
-
- **\`.fraim/templates/\`** - Custom templates that override generic ones
|
|
1699
|
-
- **\`.fraim/scripts/\`** - Custom scripts that override generic ones
|
|
1700
|
-
|
|
1701
|
-
**Priority**: Local customizations always take precedence over generic files from the cloud.
|
|
1702
|
-
|
|
1703
|
-
## Common Config Paths
|
|
1704
|
-
|
|
1705
|
-
- \`config.project.name\` - Project name
|
|
1706
|
-
- \`config.project.primaryLanguage\` - Main programming language
|
|
1707
|
-
- \`config.project.type\` - Project type (saas-product, library, etc.)
|
|
1708
|
-
- \`config.testing.framework\` - Test framework (pytest, jest, etc.)
|
|
1709
|
-
- \`config.testing.testLocation\` - Where tests are located
|
|
1710
|
-
- \`config.testing.testNaming\` - Test file naming pattern
|
|
1711
|
-
- \`config.git.defaultBranch\` - Default git branch
|
|
1712
|
-
- \`config.git.branchNaming\` - Branch naming pattern
|
|
1713
|
-
|
|
1714
|
-
## Workflow Execution
|
|
1715
|
-
|
|
1716
|
-
When executing workflows:
|
|
1717
|
-
|
|
1718
|
-
1. **Read local config first** - Understand your project's settings
|
|
1719
|
-
2. **Replace template variables** - Use config values in commands and file paths
|
|
1720
|
-
3. **Check for local customizations** - Use local overrides if they exist
|
|
1721
|
-
4. **Execute in workspace** - All operations happen in your project directory
|
|
1722
|
-
|
|
1723
|
-
## If Config Doesn't Exist
|
|
1724
|
-
|
|
1725
|
-
If \`.fraim/config.json\` doesn't exist:
|
|
1726
|
-
|
|
1727
|
-
1. Use \`fraim_setup\` tool to create it interactively
|
|
1728
|
-
2. Or use default values (see FRAIM defaults)
|
|
1729
|
-
3. Or create it manually with project-specific settings
|
|
1730
|
-
|
|
1731
|
-
## Next Steps
|
|
1732
|
-
|
|
1733
|
-
1. Read \`.fraim/config.json\` from your workspace
|
|
1734
|
-
2. Use config values when you see \`{{config.*}}\` variables
|
|
1735
|
-
3. Check for local customizations in \`.fraim/\` directories
|
|
1736
|
-
4. Execute workflows using your project's specific settings`;
|
|
1737
|
-
return {
|
|
1738
|
-
content: [
|
|
1739
|
-
{
|
|
1740
|
-
type: 'text',
|
|
1741
|
-
text: instructions
|
|
1742
|
-
}
|
|
1743
|
-
]
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
async handleFraimConnect(args, apiKey, userId) {
|
|
1747
|
-
if (!apiKey) {
|
|
1748
|
-
throw new Error('No API Key found in context for fraim_connect');
|
|
1749
|
-
}
|
|
1750
|
-
// Validate required agent information
|
|
1751
|
-
if (!args.agent?.name || !args.agent?.model) {
|
|
1752
|
-
throw new Error('Agent information is required. Please provide agent.name and agent.model in your fraim_connect call.');
|
|
1753
|
-
}
|
|
1754
|
-
// Validate agent information to prevent BS
|
|
1755
|
-
const validAgentNames = ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity', 'ChatGPT', 'Copilot'];
|
|
1756
|
-
if (!validAgentNames.some(valid => args.agent.name.toLowerCase().includes(valid.toLowerCase()))) {
|
|
1757
|
-
throw new Error(`Invalid agent name "${args.agent.name}". Please use one of: ${validAgentNames.join(', ')}`);
|
|
1758
|
-
}
|
|
1759
|
-
// Validate machine information
|
|
1760
|
-
if (!args.machine?.hostname || !args.machine?.platform) {
|
|
1761
|
-
throw new Error('Machine information is required. Please provide machine.hostname and machine.platform. Use os.hostname() and process.platform.');
|
|
1762
|
-
}
|
|
1763
|
-
// Validate repo information - check if .fraim/config.json exists and use it
|
|
1764
|
-
let repoInfo = args.repo || {};
|
|
1765
|
-
// Note: MCP server cannot access client-side .fraim/config.json
|
|
1766
|
-
// Repository info must be provided by the client in the fraim_connect call
|
|
1767
|
-
if (!repoInfo.url) {
|
|
1768
|
-
throw new Error('Repository information is required. Please provide repo.url in the fraim_connect call.');
|
|
1769
|
-
}
|
|
1770
|
-
// 1. Generate Session ID
|
|
1771
|
-
const sessionId = (0, crypto_1.randomUUID)();
|
|
1772
|
-
// userId passed from context
|
|
1773
|
-
const finalUserId = userId || 'unknown';
|
|
1774
|
-
// 2. Create Session in DB with enhanced information
|
|
1775
|
-
const session = {
|
|
1776
|
-
sessionId,
|
|
1777
|
-
userId: finalUserId,
|
|
1778
|
-
agent: {
|
|
1779
|
-
name: args.agent.name,
|
|
1780
|
-
model: args.agent.model,
|
|
1781
|
-
version: args.agent.version
|
|
1782
|
-
},
|
|
1783
|
-
machine: args.machine,
|
|
1784
|
-
repo: repoInfo,
|
|
1785
|
-
startTime: new Date(),
|
|
1786
|
-
lastActive: new Date()
|
|
1787
|
-
};
|
|
1788
|
-
const keyData = await this.dbService.verifyApiKey(apiKey);
|
|
1789
|
-
if (keyData)
|
|
1790
|
-
session.userId = keyData.userId;
|
|
1791
|
-
await this.dbService.createSession(session);
|
|
1792
|
-
// 3. Register in SessionManager
|
|
1793
|
-
this.sessionManager.registerSession(apiKey, sessionId);
|
|
1794
|
-
// 4. Generate informative response
|
|
1795
|
-
const agentInfo = `${args.agent.name}${args.agent.model ? ` (${args.agent.model})` : ''}${args.agent.version ? ` v${args.agent.version}` : ''}`;
|
|
1796
|
-
const machineInfo = `${args.machine.hostname || 'unknown'} (${args.machine.platform || 'unknown'})`;
|
|
1797
|
-
const repoInfoDisplay = repoInfo.name || repoInfo.url || 'unknown';
|
|
1798
|
-
return {
|
|
1799
|
-
content: [{
|
|
1800
|
-
type: 'text',
|
|
1801
|
-
text: `✅ **FRAIM Session Connected!**
|
|
1802
|
-
|
|
1803
|
-
**Session ID**: ${sessionId}
|
|
1804
|
-
**Agent**: ${agentInfo}
|
|
1805
|
-
**Machine**: ${machineInfo}
|
|
1806
|
-
**Repository**: ${repoInfoDisplay}
|
|
1807
|
-
|
|
1808
|
-
🔄 Telemetry active. All FRAIM tools are now unlocked.
|
|
1809
|
-
|
|
1810
|
-
**Next Steps**:
|
|
1811
|
-
- Use \`get_fraim_init\` to see available workflows
|
|
1812
|
-
- Use \`list_fraim_workflows\` to browse by category
|
|
1813
|
-
- Use \`get_fraim_workflow({ workflow: "name" })\` to start working`
|
|
1814
|
-
}],
|
|
1815
|
-
sessionId: sessionId
|
|
1816
|
-
};
|
|
1817
|
-
}
|
|
1818
|
-
async handleSeekCoachingOnNextStep(args) {
|
|
1819
|
-
console.log('🔧 MCP Server: handleSeekCoachingOnNextStep called with args:', JSON.stringify(args, null, 2));
|
|
1820
|
-
try {
|
|
1821
|
-
const response = await this.aiCoach.handleCoachingRequest({
|
|
1822
|
-
workflowType: args.workflowType,
|
|
1823
|
-
issueNumber: args.issueNumber,
|
|
1824
|
-
currentPhase: args.currentPhase,
|
|
1825
|
-
status: args.status,
|
|
1826
|
-
evidence: args.evidence,
|
|
1827
|
-
findings: args.findings
|
|
1828
|
-
});
|
|
1829
|
-
console.log('✅ MCP Server: AI Coach returned response, length:', response.length);
|
|
1830
|
-
return {
|
|
1831
|
-
content: [{
|
|
1832
|
-
type: 'text',
|
|
1833
|
-
text: response
|
|
1834
|
-
}]
|
|
1835
|
-
};
|
|
1836
|
-
}
|
|
1837
|
-
catch (error) {
|
|
1838
|
-
console.error('❌ AI Coach guidance failed:', error);
|
|
1839
|
-
return {
|
|
1840
|
-
content: [{
|
|
1841
|
-
type: 'text',
|
|
1842
|
-
text: `# ❌ AI Coach Guidance Failed\n\n**Error**: ${error instanceof Error ? error.message : 'Unknown error'}\n\nPlease check your request parameters and try again.`
|
|
1843
|
-
}],
|
|
1844
|
-
isError: true,
|
|
1845
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1846
|
-
};
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
async start(port = 3002) {
|
|
1850
|
-
try {
|
|
1851
|
-
// Connect to database before starting server
|
|
1852
|
-
await this.dbService.connect();
|
|
1853
|
-
this.app.listen(port, () => {
|
|
1854
|
-
console.log(`🚀 FRAIM MCP Server running on port ${port}`);
|
|
1855
|
-
console.log(`📡 MCP endpoint: http://localhost:${port}/mcp`);
|
|
1856
|
-
console.log(`🏥 Health check: http://localhost:${port}/health`);
|
|
1857
|
-
console.log(`📚 Serving ${this.fileIndex.size} files from registry`);
|
|
1858
|
-
console.log(` Path: ${this.registryPath}`);
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
catch (error) {
|
|
1862
|
-
console.error('❌ Failed to connect to database during startup:', error);
|
|
1863
|
-
throw error;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
detectProviderFromUrl(url) {
|
|
1867
|
-
if (!url)
|
|
1868
|
-
return 'github';
|
|
1869
|
-
if (url.includes('dev.azure.com') || url.includes('visualstudio.com')) {
|
|
1870
|
-
return 'ado';
|
|
1871
|
-
}
|
|
1872
|
-
return 'github';
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
exports.FraimMCPServer = FraimMCPServer;
|
|
1876
|
-
// Start the server if this file is run directly
|
|
1877
|
-
// In Azure App Service, the port is provided via process.env.PORT
|
|
1878
|
-
const serverPort = (0, git_utils_1.getPort)();
|
|
1879
|
-
const defaultFraimPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : serverPort + 2;
|
|
1880
|
-
const port = process.env.PORT ? parseInt(process.env.PORT) : defaultFraimPort;
|
|
1881
|
-
const server = new FraimMCPServer();
|
|
1882
|
-
server.start(port).catch((error) => {
|
|
1883
|
-
console.error('Failed to start FRAIM MCP Server:', error);
|
|
1884
|
-
process.exit(1);
|
|
1885
|
-
});
|