fraim-framework 2.0.90 ā 2.0.92
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/mcp.js +15 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +2 -1
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/core/config-loader.js +5 -0
- package/dist/src/core/types.js +2 -0
- package/dist/src/core/utils/git-utils.js +1 -1
- package/dist/src/core/utils/stub-generator.js +47 -47
- package/dist/src/local-mcp-server/stdio-server.js +42 -1
- package/dist/src/local-mcp-server/usage-collector.js +156 -0
- package/index.js +1 -1
- package/package.json +13 -7
- package/bin/fraim-mcp.js +0 -63
package/bin/fraim.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mcpCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const stdio_server_1 = require("../../local-mcp-server/stdio-server");
|
|
6
|
+
/**
|
|
7
|
+
* mcpCommand - Entry point for starting the local FRAIM MCP server.
|
|
8
|
+
* This integrates the local-mcp-server into the main CLI.
|
|
9
|
+
*/
|
|
10
|
+
exports.mcpCommand = new commander_1.Command('mcp')
|
|
11
|
+
.description('Start the local FRAIM MCP server (stdio)')
|
|
12
|
+
.action(() => {
|
|
13
|
+
const server = new stdio_server_1.FraimLocalMCPServer();
|
|
14
|
+
server.start();
|
|
15
|
+
});
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -48,6 +48,7 @@ const add_provider_1 = require("./commands/add-provider");
|
|
|
48
48
|
const override_1 = require("./commands/override");
|
|
49
49
|
const list_overridable_1 = require("./commands/list-overridable");
|
|
50
50
|
const login_1 = require("./commands/login");
|
|
51
|
+
const mcp_1 = require("./commands/mcp");
|
|
51
52
|
const fs_1 = __importDefault(require("fs"));
|
|
52
53
|
const path_1 = __importDefault(require("path"));
|
|
53
54
|
const program = new commander_1.Command();
|
|
@@ -85,6 +86,7 @@ program.addCommand(add_provider_1.addProviderCommand);
|
|
|
85
86
|
program.addCommand(override_1.overrideCommand);
|
|
86
87
|
program.addCommand(list_overridable_1.listOverridableCommand);
|
|
87
88
|
program.addCommand(login_1.loginCommand);
|
|
89
|
+
program.addCommand(mcp_1.mcpCommand);
|
|
88
90
|
// Wait for async command initialization before parsing
|
|
89
91
|
(async () => {
|
|
90
92
|
// Import the initialization promise from setup command
|
|
@@ -36,7 +36,8 @@ exports.BASE_MCP_SERVERS = [
|
|
|
36
36
|
name: 'FRAIM',
|
|
37
37
|
description: 'FRAIM workflow orchestration and mentoring',
|
|
38
38
|
buildServer: (fraimKey) => ({
|
|
39
|
-
command: '
|
|
39
|
+
command: 'npx',
|
|
40
|
+
args: ['-y', 'fraim-framework@latest', 'mcp'],
|
|
40
41
|
env: {
|
|
41
42
|
// Include API key for IDE configs (Codex, VSCode, etc.)
|
|
42
43
|
// The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DeviceFlowService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
class DeviceFlowService {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start the Device Flow Login
|
|
15
|
+
*/
|
|
16
|
+
async login() {
|
|
17
|
+
console.log(chalk_1.default.blue('\nš Starting Authentication...'));
|
|
18
|
+
try {
|
|
19
|
+
// 1. Request device and user codes
|
|
20
|
+
const deviceCode = await this.requestDeviceCode();
|
|
21
|
+
console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
|
|
22
|
+
console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
|
|
23
|
+
console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
|
|
24
|
+
console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
|
|
25
|
+
// 2. Poll for the access token
|
|
26
|
+
const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
|
|
27
|
+
console.log(chalk_1.default.green('\nā
Authentication Successful!'));
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk_1.default.red(`\nā Authentication failed: ${error.message}`));
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async requestDeviceCode() {
|
|
36
|
+
const response = await axios_1.default.post(this.config.authUrl, {
|
|
37
|
+
client_id: this.config.clientId,
|
|
38
|
+
scope: this.config.scope
|
|
39
|
+
}, {
|
|
40
|
+
headers: { Accept: 'application/json' }
|
|
41
|
+
});
|
|
42
|
+
return response.data;
|
|
43
|
+
}
|
|
44
|
+
async pollForToken(deviceCode, interval) {
|
|
45
|
+
let currentInterval = interval * 1000;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const poll = async () => {
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios_1.default.post(this.config.tokenUrl, {
|
|
50
|
+
client_id: this.config.clientId,
|
|
51
|
+
device_code: deviceCode,
|
|
52
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
53
|
+
}, {
|
|
54
|
+
headers: { Accept: 'application/json' }
|
|
55
|
+
});
|
|
56
|
+
if (response.data.access_token) {
|
|
57
|
+
resolve(response.data.access_token);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (response.data.error) {
|
|
61
|
+
const error = response.data.error;
|
|
62
|
+
if (error === 'authorization_pending') {
|
|
63
|
+
// Keep polling
|
|
64
|
+
setTimeout(poll, currentInterval);
|
|
65
|
+
}
|
|
66
|
+
else if (error === 'slow_down') {
|
|
67
|
+
currentInterval += 5000;
|
|
68
|
+
setTimeout(poll, currentInterval);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reject(new Error(response.data.error_description || error));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
setTimeout(poll, currentInterval);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.DeviceFlowService = DeviceFlowService;
|
|
@@ -46,6 +46,11 @@ function loadFraimConfig() {
|
|
|
46
46
|
...(config.customizations || {})
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
if (config.customizations?.postCleanupHook || config.customizations?.cleanupCommand) {
|
|
50
|
+
if (!mergedConfig.customizations)
|
|
51
|
+
mergedConfig.customizations = {};
|
|
52
|
+
mergedConfig.customizations.postCleanupHook = config.customizations.postCleanupHook || config.customizations.cleanupCommand;
|
|
53
|
+
}
|
|
49
54
|
if (config.issueTracking && typeof config.issueTracking === 'object') {
|
|
50
55
|
mergedConfig.issueTracking = config.issueTracking;
|
|
51
56
|
}
|
package/dist/src/core/types.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* FRAIM Configuration Types
|
|
4
4
|
* TypeScript types for .fraim/config.json
|
|
5
|
+
*
|
|
6
|
+
* Each field includes rich metadata for agent detection and user interaction
|
|
5
7
|
*/
|
|
6
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
9
|
exports.DEFAULT_FRAIM_CONFIG = void 0;
|
|
@@ -31,7 +31,7 @@ function getPort() {
|
|
|
31
31
|
*/
|
|
32
32
|
function determineDatabaseName() {
|
|
33
33
|
try {
|
|
34
|
-
const branchName = process.env.FRAIM_BRANCH || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
34
|
+
const branchName = process.env.FRAIM_BRANCH || process.env.FRAIM_BRANCH_NAME || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
35
35
|
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
36
36
|
if (issueMatch) {
|
|
37
37
|
return `fraim_issue_${issueMatch[1]}`;
|
|
@@ -50,11 +50,11 @@ function extractLeadParagraph(content) {
|
|
|
50
50
|
* These stubs are committed to the user's repo for discoverability.
|
|
51
51
|
*/
|
|
52
52
|
function generateWorkflowStub(workflowName, workflowPath, intent, principles) {
|
|
53
|
-
return `${STUB_MARKER}
|
|
54
|
-
# FRAIM Workflow: ${workflowName}
|
|
55
|
-
|
|
56
|
-
> [!IMPORTANT]
|
|
57
|
-
> This is a **FRAIM-managed workflow stub**.
|
|
53
|
+
return `${STUB_MARKER}
|
|
54
|
+
# FRAIM Workflow: ${workflowName}
|
|
55
|
+
|
|
56
|
+
> [!IMPORTANT]
|
|
57
|
+
> This is a **FRAIM-managed workflow stub**.
|
|
58
58
|
> To load the full context (rules, templates, and execution steps), ask your AI agent to:
|
|
59
59
|
> \`@fraim get_fraim_workflow("${workflowName}")\`
|
|
60
60
|
>
|
|
@@ -81,19 +81,19 @@ function parseRegistryWorkflow(content) {
|
|
|
81
81
|
* Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
|
|
82
82
|
*/
|
|
83
83
|
function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
|
|
84
|
-
return `${STUB_MARKER}
|
|
85
|
-
# FRAIM Job: ${jobName}
|
|
86
|
-
|
|
87
|
-
## Intent
|
|
88
|
-
${intent}
|
|
89
|
-
|
|
90
|
-
## Outcome
|
|
91
|
-
${outcome}
|
|
92
|
-
|
|
93
|
-
## Steps
|
|
94
|
-
${steps}
|
|
95
|
-
|
|
96
|
-
---
|
|
84
|
+
return `${STUB_MARKER}
|
|
85
|
+
# FRAIM Job: ${jobName}
|
|
86
|
+
|
|
87
|
+
## Intent
|
|
88
|
+
${intent}
|
|
89
|
+
|
|
90
|
+
## Outcome
|
|
91
|
+
${outcome}
|
|
92
|
+
|
|
93
|
+
## Steps
|
|
94
|
+
${steps}
|
|
95
|
+
|
|
96
|
+
---
|
|
97
97
|
|
|
98
98
|
> [!IMPORTANT]
|
|
99
99
|
> **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
|
|
@@ -110,41 +110,41 @@ ${steps}
|
|
|
110
110
|
* Generates a lightweight markdown stub for a skill.
|
|
111
111
|
*/
|
|
112
112
|
function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
|
|
113
|
-
return `${STUB_MARKER}
|
|
114
|
-
# FRAIM Skill: ${skillName}
|
|
115
|
-
|
|
116
|
-
## Skill Input
|
|
117
|
-
${skillInput}
|
|
118
|
-
|
|
119
|
-
## Skill Output
|
|
120
|
-
${skillOutput}
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
> [!IMPORTANT]
|
|
125
|
-
> **For AI Agents:** This is a discoverability stub for the skill.
|
|
126
|
-
> All execution details must be fetched from MCP before use.
|
|
127
|
-
> To retrieve the complete skill instructions, call:
|
|
128
|
-
> \`get_fraim_file({ path: "skills/${skillPath}" })\`
|
|
113
|
+
return `${STUB_MARKER}
|
|
114
|
+
# FRAIM Skill: ${skillName}
|
|
115
|
+
|
|
116
|
+
## Skill Input
|
|
117
|
+
${skillInput}
|
|
118
|
+
|
|
119
|
+
## Skill Output
|
|
120
|
+
${skillOutput}
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
> [!IMPORTANT]
|
|
125
|
+
> **For AI Agents:** This is a discoverability stub for the skill.
|
|
126
|
+
> All execution details must be fetched from MCP before use.
|
|
127
|
+
> To retrieve the complete skill instructions, call:
|
|
128
|
+
> \`get_fraim_file({ path: "skills/${skillPath}" })\`
|
|
129
129
|
`;
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* Generates a lightweight markdown stub for a rule.
|
|
133
133
|
*/
|
|
134
134
|
function generateRuleStub(ruleName, rulePath, intent) {
|
|
135
|
-
return `${STUB_MARKER}
|
|
136
|
-
# FRAIM Rule: ${ruleName}
|
|
137
|
-
|
|
138
|
-
## Intent
|
|
139
|
-
${intent}
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
> [!IMPORTANT]
|
|
144
|
-
> **For AI Agents:** This is a discoverability stub for the rule.
|
|
145
|
-
> All rule details must be fetched from MCP before use.
|
|
146
|
-
> To retrieve the complete rule instructions, call:
|
|
147
|
-
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
135
|
+
return `${STUB_MARKER}
|
|
136
|
+
# FRAIM Rule: ${ruleName}
|
|
137
|
+
|
|
138
|
+
## Intent
|
|
139
|
+
${intent}
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
> [!IMPORTANT]
|
|
144
|
+
> **For AI Agents:** This is a discoverability stub for the rule.
|
|
145
|
+
> All rule details must be fetched from MCP before use.
|
|
146
|
+
> To retrieve the complete rule instructions, call:
|
|
147
|
+
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
148
148
|
`;
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
@@ -29,6 +29,7 @@ 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 usage_collector_js_1 = require("./usage-collector.js");
|
|
32
33
|
/**
|
|
33
34
|
* Handle template substitution logic separately for better testability
|
|
34
35
|
*/
|
|
@@ -264,6 +265,9 @@ class FraimLocalMCPServer {
|
|
|
264
265
|
this.log(`š API key: ${this.apiKey.substring(0, 10)}...`);
|
|
265
266
|
this.log(`Local MCP version: ${this.localVersion}`);
|
|
266
267
|
this.log(`š DEBUG BUILD: Machine detection v2 active`);
|
|
268
|
+
// Initialize usage collector
|
|
269
|
+
this.usageCollector = new usage_collector_js_1.UsageCollector();
|
|
270
|
+
this.log('š Usage analytics collector initialized');
|
|
267
271
|
}
|
|
268
272
|
/**
|
|
269
273
|
* Load API key from environment variable or user config file
|
|
@@ -1260,6 +1264,8 @@ class FraimLocalMCPServer {
|
|
|
1260
1264
|
this.engine.setMachineInfo(this.machineInfo);
|
|
1261
1265
|
this.engine.setRepoInfo(this.repoInfo);
|
|
1262
1266
|
}
|
|
1267
|
+
// In a proxy setup, the remote server resolves the API key ID during event upload.
|
|
1268
|
+
// No local resolution needed.
|
|
1263
1269
|
// Update the request with injected info
|
|
1264
1270
|
request.params.arguments = args;
|
|
1265
1271
|
}
|
|
@@ -1402,9 +1408,9 @@ class FraimLocalMCPServer {
|
|
|
1402
1408
|
// Force ALL tools/call requests to return raw definitions so the proxy
|
|
1403
1409
|
// can resolve templates and replace includes locally
|
|
1404
1410
|
const toolName = request.params?.name;
|
|
1411
|
+
const args = request.params?.arguments || {};
|
|
1405
1412
|
let injectedRequest = request;
|
|
1406
1413
|
if (request.method === 'tools/call' && typeof toolName === 'string') {
|
|
1407
|
-
const args = request.params.arguments || {};
|
|
1408
1414
|
// š SMART DISPATCHER: Intercept mentoring and job/workflow tools for local overrides
|
|
1409
1415
|
if (toolName === 'seekMentoring') {
|
|
1410
1416
|
try {
|
|
@@ -1482,6 +1488,11 @@ class FraimLocalMCPServer {
|
|
|
1482
1488
|
}
|
|
1483
1489
|
const response = await this._doProxyToRemote(injectedRequest, requestId);
|
|
1484
1490
|
const processedResponse = await this.finalizeToolResponse(injectedRequest, response, requestSessionId, requestId);
|
|
1491
|
+
// Single point for usage tracking - log all tool calls
|
|
1492
|
+
if (injectedRequest.method === 'tools/call' && requestSessionId && toolName) {
|
|
1493
|
+
const success = !processedResponse.error;
|
|
1494
|
+
this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
|
|
1495
|
+
}
|
|
1485
1496
|
this.log(`š¤ ${injectedRequest.method} ā ${processedResponse.error ? 'ERROR' : 'OK'}`);
|
|
1486
1497
|
return processedResponse;
|
|
1487
1498
|
}
|
|
@@ -1574,6 +1585,17 @@ class FraimLocalMCPServer {
|
|
|
1574
1585
|
start() {
|
|
1575
1586
|
let buffer = '';
|
|
1576
1587
|
process.stdin.setEncoding('utf8');
|
|
1588
|
+
// Set up periodic usage data upload
|
|
1589
|
+
const uploadInterval = setInterval(() => {
|
|
1590
|
+
this.uploadUsageData().catch(error => {
|
|
1591
|
+
this.log(`ā ļø Failed to upload usage data: ${error.message}`);
|
|
1592
|
+
});
|
|
1593
|
+
}, 60000); // Upload every minute
|
|
1594
|
+
// Clean up interval on shutdown
|
|
1595
|
+
const cleanup = () => {
|
|
1596
|
+
clearInterval(uploadInterval);
|
|
1597
|
+
this.usageCollector.shutdown();
|
|
1598
|
+
};
|
|
1577
1599
|
process.stdin.on('data', async (chunk) => {
|
|
1578
1600
|
buffer += chunk;
|
|
1579
1601
|
// Process complete JSON-RPC messages (newline-delimited)
|
|
@@ -1629,21 +1651,40 @@ class FraimLocalMCPServer {
|
|
|
1629
1651
|
});
|
|
1630
1652
|
process.stdin.on('end', () => {
|
|
1631
1653
|
this.log('š Stdin closed, shutting down...');
|
|
1654
|
+
cleanup();
|
|
1632
1655
|
this.emitFallbackSummary('stdin_end');
|
|
1633
1656
|
process.exit(0);
|
|
1634
1657
|
});
|
|
1635
1658
|
process.on('SIGTERM', () => {
|
|
1636
1659
|
this.log('š SIGTERM received, shutting down...');
|
|
1660
|
+
cleanup();
|
|
1637
1661
|
this.emitFallbackSummary('sigterm');
|
|
1638
1662
|
process.exit(0);
|
|
1639
1663
|
});
|
|
1640
1664
|
process.on('SIGINT', () => {
|
|
1641
1665
|
this.log('š SIGINT received, shutting down...');
|
|
1666
|
+
cleanup();
|
|
1642
1667
|
this.emitFallbackSummary('sigint');
|
|
1643
1668
|
process.exit(0);
|
|
1644
1669
|
});
|
|
1645
1670
|
this.log('ā
FRAIM Local MCP Server ready');
|
|
1646
1671
|
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Flush collected usage data to the local database
|
|
1674
|
+
*/
|
|
1675
|
+
async uploadUsageData() {
|
|
1676
|
+
if (this.usageCollector.getEventCount() === 0) {
|
|
1677
|
+
return; // Nothing to flush
|
|
1678
|
+
}
|
|
1679
|
+
try {
|
|
1680
|
+
const count = this.usageCollector.getEventCount();
|
|
1681
|
+
await this.usageCollector.flush(this.remoteUrl, this.apiKey);
|
|
1682
|
+
this.log(`š Flushed ${count} usage events to remote server`);
|
|
1683
|
+
}
|
|
1684
|
+
catch (error) {
|
|
1685
|
+
this.log(`ā Usage flushing error: ${error.message}`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1647
1688
|
}
|
|
1648
1689
|
exports.FraimLocalMCPServer = FraimLocalMCPServer;
|
|
1649
1690
|
FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE = [
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UsageCollector = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
/**
|
|
9
|
+
* Usage event collector for local MCP proxy
|
|
10
|
+
* Collects usage events and batches them for upload
|
|
11
|
+
*/
|
|
12
|
+
class UsageCollector {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.events = [];
|
|
15
|
+
this.apiKeyId = null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set the API key ID for this session
|
|
19
|
+
*/
|
|
20
|
+
setApiKeyId(apiKeyId) {
|
|
21
|
+
this.apiKeyId = apiKeyId;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Collect MCP tool call event
|
|
25
|
+
*/
|
|
26
|
+
collectMCPCall(toolName, args, sessionId, success = true, duration) {
|
|
27
|
+
if (!this.apiKeyId) {
|
|
28
|
+
return; // Silently skip if no API key
|
|
29
|
+
}
|
|
30
|
+
const parsed = this.parseMCPCall(toolName, args);
|
|
31
|
+
if (!parsed)
|
|
32
|
+
return;
|
|
33
|
+
const event = {
|
|
34
|
+
type: parsed.type,
|
|
35
|
+
name: parsed.name,
|
|
36
|
+
apiKeyId: this.apiKeyId,
|
|
37
|
+
sessionId,
|
|
38
|
+
success
|
|
39
|
+
};
|
|
40
|
+
if (duration !== undefined) {
|
|
41
|
+
event.duration = duration;
|
|
42
|
+
}
|
|
43
|
+
this.events.push(event);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Collect usage event directly (for backward compatibility with tests)
|
|
47
|
+
*/
|
|
48
|
+
collectEvent(type, name, sessionId, success = true, duration) {
|
|
49
|
+
if (!this.apiKeyId) {
|
|
50
|
+
return; // Silently skip if no API key
|
|
51
|
+
}
|
|
52
|
+
const event = {
|
|
53
|
+
type,
|
|
54
|
+
name,
|
|
55
|
+
apiKeyId: this.apiKeyId,
|
|
56
|
+
sessionId,
|
|
57
|
+
success
|
|
58
|
+
};
|
|
59
|
+
if (duration !== undefined) {
|
|
60
|
+
event.duration = duration;
|
|
61
|
+
}
|
|
62
|
+
this.events.push(event);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse MCP tool call to extract usage analytics info
|
|
66
|
+
*/
|
|
67
|
+
parseMCPCall(toolName, args) {
|
|
68
|
+
switch (toolName) {
|
|
69
|
+
case 'get_fraim_job':
|
|
70
|
+
return { type: 'job', name: args.job || 'unknown' };
|
|
71
|
+
case 'get_fraim_file':
|
|
72
|
+
if (args.path) {
|
|
73
|
+
return UsageCollector.parseComponentName(args.path);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
case 'seekMentoring':
|
|
77
|
+
return { type: 'mentoring', name: args.workflowType || 'unknown' };
|
|
78
|
+
default:
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get collected events for upload and clear the queue
|
|
84
|
+
*/
|
|
85
|
+
getEventsForUpload() {
|
|
86
|
+
const eventsToUpload = [...this.events];
|
|
87
|
+
this.events = [];
|
|
88
|
+
return eventsToUpload;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get current event count
|
|
92
|
+
*/
|
|
93
|
+
getEventCount() {
|
|
94
|
+
return this.events.length;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Flush events to the remote server via API
|
|
98
|
+
*/
|
|
99
|
+
async flush(remoteUrl, apiKey) {
|
|
100
|
+
if (this.events.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
const events = [...this.events];
|
|
103
|
+
this.events = [];
|
|
104
|
+
try {
|
|
105
|
+
await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
|
|
106
|
+
events,
|
|
107
|
+
apiKey
|
|
108
|
+
}, {
|
|
109
|
+
timeout: 5000,
|
|
110
|
+
headers: {
|
|
111
|
+
'x-api-key': apiKey,
|
|
112
|
+
'Content-Type': 'application/json'
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// Success - events are already cleared from the queue
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const status = error.response?.status;
|
|
119
|
+
const message = error.response?.data?.error || error.message;
|
|
120
|
+
console.error(`ā Failed to flush usage events (HTTP ${status}): ${message}`);
|
|
121
|
+
// Put events back at the beginning of the queue for next try
|
|
122
|
+
this.events = [...events, ...this.events];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Shutdown the collector
|
|
127
|
+
*/
|
|
128
|
+
shutdown() {
|
|
129
|
+
this.events = [];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse component name from file path
|
|
133
|
+
*/
|
|
134
|
+
static parseComponentName(path) {
|
|
135
|
+
if (path.includes('/jobs/')) {
|
|
136
|
+
const jobMatch = path.match(/\/jobs\/[^/]+\/[^/]+\/([^/]+)\.md$/);
|
|
137
|
+
if (jobMatch) {
|
|
138
|
+
return { type: 'job', name: jobMatch[1] };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (path.includes('/skills/')) {
|
|
142
|
+
const skillMatch = path.match(/\/skills\/[^/]+\/([^/]+)\.md$/);
|
|
143
|
+
if (skillMatch) {
|
|
144
|
+
return { type: 'skill', name: skillMatch[1] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (path.includes('/rules/')) {
|
|
148
|
+
const ruleMatch = path.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
|
|
149
|
+
if (ruleMatch) {
|
|
150
|
+
return { type: 'rule', name: ruleMatch[1] };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.UsageCollector = UsageCollector;
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.92",
|
|
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": {
|
|
7
7
|
"fraim": "./index.js",
|
|
8
|
-
"fraim-framework": "./index.js"
|
|
9
|
-
"fraim-mcp": "./bin/fraim-mcp.js"
|
|
8
|
+
"fraim-framework": "./index.js"
|
|
10
9
|
},
|
|
11
10
|
"scripts": {
|
|
12
11
|
"dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
|
|
13
12
|
"dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
|
|
14
|
-
"build": "tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
|
|
13
|
+
"build": "tsc && npm run build:stubs && npm run build:fraim-brain && npm run build:config-schema && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
|
|
15
14
|
"build:stubs": "tsx scripts/build-stub-registry.ts",
|
|
16
15
|
"build:fraim-brain": "node scripts/generate-fraim-brain.js",
|
|
16
|
+
"build:config-schema": "tsx scripts/generate-config-schema-template.ts",
|
|
17
17
|
"test-all": "npm run test && npm run test:isolated && npm run test:ui",
|
|
18
18
|
"test": "node scripts/test-with-server.js",
|
|
19
19
|
"test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"postinstall": "fraim sync --skip-updates || echo 'FRAIM setup skipped.'",
|
|
37
37
|
"prepublishOnly": "npm run build",
|
|
38
38
|
"release": "npm version patch && npm publish",
|
|
39
|
-
"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",
|
|
39
|
+
"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",
|
|
40
40
|
"validate:brain-mapping": "tsx scripts/validate-brain-mapping.ts",
|
|
41
41
|
"validate:fraim-pro-assets": "tsx scripts/validate-fraim-pro-assets.ts",
|
|
42
42
|
"validate:workflows": "tsx scripts/validate-workflows.ts",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"validate:bootstrap-config-coverage": "tsx scripts/validate-bootstrap-config-coverage.ts",
|
|
48
48
|
"validate:provider-action-mappings": "tsx scripts/validate-provider-action-mappings.ts",
|
|
49
49
|
"validate:fidelity": "tsx scripts/validate-fidelity.ts",
|
|
50
|
-
"validate:config-tokens": "tsx scripts/validate-config-tokens.ts"
|
|
50
|
+
"validate:config-tokens": "tsx scripts/validate-config-tokens.ts",
|
|
51
|
+
"validate:template-syntax": "tsx scripts/validate-template-syntax.ts"
|
|
51
52
|
},
|
|
52
53
|
"repository": {
|
|
53
54
|
"type": "git",
|
|
@@ -82,10 +83,12 @@
|
|
|
82
83
|
},
|
|
83
84
|
"devDependencies": {
|
|
84
85
|
"@playwright/test": "^1.58.2",
|
|
86
|
+
"@types/adm-zip": "^0.5.7",
|
|
85
87
|
"@types/cors": "^2.8.19",
|
|
86
88
|
"@types/express": "^5.0.6",
|
|
87
89
|
"@types/node": "^20.0.0",
|
|
88
90
|
"@types/prompts": "^2.4.9",
|
|
91
|
+
"@types/semver": "^7.7.1",
|
|
89
92
|
"fast-glob": "^3.3.3",
|
|
90
93
|
"html-to-docx": "^1.8.0",
|
|
91
94
|
"markdown-it": "^14.1.1",
|
|
@@ -114,6 +117,7 @@
|
|
|
114
117
|
"access": "public"
|
|
115
118
|
},
|
|
116
119
|
"dependencies": {
|
|
120
|
+
"adm-zip": "^0.5.16",
|
|
117
121
|
"axios": "^1.7.0",
|
|
118
122
|
"chalk": "4.1.2",
|
|
119
123
|
"commander": "^14.0.2",
|
|
@@ -124,8 +128,10 @@
|
|
|
124
128
|
"node-edge-tts": "^1.2.10",
|
|
125
129
|
"prompts": "^2.4.2",
|
|
126
130
|
"resend": "^6.9.3",
|
|
131
|
+
"semver": "^7.7.4",
|
|
127
132
|
"stripe": "^20.3.1",
|
|
128
133
|
"toml": "^3.0.0",
|
|
129
|
-
"tree-kill": "^1.2.2"
|
|
134
|
+
"tree-kill": "^1.2.2",
|
|
135
|
+
"xml2js": "^0.6.2"
|
|
130
136
|
}
|
|
131
137
|
}
|
package/bin/fraim-mcp.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* FRAIM MCP Server - Smart Entry Point
|
|
5
|
-
* This file handles both production (dist/) and development (src/) environments.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const { spawn } = require('child_process');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Runs the MCP server using either the compiled JS or the source TS via tsx
|
|
14
|
-
*/
|
|
15
|
-
function runMCPServer() {
|
|
16
|
-
const distPath = path.join(__dirname, '..', 'dist', 'src', 'local-mcp-server', 'stdio-server.js');
|
|
17
|
-
const srcPath = path.join(__dirname, '..', 'src', 'local-mcp-server', 'stdio-server.ts');
|
|
18
|
-
|
|
19
|
-
// 1. Check if we have a compiled version (Production / CI)
|
|
20
|
-
if (fs.existsSync(distPath)) {
|
|
21
|
-
// Use spawn to run the compiled JS so it's treated as the main module
|
|
22
|
-
const result = spawn('node', [distPath, ...process.argv.slice(2)], {
|
|
23
|
-
stdio: 'inherit',
|
|
24
|
-
shell: false,
|
|
25
|
-
windowsHide: true
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Forward exit code
|
|
29
|
-
result.on('exit', (code) => {
|
|
30
|
-
process.exit(code || 0);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 2. Fallback to source version using tsx (Development)
|
|
37
|
-
if (fs.existsSync(srcPath)) {
|
|
38
|
-
// Use spawn to run tsx so it handles stdio correctly for MCP
|
|
39
|
-
const result = spawn('npx', ['tsx', srcPath, ...process.argv.slice(2)], {
|
|
40
|
-
stdio: 'inherit',
|
|
41
|
-
shell: true,
|
|
42
|
-
windowsHide: true
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Forward exit code
|
|
46
|
-
result.on('exit', (code) => {
|
|
47
|
-
process.exit(code || 0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.error('ā FRAIM MCP Server Error: Could not find server entry point.');
|
|
54
|
-
console.error('Expected one of:');
|
|
55
|
-
console.error(` - ${distPath}`);
|
|
56
|
-
console.error(` - ${srcPath}`);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// If this file is run directly, run the MCP server
|
|
61
|
-
if (require.main === module) {
|
|
62
|
-
runMCPServer();
|
|
63
|
-
}
|