fraim-framework 2.0.95 → 2.0.97

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 +124 -51
  38. package/dist/src/local-mcp-server/usage-collector.js +109 -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
  }
@@ -1411,15 +1447,6 @@ class FraimLocalMCPServer {
1411
1447
  }
1412
1448
  const response = await this._doProxyToRemote(injectedRequest, requestId);
1413
1449
  const processedResponse = await this.finalizeToolResponse(injectedRequest, response, requestSessionId, requestId);
1414
- // Single point for usage tracking - log all tool calls
1415
- if (injectedRequest.method === 'tools/call' && requestSessionId && toolName) {
1416
- const success = !processedResponse.error;
1417
- this.log(`📊 Collecting usage: ${toolName} (session: ${requestSessionId}, success: ${success})`);
1418
- this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1419
- }
1420
- else if (injectedRequest.method === 'tools/call') {
1421
- this.log(`⚠️ Skipping usage collection: toolName=${toolName}, sessionId=${requestSessionId}`);
1422
- }
1423
1450
  this.log(`📤 ${injectedRequest.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1424
1451
  return processedResponse;
1425
1452
  }
@@ -1540,6 +1567,8 @@ class FraimLocalMCPServer {
1540
1567
  const response = await this.handleRequest(message);
1541
1568
  // Only send response if we got one (null means we handled it internally)
1542
1569
  if (response) {
1570
+ // Collect usage for all tools/call requests before sending response
1571
+ this.collectUsageForResponse(message, response);
1543
1572
  process.stdout.write(JSON.stringify(response) + '\n');
1544
1573
  }
1545
1574
  }
@@ -1594,7 +1623,46 @@ class FraimLocalMCPServer {
1594
1623
  this.log('✅ FRAIM Local MCP Server ready');
1595
1624
  }
1596
1625
  /**
1597
- * Flush collected usage data to the local database
1626
+ * Collect usage analytics for tools/call requests
1627
+ */
1628
+ collectUsageForResponse(request, response) {
1629
+ // Only collect usage for tools/call requests
1630
+ if (request.method !== 'tools/call') {
1631
+ return;
1632
+ }
1633
+ const toolName = request.params?.name;
1634
+ const args = request.params?.arguments || {};
1635
+ const requestSessionId = this.extractSessionIdFromRequest(request);
1636
+ if (toolName && requestSessionId) {
1637
+ const success = !response.error;
1638
+ this.log(`📊 Collecting usage: ${toolName} (session: ${requestSessionId}, success: ${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
+ }
1659
+ }
1660
+ else if (request.method === 'tools/call') {
1661
+ this.log(`⚠️ Skipping usage collection: toolName=${toolName}, sessionId=${requestSessionId}`);
1662
+ }
1663
+ }
1664
+ /**
1665
+ * Flush collected usage data to the remote server
1598
1666
  */
1599
1667
  async uploadUsageData() {
1600
1668
  const eventCount = this.usageCollector.getEventCount();
@@ -1603,11 +1671,16 @@ class FraimLocalMCPServer {
1603
1671
  return; // Nothing to flush
1604
1672
  }
1605
1673
  try {
1674
+ this.log(`📊 Attempting to flush ${eventCount} events to ${this.remoteUrl}/api/analytics/events`);
1606
1675
  await this.usageCollector.flush(this.remoteUrl, this.apiKey);
1607
- this.log(`📊 Flushed ${eventCount} usage events to remote server`);
1676
+ this.log(`📊 Successfully flushed ${eventCount} usage events to remote server`);
1608
1677
  }
1609
1678
  catch (error) {
1610
- 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
+ }
1611
1684
  }
1612
1685
  }
1613
1686
  }
