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.
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-ide.js +1 -1
- package/dist/src/cli/commands/doctor.js +6 -6
- package/dist/src/cli/commands/init-project.js +63 -52
- package/dist/src/cli/commands/list-overridable.js +33 -55
- package/dist/src/cli/commands/list.js +35 -9
- package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
- package/dist/src/cli/commands/override.js +18 -39
- package/dist/src/cli/commands/setup.js +1 -1
- package/dist/src/cli/commands/sync.js +34 -27
- package/dist/src/cli/doctor/check-runner.js +3 -3
- package/dist/src/cli/doctor/checks/global-setup-checks.js +13 -13
- package/dist/src/cli/doctor/checks/project-setup-checks.js +12 -12
- package/dist/src/cli/doctor/checks/scripts-checks.js +2 -2
- package/dist/src/cli/doctor/checks/workflow-checks.js +56 -60
- package/dist/src/cli/doctor/reporters/console-reporter.js +1 -1
- package/dist/src/cli/fraim.js +3 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -1
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +2 -2
- package/dist/src/cli/setup/first-run.js +4 -3
- package/dist/src/cli/utils/agent-adapters.js +126 -0
- package/dist/src/cli/utils/fraim-gitignore.js +15 -21
- package/dist/src/cli/utils/project-bootstrap.js +93 -0
- package/dist/src/cli/utils/remote-sync.js +20 -67
- package/dist/src/core/ai-mentor.js +31 -49
- package/dist/src/core/config-loader.js +57 -62
- package/dist/src/core/config-writer.js +75 -0
- package/dist/src/core/types.js +1 -1
- package/dist/src/core/utils/job-parser.js +176 -0
- package/dist/src/core/utils/local-registry-resolver.js +61 -71
- package/dist/src/core/utils/project-fraim-migration.js +103 -0
- package/dist/src/core/utils/project-fraim-paths.js +38 -0
- package/dist/src/core/utils/stub-generator.js +41 -75
- package/dist/src/core/utils/workflow-parser.js +5 -3
- package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
- package/dist/src/local-mcp-server/stdio-server.js +124 -51
- package/dist/src/local-mcp-server/usage-collector.js +109 -27
- package/index.js +1 -1
- 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,
|
|
353
|
-
this.log(
|
|
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,
|
|
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
|
|
374
|
+
// Skip the home directory FRAIM dir and continue searching for a project-specific one
|
|
370
375
|
if (homeDir && currentDir === homeDir) {
|
|
371
|
-
this.log(`
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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 =
|
|
844
|
-
|
|
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
|
-
|
|
848
|
-
|
|
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
|
|
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
|
-
|
|
877
|
-
this.
|
|
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:
|
|
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.
|
|
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'
|
|
1351
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1377
|
-
if (toolName === 'list_fraim_jobs'
|
|
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(
|
|
1419
|
+
const localItems = await resolver.listItems('job');
|
|
1384
1420
|
if (localItems.length > 0) {
|
|
1385
|
-
this.log(`📦 Aggregating ${localItems.length} local
|
|
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 ${
|
|
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
|
-
*
|
|
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(`📊
|
|
1676
|
+
this.log(`📊 ✅ Successfully flushed ${eventCount} usage events to remote server`);
|
|
1608
1677
|
}
|
|
1609
1678
|
catch (error) {
|
|
1610
|
-
this.log(
|
|
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
|
|
30
|
+
collectMCPCall(toolName, args, sessionId, success = true) {
|
|
31
31
|
const parsed = this.parseMCPCall(toolName, args);
|
|
32
32
|
if (!parsed) {
|
|
33
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
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:
|
|
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:
|
|
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",
|