fraim-framework 2.0.59 → 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,68 @@ class FraimLocalMCPServer {
39
41
  console.error(`[FRAIM ERROR] ${message}`);
40
42
  }
41
43
  findProjectRoot() {
42
- // Start from cwd and search upwards for .fraim directory
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;
76
+ this.log(`🔍 Starting search from: ${currentDir}`);
77
+ if (homeDir) {
78
+ this.log(`🏠 Home directory (will skip): ${homeDir}`);
79
+ }
45
80
  while (currentDir !== root) {
46
81
  const fraimDir = (0, path_1.join)(currentDir, '.fraim');
82
+ this.log(` Checking: ${fraimDir}`);
47
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
+ }
90
+ this.log(`✅ Found .fraim at: ${currentDir}`);
48
91
  return currentDir;
49
92
  }
50
93
  currentDir = (0, path_1.dirname)(currentDir);
51
94
  }
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`);
52
101
  return null;
53
102
  }
54
103
  loadConfig() {
55
104
  try {
105
+ this.log(`📍 Process started from: ${process.cwd()}`);
56
106
  // Try to find project root by searching for .fraim directory
57
107
  const projectDir = this.findProjectRoot() || process.cwd();
58
108
  const configPath = (0, path_1.join)(projectDir, '.fraim', 'config.json');
@@ -153,11 +203,58 @@ class FraimLocalMCPServer {
153
203
  };
154
204
  }
155
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
+ }
156
233
  /**
157
234
  * Handle incoming MCP request
158
235
  */
159
236
  async handleRequest(request) {
160
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
+ }
161
258
  // Proxy to remote server
162
259
  const response = await this.proxyToRemote(request);
163
260
  // Process template substitution
@@ -165,6 +262,47 @@ class FraimLocalMCPServer {
165
262
  this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
166
263
  return processedResponse;
167
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
+ }
168
306
  /**
169
307
  * Start STDIO server
170
308
  */
@@ -180,13 +318,28 @@ class FraimLocalMCPServer {
180
318
  buffer = buffer.slice(newlineIndex + 1);
181
319
  if (line) {
182
320
  try {
183
- const request = JSON.parse(line);
184
- const response = await this.handleRequest(request);
185
- // Send response to stdout
186
- process.stdout.write(JSON.stringify(response) + '\n');
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
+ }
187
340
  }
188
341
  catch (error) {
189
- this.logError(`Request processing failed: ${error.message}`);
342
+ this.logError(`Message processing failed: ${error.message}`);
190
343
  // Send error response
191
344
  const errorResponse = {
192
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
- // 1. Extract JSON Metadata
17
+ // Try to extract JSON Metadata (frontmatter)
14
18
  const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
15
- if (!metadataMatch)
16
- return null;
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
- // 2. Extract Overview (Content after metadata but before first phase header)
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
- // 3. Extract Phases (id -> content)
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
- // The content includes the header (we'll reconstruct it for the agent or just return the body)
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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.59",
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": {