@@ -27,12 +27,17 @@ class UsageCollector {
27
27
  /**
28
28
  * Collect MCP tool call event
29
29
  */
30
- collectMCPCall(toolName, args, sessionId, success = true, duration) {
30
+ collectMCPCall(toolName, args, sessionId, success = true) {
31
31
  const parsed = this.parseMCPCall(toolName, args);
32
32
  if (!parsed) {
33
- console.error(`[UsageCollector] 🚫 Tool not tracked: ${toolName}`);
33
+ const errorMsg = `[UsageCollector] 🚫 Tool not tracked: ${toolName}`;
34
+ console.error(errorMsg);
35
+ // Also log to stderr for better visibility in main logs
36
+ process.stderr.write(errorMsg + '\n');
34
37
  return;
35
38
  }
39
+ // Extract useful args for analytics
40
+ const analyticsArgs = this.extractAnalyticsArgs(toolName, args);
36
41
  const event = {
37
42
  type: parsed.type,
38
43
  name: parsed.name,
@@ -40,18 +45,19 @@ class UsageCollector {
40
45
  // The server will override this with the correct value from the auth token.
41
46
  apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
42
47
  sessionId,
43
- success
48
+ success,
49
+ args: analyticsArgs
44
50
  };
45
- if (duration !== undefined) {
46
- event.duration = duration;
47
- }
48
51
  this.events.push(event);
49
- console.error(`[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`);
52
+ const successMsg = `[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`;
53
+ console.error(successMsg);
54
+ // Also log to stderr for better visibility in main logs
55
+ process.stderr.write(successMsg + '\n');
50
56
  }
51
57
  /**
52
58
  * Collect usage event directly (for backward compatibility with tests)
53
59
  */
54
- collectEvent(type, name, sessionId, success = true, duration) {
60
+ collectEvent(type, name, sessionId, success = true, args) {
55
61
  const event = {
56
62
  type,
57
63
  name,
@@ -59,13 +65,56 @@ class UsageCollector {
59
65
  // The server will override this with the correct value from the auth token.
60
66
  apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
61
67
  sessionId,
62
- success
68
+ success,
69
+ args
63
70
  };
64
- if (duration !== undefined) {
65
- event.duration = duration;
66
- }
67
71
  this.events.push(event);
68
72
  }
73
+ /**
74
+ * Extract useful arguments for analytics tracking
75
+ */
76
+ extractAnalyticsArgs(toolName, args) {
77
+ const analyticsArgs = {};
78
+ switch (toolName) {
79
+ case 'seekMentoring':
80
+ if (args.currentPhase)
81
+ analyticsArgs.currentPhase = args.currentPhase;
82
+ if (args.status)
83
+ analyticsArgs.status = args.status;
84
+ if (args.jobName)
85
+ analyticsArgs.jobName = args.jobName;
86
+ if (args.issueNumber)
87
+ analyticsArgs.issueNumber = args.issueNumber;
88
+ break;
89
+ case 'get_fraim_file':
90
+ if (args.path) {
91
+ analyticsArgs.path = args.path;
92
+ // Extract category and subcategory from path
93
+ const pathParts = args.path.split('/');
94
+ if (pathParts.length >= 2) {
95
+ analyticsArgs.category = pathParts[1]; // skills, jobs, rules, workflows
96
+ if (pathParts.length >= 3) {
97
+ analyticsArgs.subcategory = pathParts[2]; // engineering, product-building, etc.
98
+ }
99
+ }
100
+ }
101
+ break;
102
+ case 'get_fraim_job':
103
+ if (args.job)
104
+ analyticsArgs.job = args.job;
105
+ break;
106
+ case 'fraim_connect':
107
+ if (args.agent?.name)
108
+ analyticsArgs.agentName = args.agent.name;
109
+ if (args.agent?.model)
110
+ analyticsArgs.agentModel = args.agent.model;
111
+ break;
112
+ default:
113
+ // For other tools, don't collect args to avoid PII
114
+ return undefined;
115
+ }
116
+ return Object.keys(analyticsArgs).length > 0 ? analyticsArgs : undefined;
117
+ }
69
118
  /**
70
119
  * Parse MCP tool call to extract usage analytics info
71
120
  */
@@ -79,7 +128,11 @@ class UsageCollector {
79
128
  }
80
129
  return null;
81
130
  case 'seekMentoring':
82
- return { type: 'mentoring', name: args.workflowType || 'unknown' };
131
+ return { type: 'mentoring', name: args.jobName || 'unknown' };
132
+ case 'list_fraim_jobs':
133
+ return { type: 'job', name: 'list' };
134
+ case 'fraim_connect':
135
+ return { type: 'session', name: 'connect' };
83
136
  default:
84
137
  return null;
85
138
  }
@@ -103,12 +156,16 @@ class UsageCollector {
103
156
  */
104
157
  async flush(remoteUrl, apiKey) {
105
158
  if (this.events.length === 0) {
106
- console.error(`[UsageCollector] 📊 No events to flush`);
159
+ const noEventsMsg = `[UsageCollector] 📊 No events to flush`;
160
+ console.error(noEventsMsg);
161
+ process.stderr.write(noEventsMsg + '\n');
107
162
  return;
108
163
  }
109
164
  const events = [...this.events];
110
165
  this.events = [];
111
- console.error(`[UsageCollector] 📤 Flushing ${events.length} events to ${remoteUrl}/api/analytics/events`);
166
+ const flushMsg = `[UsageCollector] 📤 Flushing ${events.length} events to ${remoteUrl}/api/analytics/events`;
167
+ console.error(flushMsg);
168
+ process.stderr.write(flushMsg + '\n');
112
169
  try {
113
170
  const response = await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
114
171
  events,
@@ -120,15 +177,27 @@ class UsageCollector {
120
177
  'Content-Type': 'application/json'
121
178
  }
122
179
  });
123
- console.error(`[UsageCollector] ✅ Successfully flushed ${events.length} events (HTTP ${response.status})`);
180
+ const successMsg = `[UsageCollector] ✅ Successfully flushed ${events.length} events (HTTP ${response.status})`;
181
+ console.error(successMsg);
182
+ process.stderr.write(successMsg + '\n');
124
183
  // Success - events are already cleared from the queue
125
184
  }
126
185
  catch (error) {
127
186
  const status = error.response?.status;
128
187
  const message = error.response?.data?.error || error.message;
129
- console.error(`[UsageCollector] ❌ Failed to flush usage events (HTTP ${status}): ${message}`);
188
+ const errorMsg = `[UsageCollector] ❌ Failed to flush usage events (HTTP ${status}): ${message}`;
189
+ console.error(errorMsg);
190
+ process.stderr.write(errorMsg + '\n');
191
+ // Log additional debug info
192
+ if (error.response?.data) {
193
+ const debugMsg = `[UsageCollector] 🔍 Response data: ${JSON.stringify(error.response.data)}`;
194
+ console.error(debugMsg);
195
+ process.stderr.write(debugMsg + '\n');
196
+ }
130
197
  // Put events back at the beginning of the queue for next try
131
198
  this.events = [...events, ...this.events];
199
+ // Re-throw the error so the main server can log it too
200
+ throw error;
132
201
  }
133
202
  }
134
203
  /**
@@ -141,24 +210,37 @@ class UsageCollector {
141
210
  * Parse component name from file path
142
211
  */
143
212
  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$/);
213
+ // Normalize path to have leading slash for consistent regex matching
214
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
215
+ // Match skills files: /skills/category/skill-name.md
216
+ if (normalizedPath.includes('/skills/')) {
217
+ const skillMatch = normalizedPath.match(/\/skills\/[^/]+\/([^/]+)\.md$/);
152
218
  if (skillMatch) {
153
219
  return { type: 'skill', name: skillMatch[1] };
154
220
  }
155
221
  }
156
- if (path.includes('/rules/')) {
157
- const ruleMatch = path.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
222
+ // Match job files: /jobs/category/subcategory/job-name.md
223
+ if (normalizedPath.includes('/jobs/')) {
224
+ const jobMatch = normalizedPath.match(/\/jobs\/[^/]+\/[^/]+\/([^/]+)\.md$/);
225
+ if (jobMatch) {
226
+ return { type: 'job', name: jobMatch[1] };
227
+ }
228
+ }
229
+ // Match rule files: /rules/category/rule-name.md
230
+ if (normalizedPath.includes('/rules/')) {
231
+ const ruleMatch = normalizedPath.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
158
232
  if (ruleMatch) {
159
233
  return { type: 'rule', name: ruleMatch[1] };
160
234
  }
161
235
  }
236
+ // For other paths that don't match the expected patterns,
237
+ // we'll classify them as 'job' type with a generic name
238
+ // This ensures we track usage even for non-standard paths
239
+ const fileName = normalizedPath.split('/').pop();
240
+ if (fileName && fileName.endsWith('.md')) {
241
+ const baseName = fileName.replace(/\.md$/, '');
242
+ return { type: 'job', name: baseName };
243
+ }
162
244
  return null;
163
245
  }
164
246
  }
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.95",
3
+ "version": "2.0.97",
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",