fraim-framework 2.0.96 → 2.0.98

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.
Files changed (40) hide show
  1. package/bin/fraim.js +1 -1
  2. package/dist/src/cli/commands/add-ide.js +1 -1
  3. package/dist/src/cli/commands/doctor.js +6 -6
  4. package/dist/src/cli/commands/init-project.js +63 -52
  5. package/dist/src/cli/commands/list-overridable.js +33 -55
  6. package/dist/src/cli/commands/list.js +35 -9
  7. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  8. package/dist/src/cli/commands/override.js +18 -39
  9. package/dist/src/cli/commands/setup.js +1 -1
  10. package/dist/src/cli/commands/sync.js +34 -27
  11. package/dist/src/cli/doctor/check-runner.js +3 -3
  12. package/dist/src/cli/doctor/checks/global-setup-checks.js +13 -13
  13. package/dist/src/cli/doctor/checks/project-setup-checks.js +12 -12
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +2 -2
  15. package/dist/src/cli/doctor/checks/workflow-checks.js +56 -60
  16. package/dist/src/cli/doctor/reporters/console-reporter.js +1 -1
  17. package/dist/src/cli/fraim.js +3 -1
  18. package/dist/src/cli/mcp/mcp-server-registry.js +1 -1
  19. package/dist/src/cli/services/device-flow-service.js +83 -0
  20. package/dist/src/cli/setup/auto-mcp-setup.js +2 -2
  21. package/dist/src/cli/setup/first-run.js +4 -3
  22. package/dist/src/cli/utils/agent-adapters.js +126 -0
  23. package/dist/src/cli/utils/fraim-gitignore.js +15 -21
  24. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  25. package/dist/src/cli/utils/remote-sync.js +20 -67
  26. package/dist/src/core/ai-mentor.js +31 -49
  27. package/dist/src/core/config-loader.js +57 -62
  28. package/dist/src/core/config-writer.js +75 -0
  29. package/dist/src/core/types.js +1 -1
  30. package/dist/src/core/utils/job-parser.js +176 -0
  31. package/dist/src/core/utils/local-registry-resolver.js +61 -71
  32. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  33. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  34. package/dist/src/core/utils/stub-generator.js +41 -75
  35. package/dist/src/core/utils/workflow-parser.js +5 -3
  36. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  37. package/dist/src/local-mcp-server/stdio-server.js +103 -43
  38. package/dist/src/local-mcp-server/usage-collector.js +126 -27
  39. package/index.js +1 -1
  40. package/package.json +3 -4
@@ -29,7 +29,9 @@ const provider_utils_1 = require("../core/utils/provider-utils");
29
29
  const object_utils_1 = require("../core/utils/object-utils");
30
30
  const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
31
31
  const ai_mentor_1 = require("../core/ai-mentor");
32
+ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
32
33
  const usage_collector_js_1 = require("./usage-collector.js");
34
+ const learning_context_builder_js_1 = require("./learning-context-builder.js");
33
35
  /**
34
36
  * Handle template substitution logic separately for better testability
35
37
  */
