fraim-framework 2.0.60 → 2.0.62
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.
|
@@ -20,13 +20,15 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
20
20
|
class FraimLocalMCPServer {
|
|
21
21
|
constructor() {
|
|
22
22
|
this.config = null;
|
|
23
|
+
this.clientSupportsRoots = false;
|
|
24
|
+
this.workspaceRoot = null;
|
|
25
|
+
this.pendingRootsRequest = false;
|
|
23
26
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
24
27
|
this.apiKey = process.env.FRAIM_API_KEY || '';
|
|
25
28
|
if (!this.apiKey) {
|
|
26
29
|
this.logError('❌ FRAIM_API_KEY environment variable is required');
|
|
27
30
|
process.exit(1);
|
|
28
31
|
}
|
|
29
|
-
this.loadConfig();
|
|
30
32
|
this.log('🚀 FRAIM Local MCP Server starting...');
|
|
31
33
|
this.log(`📡 Remote server: ${this.remoteUrl}`);
|
|
32
34
|
this.log(`🔑 API key: ${this.apiKey.substring(0, 10)}...`);
|
|
@@ -39,20 +41,63 @@ class FraimLocalMCPServer {
|
|
|
39
41
|
console.error(`[FRAIM ERROR] ${message}`);
|
|
40
42
|
}
|
|
41
43
|
findProjectRoot() {
|
|
42
|
-
//
|
|
44
|
+
// If we already have workspace root from MCP roots, use it
|
|
45
|
+
if (this.workspaceRoot) {
|
|
46
|
+
this.log(`✅ Using workspace root from MCP roots: ${this.workspaceRoot}`);
|
|
47
|
+
return this.workspaceRoot;
|
|
48
|
+
}
|
|
49
|
+
// Log all potentially useful environment variables for debugging
|
|
50
|
+
this.log(`🔍 Environment variables:`);
|
|
51
|
+
this.log(` WORKSPACE_FOLDER_PATHS: ${process.env.WORKSPACE_FOLDER_PATHS || '(not set)'}`);
|
|
52
|
+
this.log(` WORKSPACE_FOLDER: ${process.env.WORKSPACE_FOLDER || '(not set)'}`);
|
|
53
|
+
this.log(` PROJECT_ROOT: ${process.env.PROJECT_ROOT || '(not set)'}`);
|
|
54
|
+
this.log(` VSCODE_CWD: ${process.env.VSCODE_CWD || '(not set)'}`);
|
|
55
|
+
this.log(` INIT_CWD: ${process.env.INIT_CWD || '(not set)'}`);
|
|
56
|
+
this.log(` HOME: ${process.env.HOME || '(not set)'}`);
|
|
57
|
+
this.log(` USERPROFILE: ${process.env.USERPROFILE || '(not set)'}`);
|
|
58
|
+
// Priority 1: Check for IDE-provided workspace environment variables
|
|
59
|
+
const workspaceHints = [
|
|
60
|
+
process.env.WORKSPACE_FOLDER_PATHS?.split(':')[0], // Cursor provides this (colon-separated for multi-root)
|
|
61
|
+
process.env.WORKSPACE_FOLDER, // VSCode and others
|
|
62
|
+
process.env.PROJECT_ROOT,
|
|
63
|
+
process.env.VSCODE_CWD,
|
|
64
|
+
process.env.INIT_CWD,
|
|
65
|
+
];
|
|
66
|
+
for (const hint of workspaceHints) {
|
|
67
|
+
if (hint && (0, fs_1.existsSync)((0, path_1.join)(hint, '.fraim'))) {
|
|
68
|
+
this.log(`✅ Found .fraim via workspace env var: ${hint}`);
|
|
69
|
+
return hint;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Priority 2: Search upwards from cwd, but skip home directory
|
|
43
73
|
let currentDir = process.cwd();
|
|
44
74
|
const root = (0, path_1.parse)(currentDir).root;
|
|
75
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
45
76
|
this.log(`🔍 Starting search from: ${currentDir}`);
|
|
77
|
+
if (homeDir) {
|
|
78
|
+
this.log(`🏠 Home directory (will skip): ${homeDir}`);
|
|
79
|
+
}
|
|
46
80
|
while (currentDir !== root) {
|
|
47
81
|
const fraimDir = (0, path_1.join)(currentDir, '.fraim');
|
|
48
82
|
this.log(` Checking: ${fraimDir}`);
|
|
49
83
|
if ((0, fs_1.existsSync)(fraimDir)) {
|
|
84
|
+
// Skip home directory .fraim - continue searching for project-specific one
|
|
85
|
+
if (homeDir && currentDir === homeDir) {
|
|
86
|
+
this.log(` ⚠️ Skipping home directory .fraim, continuing search...`);
|
|
87
|
+
currentDir = (0, path_1.dirname)(currentDir);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
50
90
|
this.log(`✅ Found .fraim at: ${currentDir}`);
|
|
51
91
|
return currentDir;
|
|
52
92
|
}
|
|
53
93
|
currentDir = (0, path_1.dirname)(currentDir);
|
|
54
94
|
}
|
|
55
|
-
|
|
95
|
+
// Priority 3: Fall back to home directory .fraim if nothing else found
|
|
96
|
+
if (homeDir && (0, fs_1.existsSync)((0, path_1.join)(homeDir, '.fraim'))) {
|
|
97
|
+
this.log(`⚠️ Using home directory .fraim as fallback: ${homeDir}`);
|
|
98
|
+
return homeDir;
|
|
99
|
+
}
|
|
100
|
+
this.log(`❌ No .fraim directory found`);
|
|
56
101
|
return null;
|
|
57
102
|
}
|
|
58
103
|
loadConfig() {
|
|
@@ -158,11 +203,58 @@ class FraimLocalMCPServer {
|
|
|
158
203
|
};
|
|
159
204
|
}
|
|
160
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Try to request workspace roots from MCP client
|
|
208
|
+
*/
|
|
209
|
+
requestRootsFromClient() {
|
|
210
|
+
if (!this.clientSupportsRoots || this.pendingRootsRequest) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
this.log('🔍 Requesting workspace roots from client...');
|
|
215
|
+
this.pendingRootsRequest = true;
|
|
216
|
+
// Send roots/list request to client via stdout
|
|
217
|
+
const rootsRequest = {
|
|
218
|
+
jsonrpc: '2.0',
|
|
219
|
+
id: 'fraim-roots-query',
|
|
220
|
+
method: 'roots/list',
|
|
221
|
+
params: {}
|
|
222
|
+
};
|
|
223
|
+
this.log(`📤 Sending roots/list request: ${JSON.stringify(rootsRequest)}`);
|
|
224
|
+
process.stdout.write(JSON.stringify(rootsRequest) + '\n');
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.log(`⚠️ Failed to request roots: ${error.message}`);
|
|
228
|
+
this.pendingRootsRequest = false;
|
|
229
|
+
// Fall back to env var search
|
|
230
|
+
this.loadConfig();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
161
233
|
/**
|
|
162
234
|
* Handle incoming MCP request
|
|
163
235
|
*/
|
|
164
236
|
async handleRequest(request) {
|
|
165
237
|
this.log(`📥 ${request.method}`);
|
|
238
|
+
// Special handling for initialize request
|
|
239
|
+
if (request.method === 'initialize') {
|
|
240
|
+
// Check if client supports roots
|
|
241
|
+
const clientCapabilities = request.params?.capabilities;
|
|
242
|
+
if (clientCapabilities?.roots) {
|
|
243
|
+
this.clientSupportsRoots = true;
|
|
244
|
+
this.log(`✅ Client supports roots capability (listChanged: ${clientCapabilities.roots.listChanged})`);
|
|
245
|
+
}
|
|
246
|
+
// Proxy initialize to remote server first
|
|
247
|
+
const response = await this.proxyToRemote(request);
|
|
248
|
+
const processedResponse = this.processResponse(response);
|
|
249
|
+
// After successful initialization, load config using fallback methods
|
|
250
|
+
if (!processedResponse.error) {
|
|
251
|
+
// For now, don't request roots - just use env var + upward search
|
|
252
|
+
// TODO: Implement roots/list properly after initialization is complete
|
|
253
|
+
this.loadConfig();
|
|
254
|
+
}
|
|
255
|
+
this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
|
|
256
|
+
return processedResponse;
|
|
257
|
+
}
|
|
166
258
|
// Proxy to remote server
|
|
167
259
|
const response = await this.proxyToRemote(request);
|
|
168
260
|
// Process template substitution
|
|
@@ -170,6 +262,47 @@ class FraimLocalMCPServer {
|
|
|
170
262
|
this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
|
|
171
263
|
return processedResponse;
|
|
172
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Handle incoming MCP response (to our roots/list request)
|
|
267
|
+
*/
|
|
268
|
+
handleResponse(response) {
|
|
269
|
+
// Check if this is a response to our roots/list request
|
|
270
|
+
if (response.id === 'fraim-roots-query') {
|
|
271
|
+
this.log(`📥 Response to roots/list request`);
|
|
272
|
+
this.pendingRootsRequest = false;
|
|
273
|
+
if (response.error) {
|
|
274
|
+
this.log(`⚠️ Client returned error for roots/list: ${response.error.message}`);
|
|
275
|
+
// Fall back to env var + upward search
|
|
276
|
+
this.loadConfig();
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const roots = response.result?.roots;
|
|
280
|
+
if (roots && Array.isArray(roots) && roots.length > 0) {
|
|
281
|
+
// Use the first root (typically the primary workspace folder)
|
|
282
|
+
const firstRoot = roots[0];
|
|
283
|
+
if (firstRoot.uri && firstRoot.uri.startsWith('file://')) {
|
|
284
|
+
// Convert file:// URI to local path
|
|
285
|
+
// Handle both Unix (/path) and Windows (C:/path) paths
|
|
286
|
+
let rootPath = firstRoot.uri.replace('file://', '');
|
|
287
|
+
// Windows: file:///C:/path -> C:/path
|
|
288
|
+
rootPath = rootPath.replace(/^\/([A-Z]:)/i, '$1');
|
|
289
|
+
this.log(`✅ Got workspace root from client: ${rootPath} (${firstRoot.name || 'unnamed'})`);
|
|
290
|
+
this.workspaceRoot = rootPath;
|
|
291
|
+
// Now load config with the correct workspace root
|
|
292
|
+
this.loadConfig();
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
this.log(`⚠️ Root URI is not a file:// URI: ${firstRoot.uri}`);
|
|
296
|
+
this.loadConfig();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
this.log('⚠️ No roots provided by client');
|
|
301
|
+
this.loadConfig();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
173
306
|
/**
|
|
174
307
|
* Start STDIO server
|
|
175
308
|
*/
|
|
@@ -185,13 +318,28 @@ class FraimLocalMCPServer {
|
|
|
185
318
|
buffer = buffer.slice(newlineIndex + 1);
|
|
186
319
|
if (line) {
|
|
187
320
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
|
|
321
|
+
const message = JSON.parse(line);
|
|
322
|
+
// Distinguish between requests and responses
|
|
323
|
+
// Requests have 'method', responses have 'result' or 'error' (but no 'method')
|
|
324
|
+
if ('method' in message && message.method) {
|
|
325
|
+
// This is a request from the client
|
|
326
|
+
const response = await this.handleRequest(message);
|
|
327
|
+
// Only send response if we got one (null means we handled it internally)
|
|
328
|
+
if (response) {
|
|
329
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if ('result' in message || 'error' in message) {
|
|
333
|
+
// This is a response from the client (to our roots/list request)
|
|
334
|
+
this.handleResponse(message);
|
|
335
|
+
// Don't send anything back - this was a response to our request
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.logError(`Unknown message type: ${JSON.stringify(message)}`);
|
|
339
|
+
}
|
|
192
340
|
}
|
|
193
341
|
catch (error) {
|
|
194
|
-
this.logError(`
|
|
342
|
+
this.logError(`Message processing failed: ${error.message}`);
|
|
195
343
|
// Send error response
|
|
196
344
|
const errorResponse = {
|
|
197
345
|
jsonrpc: '2.0',
|
|
@@ -2,18 +2,33 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkflowParser = void 0;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
5
6
|
class WorkflowParser {
|
|
6
7
|
/**
|
|
7
8
|
* Parse a workflow markdown file into a structured definition
|
|
9
|
+
* Supports two formats:
|
|
10
|
+
* 1. Phase-based workflows with JSON frontmatter (e.g., implement, spec)
|
|
11
|
+
* 2. Simple workflows without frontmatter (e.g., bootstrap workflows)
|
|
8
12
|
*/
|
|
9
13
|
static parse(filePath) {
|
|
10
14
|
if (!(0, fs_1.existsSync)(filePath))
|
|
11
15
|
return null;
|
|
12
16
|
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
13
|
-
//
|
|
17
|
+
// Try to extract JSON Metadata (frontmatter)
|
|
14
18
|
const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
15
|
-
if (
|
|
16
|
-
|
|
19
|
+
if (metadataMatch) {
|
|
20
|
+
// Phase-based workflow with frontmatter
|
|
21
|
+
return this.parsePhaseBasedWorkflow(filePath, content, metadataMatch);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Simple workflow without frontmatter
|
|
25
|
+
return this.parseSimpleWorkflow(filePath, content);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a phase-based workflow with JSON frontmatter
|
|
30
|
+
*/
|
|
31
|
+
static parsePhaseBasedWorkflow(filePath, content, metadataMatch) {
|
|
17
32
|
let metadata;
|
|
18
33
|
try {
|
|
19
34
|
metadata = JSON.parse(metadataMatch[1]);
|
|
@@ -22,7 +37,7 @@ class WorkflowParser {
|
|
|
22
37
|
console.error(`❌ Failed to parse JSON metadata in ${filePath}:`, e);
|
|
23
38
|
return null;
|
|
24
39
|
}
|
|
25
|
-
//
|
|
40
|
+
// Extract Overview (Content after metadata but before first phase header)
|
|
26
41
|
const contentAfterMetadata = content.substring(metadataMatch[0].length).trim();
|
|
27
42
|
const firstPhaseIndex = contentAfterMetadata.search(/^##\s+Phase:/m);
|
|
28
43
|
let overview = '';
|
|
@@ -34,7 +49,7 @@ class WorkflowParser {
|
|
|
34
49
|
else {
|
|
35
50
|
overview = contentAfterMetadata;
|
|
36
51
|
}
|
|
37
|
-
//
|
|
52
|
+
// Extract Phases (id -> content)
|
|
38
53
|
const phases = new Map();
|
|
39
54
|
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
40
55
|
// Skip the first part (empty or overview overlap)
|
|
@@ -44,15 +59,32 @@ class WorkflowParser {
|
|
|
44
59
|
const firstLine = sectionLines[0].trim();
|
|
45
60
|
// Extract phase ID (slug before any (Phase X) or space)
|
|
46
61
|
const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
|
|
47
|
-
//
|
|
48
|
-
// But usually, the agent wants the whole section including the Phase ID header.
|
|
49
|
-
// We'll store the whole section but prepend the header because split removed it.
|
|
62
|
+
// Store the whole section with header
|
|
50
63
|
phases.set(id, `## Phase: ${section.trim()}`);
|
|
51
64
|
}
|
|
52
65
|
return {
|
|
53
66
|
metadata,
|
|
54
67
|
overview,
|
|
55
|
-
phases
|
|
68
|
+
phases,
|
|
69
|
+
isSimple: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a simple workflow without frontmatter (bootstrap-style)
|
|
74
|
+
*/
|
|
75
|
+
static parseSimpleWorkflow(filePath, content) {
|
|
76
|
+
// Extract workflow name from filename
|
|
77
|
+
const workflowName = (0, path_1.basename)(filePath, '.md');
|
|
78
|
+
// For simple workflows, the entire content is the overview
|
|
79
|
+
// No phases, just execution steps
|
|
80
|
+
const metadata = {
|
|
81
|
+
name: workflowName
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
metadata,
|
|
85
|
+
overview: content.trim(),
|
|
86
|
+
phases: new Map(),
|
|
87
|
+
isSimple: true
|
|
56
88
|
};
|
|
57
89
|
}
|
|
58
90
|
/**
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.62",
|
|
4
4
|
"description": "FRAIM v2: Framework for Rigor-based AI Management - Transform from solo developer to AI manager orchestrating production-ready code with enterprise-grade discipline",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|