@@ -75,6 +77,9 @@ class FraimTemplateEngine {
75
77
  setUserEmail(email) {
76
78
  this.userEmail = email;
77
79
  }
80
+ getUserEmail() {
81
+ return this.userEmail;
82
+ }
78
83
  substituteTemplates(content) {
79
84
  let result = content;
80
85
  // Substitute {{proxy.user.email}} with the email captured from fraim_connect
@@ -349,8 +354,8 @@ class FraimLocalMCPServer {
349
354
  process.env.INIT_CWD,
350
355
  ];
351
356
  for (const hint of workspaceHints) {
352
- if (hint && (0, fs_1.existsSync)((0, path_1.join)(hint, '.fraim'))) {
353
- this.log(`✅ Found .fraim via workspace env var: ${hint}`);
357
+ if (hint && (0, project_fraim_paths_1.workspaceFraimExists)(hint)) {
358
+ this.log(`Found ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)()} via workspace env var: ${hint}`);
354
359
  return hint;
355
360
  }
356
361
  }
@@ -363,43 +368,43 @@ class FraimLocalMCPServer {
363
368
  this.log(`🏠 Home directory (will skip): ${homeDir}`);
364
369
  }
365
370
  while (currentDir !== root) {
366
- const fraimDir = (0, path_1.join)(currentDir, '.fraim');
371
+ const fraimDir = (0, project_fraim_paths_1.getWorkspaceConfigPath)(currentDir).replace(/[\\/]config\.json$/, '');
367
372
  this.log(` Checking: ${fraimDir}`);
368
373
  if ((0, fs_1.existsSync)(fraimDir)) {
369
- // Skip home directory .fraim - continue searching for project-specific one
374
+ // Skip the home directory FRAIM dir and continue searching for a project-specific one
370
375
  if (homeDir && currentDir === homeDir) {
371
- this.log(` ⚠️ Skipping home directory .fraim, continuing search...`);
376
+ this.log(`Skipping home directory ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)()}, continuing search...`);
372
377
  currentDir = (0, path_1.dirname)(currentDir);
373
378
  continue;
374
379
  }
375
- this.log(`✅ Found .fraim at: ${currentDir}`);
380
+ this.log(`Found workspace FRAIM dir at: ${currentDir}`);
376
381
  return currentDir;
377
382
  }
378
383
  currentDir = (0, path_1.dirname)(currentDir);
379
384
  }
380
- // Priority 3: Fall back to home directory .fraim if nothing else found
385
+ // Priority 3: Fall back to the home directory .fraim if nothing else found
381
386
  if (homeDir && (0, fs_1.existsSync)((0, path_1.join)(homeDir, '.fraim'))) {
382
387
  this.log(`⚠️ Using home directory .fraim as fallback: ${homeDir}`);
383
388
  return homeDir;
384
389
  }
385
- this.log(`❌ No .fraim directory found`);
390
+ this.log('No workspace FRAIM directory found');
386
391
  return null;
387
392
  }
388
393
  loadConfig() {
389
394
  try {
390
395
  this.log(`📍 Process started from: ${process.cwd()}`);
391
- // Try to find project root by searching for .fraim directory
396
+ // Try to find the project root by searching for the workspace FRAIM directory
392
397
  const projectDir = this.findProjectRoot() || process.cwd();
393
- const configPath = (0, path_1.join)(projectDir, '.fraim', 'config.json');
398
+ const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(projectDir);
394
399
  this.log(`🔍 Looking for config at: ${configPath}`);
395
400
  if ((0, fs_1.existsSync)(configPath)) {
396
401
  const configContent = (0, fs_1.readFileSync)(configPath, 'utf8');
397
402
  this.config = JSON.parse(configContent);
398
- this.log('✅ Loaded local .fraim/config.json');
403
+ this.log(`Loaded local ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')}`);
399
404
  }
400
405
  else {
401
406
  this.config = null;
402
- this.log('⚠️ No .fraim/config.json found - template substitution disabled');
407
+ this.log(`No ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')} found - template substitution disabled`);
403
408
  }
404
409
  if (this.engine) {
405
410
  this.engine.setConfig(this.config);
@@ -546,6 +551,10 @@ class FraimLocalMCPServer {
546
551
  * Get the user's working style preference from ~/.fraim/config.json
547
552
  */
548
553
  getWorkingStyle() {
554
+ // Workspace config takes precedence over global config
555
+ if (this.config?.workingStyle) {
556
+ return this.config.workingStyle;
557
+ }
549
558
  try {
550
559
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
551
560
  const configPath = (0, path_1.join)(homeDir, '.fraim', 'config.json');
@@ -836,16 +845,17 @@ class FraimLocalMCPServer {
836
845
  registryPath = await resolver.findRegistryPath('skills', args.skill);
837
846
  else if (toolName === 'get_fraim_job')
838
847
  registryPath = await resolver.findRegistryPath('jobs', args.job);
839
- else if (toolName === 'get_fraim_workflow')
840
- registryPath = await resolver.findRegistryPath('workflows', args.workflow);
841
848
  if (registryPath) {
842
849
  const localOverride = resolver.hasLocalOverride(registryPath);
843
- const remoteFailed = response.error && (response.error.code === -32001 ||
844
- response.error.data?.remoteStatus === 404);
850
+ const remoteFailed = Boolean(response.error);
851
+ if (response.error) {
852
+ this.log(`[req:${requestId}] Remote error for ${registryPath}: code=${response.error.code}`);
853
+ }
845
854
  if (localOverride || remoteFailed) {
846
855
  try {
847
- const resolved = await resolver.resolveFile(registryPath);
848
- if (resolved.content) {
856
+ // personalized-employee/ takes priority; when remote fails, resolver falls back to synced/cached content
857
+ const resolved = await resolver.resolveFile(registryPath).catch(() => null);
858
+ if (resolved?.content) {
849
859
  const substitutedContent = this.substituteTemplates(resolved.content);
850
860
  const newResponse = {
851
861
  ...response,
@@ -856,7 +866,6 @@ class FraimLocalMCPServer {
856
866
  };
857
867
  delete newResponse.error;
858
868
  finalizedResponse = newResponse;
859
- // Continue to Step 2 to resolve includes within the local override
860
869
  }
861
870
  }
862
871
  catch (error) {
@@ -867,14 +876,35 @@ class FraimLocalMCPServer {
867
876
  }
868
877
  // 2. Resolve includes within the content (for all registry tools)
869
878
  finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId, requestId);
870
- // 3. After fraim_connect succeeds, capture user email for {{proxy.user.email}} substitution
879
+ // 3. After fraim_connect succeeds, capture user email and inject learning context.
871
880
  if (toolName === 'fraim_connect' && !finalizedResponse.error) {
872
881
  const text = finalizedResponse.result?.content?.[0]?.text;
873
882
  if (typeof text === 'string') {
874
883
  const emailMatch = text.match(/Your identity for this session: \*\*([^*]+)\*\*/);
875
884
  if (emailMatch) {
876
- this.ensureEngine().setUserEmail(emailMatch[1].trim());
877
- this.log(`[req:${requestId}] Captured user email for template substitution: ${emailMatch[1].trim()}`);
885
+ const userEmail = emailMatch[1].trim();
886
+ this.ensureEngine().setUserEmail(userEmail);
887
+ this.log(`[req:${requestId}] Captured user email for template substitution: ${userEmail}`);
888
+ // Inject learning context from the local workspace (RFC 177: files live on disk, not server).
889
+ const workspaceRoot = this.findProjectRoot() || process.cwd();
890
+ const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, false);
891
+ if (learningSection) {
892
+ finalizedResponse.result.content[0].text = text + learningSection;
893
+ this.log(`[req:${requestId}] Injected learning context for ${userEmail} from ${workspaceRoot}`);
894
+ }
895
+ }
896
+ }
897
+ }
898
+ // 4. After get_fraim_job succeeds, inject learning context with job-focus frame.
899
+ if (toolName === 'get_fraim_job' && !finalizedResponse.error) {
900
+ const text = finalizedResponse.result?.content?.[0]?.text;
901
+ const userEmail = this.ensureEngine().getUserEmail();
902
+ if (typeof text === 'string' && userEmail) {
903
+ const workspaceRoot = this.findProjectRoot() || process.cwd();
904
+ const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true);
905
+ if (learningSection) {
906
+ finalizedResponse.result.content[0].text = text + `\n\n---` + learningSection;
907
+ this.log(`[req:${requestId}] Injected job-focus learning context for ${userEmail}`);
878
908
  }
879
909
  }
880
910
  }
@@ -1167,7 +1197,7 @@ class FraimLocalMCPServer {
1167
1197
  id: request.id,
1168
1198
  error: {
1169
1199
  code: -32603,
1170
- message: 'Failed to detect repository information. Please ensure you are in a git repository or have .fraim/config.json configured with repository details, or provide repo info in fraim_connect arguments.'
1200
+ message: `Failed to detect repository information. Please ensure you are in a git repository or have ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')} configured with repository details, or provide repo info in fraim_connect arguments.`
1171
1201
  }
1172
1202
  };
1173
1203
  }
@@ -1339,7 +1369,7 @@ class FraimLocalMCPServer {
1339
1369
  try {
1340
1370
  const mentor = this.getMentor(requestSessionId);
1341
1371
  const tutoringResponse = await mentor.handleMentoringRequest(args);
1342
- this.log(`✅ Local seekMentoring succeeded for ${args.workflowType}:${args.currentPhase}`);
1372
+ this.log(`✅ Local seekMentoring succeeded for ${args.jobName}:${args.currentPhase}`);
1343
1373
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, tutoringResponse.message);
1344
1374
  }
1345
1375
  catch (error) {
@@ -1347,44 +1377,50 @@ class FraimLocalMCPServer {
1347
1377
  // If local fails, we continue to proxy to the remote server
1348
1378
  }
1349
1379
  }
1350
- if (toolName === 'get_fraim_job' || toolName === 'get_fraim_workflow') {
1351
- const isJob = toolName === 'get_fraim_job';
1352
- const name = isJob ? args.job : args.workflow;
1380
+ if (toolName === 'get_fraim_job') {
1381
+ const name = args.job;
1353
1382
  if (name) {
1354
1383
  try {
1355
1384
  const mentor = this.getMentor(requestSessionId);
1356
- const overview = isJob
1357
- ? await mentor.getJobOverview(name)
1358
- : await mentor.getWorkflowOverview(name);
1385
+ const overview = await mentor.getJobOverview(name);
1359
1386
  if (overview) {
1360
- this.log(`✅ Local override found for ${toolName}: ${name}`);
1387
+ this.log(`✅ Local override found for get_fraim_job: ${name}`);
1361
1388
  let responseText = overview.overview;
1362
1389
  if (!overview.isSimple) {
1363
1390
  const phaseAuthority = await mentor.getPhaseAuthorityContent();
1364
1391
  if (phaseAuthority)
1365
1392
  responseText = `${phaseAuthority}\n\n---\n\n${responseText}`;
1366
- responseText += `\n\n---\n\n**This ${isJob ? 'job' : 'workflow'} has phases.** Use \`seekMentoring\` to get phase-specific instructions.`;
1393
+ responseText += `\n\n---\n\n**This job has phases.** Use \`seekMentoring\` to get phase-specific instructions.`;
1394
+ }
1395
+ // Inject local learning context for job requests (RFC 177).
1396
+ const userEmail = this.ensureEngine().getUserEmail();
1397
+ if (userEmail) {
1398
+ const workspaceRoot = this.findProjectRoot() || process.cwd();
1399
+ const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true);
1400
+ if (learningSection) {
1401
+ responseText += `\n\n---` + learningSection;
1402
+ this.log(`✅ Injected job-focus learning context for ${userEmail} (local override path)`);
1403
+ }
1367
1404
  }
1368
1405
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, responseText);
1369
1406
  }
1370
1407
  }
1371
1408
  catch (error) {
1372
- this.log(`⚠️ Local ${toolName} failed for ${name}: ${error.message}. Falling back to remote.`);
1409
+ this.log(`⚠️ Local get_fraim_job failed for ${name}: ${error.message}. Falling back to remote.`);
1373
1410
  }
1374
1411
  }
1375
1412
  }
1376
- // DISCOVERY AGGREGATION: Merge local and remote jobs/workflows
1377
- if (toolName === 'list_fraim_jobs' || toolName === 'list_fraim_workflows') {
1378
- const isJob = toolName === 'list_fraim_jobs';
1413
+ // DISCOVERY AGGREGATION: Merge local and remote jobs
1414
+ if (toolName === 'list_fraim_jobs') {
1379
1415
  const response = await this._doProxyToRemote(request, requestId);
1380
1416
  if (!response.error && response.result?.content?.[0]?.text) {
1381
1417
  try {
1382
1418
  const resolver = this.getRegistryResolver(requestSessionId);
1383
- const localItems = await resolver.listItems(isJob ? 'job' : 'workflow');
1419
+ const localItems = await resolver.listItems('job');
1384
1420
  if (localItems.length > 0) {
1385
- this.log(`📦 Aggregating ${localItems.length} local ${isJob ? 'jobs' : 'workflows'} into remote response`);
1421
+ this.log(`📦 Aggregating ${localItems.length} local jobs into remote response`);
1386
1422
  let combinedText = response.result.content[0].text;
1387
- combinedText += `\n\n## Local & Personalized ${isJob ? 'Jobs' : 'Workflows'} (.fraim/)\n\n`;
1423
+ combinedText += `\n\n## Local & Personalized Jobs (${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)()})\n\n`;
1388
1424
  for (const item of localItems) {
1389
1425
  combinedText += `- **${item.name}**: ${item.description || '(No description available)'}\n`;
1390
1426
  }
@@ -1600,14 +1636,33 @@ class FraimLocalMCPServer {
1600
1636
  if (toolName && requestSessionId) {
1601
1637
  const success = !response.error;
1602
1638
  this.log(`📊 Collecting usage: ${toolName} (session: ${requestSessionId}, success: ${success})`);
1603
- this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1639
+ // Capture the current queue size before collection
1640
+ const beforeCount = this.usageCollector.getEventCount();
1641
+ try {
1642
+ this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1643
+ // Check if the event was actually added to the queue
1644
+ const afterCount = this.usageCollector.getEventCount();
1645
+ if (afterCount > beforeCount) {
1646
+ this.log(`📊 ✅ Event queued successfully (queue: ${afterCount})`);
1647
+ }
1648
+ else {
1649
+ this.log(`📊 ⚠️ Event not queued - tool may not be tracked: ${toolName}`);
1650
+ // Log the args for debugging path parsing issues
1651
+ if (toolName === 'get_fraim_file' && args.path) {
1652
+ this.log(`📊 🔍 Debug: get_fraim_file path="${args.path}"`);
1653
+ }
1654
+ }
1655
+ }
1656
+ catch (error) {
1657
+ this.log(`📊 ❌ Usage collection error: ${error.message}`);
1658
+ }
1604
1659
  }
1605
1660
  else if (request.method === 'tools/call') {
1606
1661
  this.log(`⚠️ Skipping usage collection: toolName=${toolName}, sessionId=${requestSessionId}`);
1607
1662
  }
1608
1663
  }
1609
1664
  /**
1610
- * Flush collected usage data to the local database
1665
+ * Flush collected usage data to the remote server
1611
1666
  */
1612
1667
  async uploadUsageData() {
1613
1668
  const eventCount = this.usageCollector.getEventCount();
@@ -1616,11 +1671,16 @@ class FraimLocalMCPServer {
1616
1671
  return; // Nothing to flush
1617
1672
  }
1618
1673
  try {
1674
+ this.log(`📊 Attempting to flush ${eventCount} events to ${this.remoteUrl}/api/analytics/events`);
1619
1675
  await this.usageCollector.flush(this.remoteUrl, this.apiKey);
1620
- this.log(`📊 Flushed ${eventCount} usage events to remote server`);
1676
+ this.log(`📊 Successfully flushed ${eventCount} usage events to remote server`);
1621
1677
  }
1622
1678
  catch (error) {
1623
- this.log(`❌ Usage flushing error: ${error.message}`);
1679
+ this.log(`📊 Usage flushing error: ${error.message}`);
1680
+ // Log additional details for debugging
1681
+ if (error.response) {
1682
+ this.log(`📊 🔍 HTTP Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data)}`);
1683
+ }
1624
1684
  }
1625
1685
  }
1626
1686
  }
@@ -18,6 +18,22 @@ class UsageCollector {
18
18
  this.events = [];
19
19
  this.apiKeyId = null;
20
20
  }
21
+ static resolveMentoringJobName(args) {
22
+ if (!args || typeof args !== 'object') {
23
+ return 'unknown';
24
+ }
25
+ if (typeof args.jobName === 'string' && args.jobName.trim().length > 0) {
26
+ return args.jobName.trim();
27
+ }
28
+ // Backward compatibility for older callers and docs that still use workflowType.
29
+ if (typeof args.workflowType === 'string' && args.workflowType.trim().length > 0) {
30
+ return args.workflowType.trim();
31
+ }
32
+ if (typeof args.job === 'string' && args.job.trim().length > 0) {
33
+ return args.job.trim();
34
+ }
35
+ return 'unknown';
36
+ }
21
37
  /**
22
38
  * Set the API key ID for this session
23
39
  */
@@ -27,12 +43,17 @@ class UsageCollector {
27
43
  /**
28
44
  * Collect MCP tool call event
29
45
  */
30
- collectMCPCall(toolName, args, sessionId, success = true, duration) {
46
+ collectMCPCall(toolName, args, sessionId, success = true) {
31
47
  const parsed = this.parseMCPCall(toolName, args);
32
48
  if (!parsed) {
33
- console.error(`[UsageCollector] 🚫 Tool not tracked: ${toolName}`);
49
+ const errorMsg = `[UsageCollector] 🚫 Tool not tracked: ${toolName}`;
50
+ console.error(errorMsg);
51
+ // Also log to stderr for better visibility in main logs
52
+ process.stderr.write(errorMsg + '\n');
34
53
  return;
35
54
  }
55
+ // Extract useful args for analytics
56
+ const analyticsArgs = this.extractAnalyticsArgs(toolName, args);
36
57
  const event = {
37
58
  type: parsed.type,
38
59
  name: parsed.name,
@@ -40,18 +61,19 @@ class UsageCollector {
40
61
  // The server will override this with the correct value from the auth token.
41
62
  apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
42
63
  sessionId,
43
- success
64
+ success,
65
+ args: analyticsArgs
44
66
  };
45
- if (duration !== undefined) {
46
- event.duration = duration;
47
- }
48
67
  this.events.push(event);
49
- console.error(`[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`);
68
+ const successMsg = `[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`;
69
+ console.error(successMsg);
70
+ // Also log to stderr for better visibility in main logs
71
+ process.stderr.write(successMsg + '\n');
50
72
  }
51
73
  /**
52
74
  * Collect usage event directly (for backward compatibility with tests)
53
75
  */
54
- collectEvent(type, name, sessionId, success = true, duration) {
76
+ collectEvent(type, name, sessionId, success = true, args) {
55
77
  const event = {
56
78
  type,
57
79
  name,
@@ -59,13 +81,57 @@ class UsageCollector {
59
81
  // The server will override this with the correct value from the auth token.
60
82
  apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
61
83
  sessionId,
62
- success
84
+ success,
85
+ args
63
86
  };
64
- if (duration !== undefined) {
65
- event.duration = duration;
66
- }
67
87
  this.events.push(event);
68
88
  }
89
+ /**
90
+ * Extract useful arguments for analytics tracking
91
+ */
92
+ extractAnalyticsArgs(toolName, args) {
93
+ const analyticsArgs = {};
94
+ switch (toolName) {
95
+ case 'seekMentoring':
96
+ if (args.currentPhase)
97
+ analyticsArgs.currentPhase = args.currentPhase;
98
+ if (args.status)
99
+ analyticsArgs.status = args.status;
100
+ const mentoringJobName = UsageCollector.resolveMentoringJobName(args);
101
+ if (mentoringJobName !== 'unknown')
102
+ analyticsArgs.jobName = mentoringJobName;
103
+ if (args.issueNumber)
104
+ analyticsArgs.issueNumber = args.issueNumber;
105
+ break;
106
+ case 'get_fraim_file':
107
+ if (args.path) {
108
+ analyticsArgs.path = args.path;
109
+ // Extract category and subcategory from path
110
+ const pathParts = args.path.split('/');
111
+ if (pathParts.length >= 2) {
112
+ analyticsArgs.category = pathParts[1]; // skills, jobs, rules, workflows
113
+ if (pathParts.length >= 3) {
114
+ analyticsArgs.subcategory = pathParts[2]; // engineering, product-building, etc.
115
+ }
116
+ }
117
+ }
118
+ break;
119
+ case 'get_fraim_job':
120
+ if (args.job)
121
+ analyticsArgs.job = args.job;
122
+ break;
123
+ case 'fraim_connect':
124
+ if (args.agent?.name)
125
+ analyticsArgs.agentName = args.agent.name;
126
+ if (args.agent?.model)
127
+ analyticsArgs.agentModel = args.agent.model;
128
+ break;
129
+ default:
130
+ // For other tools, don't collect args to avoid PII
131
+ return undefined;
132
+ }
133
+ return Object.keys(analyticsArgs).length > 0 ? analyticsArgs : undefined;
134
+ }
69
135
  /**
70
136
  * Parse MCP tool call to extract usage analytics info
71
137
  */
@@ -79,7 +145,11 @@ class UsageCollector {
79
145
  }
80
146
  return null;
81
147
  case 'seekMentoring':
82
- return { type: 'mentoring', name: args.workflowType || 'unknown' };
148
+ return { type: 'mentoring', name: UsageCollector.resolveMentoringJobName(args) };
149
+ case 'list_fraim_jobs':
150
+ return { type: 'job', name: 'list' };
151
+ case 'fraim_connect':
152
+ return { type: 'session', name: 'connect' };
83
153
  default:
84
154
  return null;
85
155
  }
@@ -103,12 +173,16 @@ class UsageCollector {
103
173
  */
104
174
  async flush(remoteUrl, apiKey) {
105
175
  if (this.events.length === 0) {
106
- console.error(`[UsageCollector] 📊 No events to flush`);
176
+ const noEventsMsg = `[UsageCollector] 📊 No events to flush`;
177
+ console.error(noEventsMsg);
178
+ process.stderr.write(noEventsMsg + '\n');
107
179
  return;
108
180
  }
109
181
  const events = [...this.events];
110
182
  this.events = [];
111
- console.error(`[UsageCollector] 📤 Flushing ${events.length} events to ${remoteUrl}/api/analytics/events`);
183
+ const flushMsg = `[UsageCollector] 📤 Flushing ${events.length} events to ${remoteUrl}/api/analytics/events`;
184
+ console.error(flushMsg);
185
+ process.stderr.write(flushMsg + '\n');
112
186
  try {
113
187
  const response = await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
114
188
  events,
@@ -120,15 +194,27 @@ class UsageCollector {
120
194
  'Content-Type': 'application/json'
121
195
  }
122
196
  });
123
- console.error(`[UsageCollector] ✅ Successfully flushed ${events.length} events (HTTP ${response.status})`);
197
+ const successMsg = `[UsageCollector] ✅ Successfully flushed ${events.length} events (HTTP ${response.status})`;
198
+ console.error(successMsg);
199
+ process.stderr.write(successMsg + '\n');
124
200
  // Success - events are already cleared from the queue
125
201
  }
126
202
  catch (error) {
127
203
  const status = error.response?.status;
128
204
  const message = error.response?.data?.error || error.message;
129
- console.error(`[UsageCollector] ❌ Failed to flush usage events (HTTP ${status}): ${message}`);
205
+ const errorMsg = `[UsageCollector] ❌ Failed to flush usage events (HTTP ${status}): ${message}`;
206
+ console.error(errorMsg);
207
+ process.stderr.write(errorMsg + '\n');
208
+ // Log additional debug info
209
+ if (error.response?.data) {
210
+ const debugMsg = `[UsageCollector] 🔍 Response data: ${JSON.stringify(error.response.data)}`;
211
+ console.error(debugMsg);
212
+ process.stderr.write(debugMsg + '\n');
213
+ }
130
214
  // Put events back at the beginning of the queue for next try
131
215
  this.events = [...events, ...this.events];
216
+ // Re-throw the error so the main server can log it too
217
+ throw error;
132
218
  }
133
219
  }
134
220
  /**
@@ -141,24 +227,37 @@ class UsageCollector {
141
227
  * Parse component name from file path
142
228
  */
143
229
  static parseComponentName(path) {
144
- if (path.includes('/jobs/')) {
145
- const jobMatch = path.match(/\/jobs\/[^/]+\/[^/]+\/([^/]+)\.md$/);
146
- if (jobMatch) {
147
- return { type: 'job', name: jobMatch[1] };
148
- }
149
- }
150
- if (path.includes('/skills/')) {
151
- const skillMatch = path.match(/\/skills\/[^/]+\/([^/]+)\.md$/);
230
+ // Normalize path to have leading slash for consistent regex matching
231
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
232
+ // Match skills files: /skills/category/skill-name.md
233
+ if (normalizedPath.includes('/skills/')) {
234
+ const skillMatch = normalizedPath.match(/\/skills\/[^/]+\/([^/]+)\.md$/);
152
235
  if (skillMatch) {
153
236
  return { type: 'skill', name: skillMatch[1] };
154
237
  }
155
238
  }
156
- if (path.includes('/rules/')) {
157
- const ruleMatch = path.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
239
+ // Match job files: /jobs/category/subcategory/job-name.md
240
+ if (normalizedPath.includes('/jobs/')) {
241
+ const jobMatch = normalizedPath.match(/\/jobs\/[^/]+\/[^/]+\/([^/]+)\.md$/);
242
+ if (jobMatch) {
243
+ return { type: 'job', name: jobMatch[1] };
244
+ }
245
+ }
246
+ // Match rule files: /rules/category/rule-name.md
247
+ if (normalizedPath.includes('/rules/')) {
248
+ const ruleMatch = normalizedPath.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
158
249
  if (ruleMatch) {
159
250
  return { type: 'rule', name: ruleMatch[1] };
160
251
  }
161
252
  }
253
+ // For other paths that don't match the expected patterns,
254
+ // we'll classify them as 'job' type with a generic name
255
+ // This ensures we track usage even for non-standard paths
256
+ const fileName = normalizedPath.split('/').pop();
257
+ if (fileName && fileName.endsWith('.md')) {
258
+ const baseName = fileName.replace(/\.md$/, '');
259
+ return { type: 'job', name: baseName };
260
+ }
162
261
  return null;
163
262
  }
164
263
  }
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.96",
3
+ "version": "2.0.98",
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": {
@@ -35,10 +35,10 @@
35
35
  "postinstall": "fraim sync --skip-updates || echo 'FRAIM setup skipped.'",
36
36
  "prepublishOnly": "npm run build",
37
37
  "release": "npm version patch && npm publish",
38
- "validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows && npm run validate:skills && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens && npm run validate:brain-mapping && npm run validate:template-syntax",
38
+ "validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:jobs && npm run validate:skills && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens && npm run validate:brain-mapping && npm run validate:template-syntax",
39
39
  "validate:brain-mapping": "tsx scripts/validate-brain-mapping.ts",
40
40
  "validate:fraim-pro-assets": "tsx scripts/validate-fraim-pro-assets.ts",
41
- "validate:workflows": "tsx scripts/validate-workflows.ts",
41
+ "validate:jobs": "tsx scripts/validate-jobs.ts",
42
42
  "validate:platform-agnostic": "tsx scripts/validate-platform-agnostic.ts",
43
43
  "validate:skills": "tsx scripts/validate-skills.ts",
44
44
  "validate:template-namespaces": "tsx scripts/validate-template-namespaces.ts",
@@ -60,7 +60,6 @@
60
60
  "ai-agents",
61
61
  "multi-agent",
62
62
  "github",
63
- "workflow",
64
63
  "automation",
65
64
  "gitops",
66
65
  "cursor",