fraim-framework 2.0.89 → 2.0.91

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-mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM MCP Server - Smart Entry Point
@@ -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
@@ -1402,9 +1406,9 @@ class FraimLocalMCPServer {
1402
1406
  // Force ALL tools/call requests to return raw definitions so the proxy
1403
1407
  // can resolve templates and replace includes locally
1404
1408
  const toolName = request.params?.name;
1409
+ const args = request.params?.arguments || {};
1405
1410
  let injectedRequest = request;
1406
1411
  if (request.method === 'tools/call' && typeof toolName === 'string') {
1407
- const args = request.params.arguments || {};
1408
1412
  // šŸ” SMART DISPATCHER: Intercept mentoring and job/workflow tools for local overrides
1409
1413
  if (toolName === 'seekMentoring') {
1410
1414
  try {
@@ -1482,6 +1486,11 @@ class FraimLocalMCPServer {
1482
1486
  }
1483
1487
  const response = await this._doProxyToRemote(injectedRequest, requestId);
1484
1488
  const processedResponse = await this.finalizeToolResponse(injectedRequest, response, requestSessionId, requestId);
1489
+ // Single point for usage tracking - log all tool calls
1490
+ if (injectedRequest.method === 'tools/call' && requestSessionId && toolName) {
1491
+ const success = !processedResponse.error;
1492
+ this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1493
+ }
1485
1494
  this.log(`šŸ“¤ ${injectedRequest.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1486
1495
  return processedResponse;
1487
1496
  }
@@ -1574,6 +1583,17 @@ class FraimLocalMCPServer {
1574
1583
  start() {
1575
1584
  let buffer = '';
1576
1585
  process.stdin.setEncoding('utf8');
1586
+ // Set up periodic usage data upload
1587
+ const uploadInterval = setInterval(() => {
1588
+ this.uploadUsageData().catch(error => {
1589
+ this.log(`āš ļø Failed to upload usage data: ${error.message}`);
1590
+ });
1591
+ }, 60000); // Upload every minute
1592
+ // Clean up interval on shutdown
1593
+ const cleanup = () => {
1594
+ clearInterval(uploadInterval);
1595
+ this.usageCollector.shutdown();
1596
+ };
1577
1597
  process.stdin.on('data', async (chunk) => {
1578
1598
  buffer += chunk;
1579
1599
  // Process complete JSON-RPC messages (newline-delimited)
@@ -1629,21 +1649,52 @@ class FraimLocalMCPServer {
1629
1649
  });
1630
1650
  process.stdin.on('end', () => {
1631
1651
  this.log('šŸ›‘ Stdin closed, shutting down...');
1652
+ cleanup();
1632
1653
  this.emitFallbackSummary('stdin_end');
1633
1654
  process.exit(0);
1634
1655
  });
1635
1656
  process.on('SIGTERM', () => {
1636
1657
  this.log('šŸ›‘ SIGTERM received, shutting down...');
1658
+ cleanup();
1637
1659
  this.emitFallbackSummary('sigterm');
1638
1660
  process.exit(0);
1639
1661
  });
1640
1662
  process.on('SIGINT', () => {
1641
1663
  this.log('šŸ›‘ SIGINT received, shutting down...');
1664
+ cleanup();
1642
1665
  this.emitFallbackSummary('sigint');
1643
1666
  process.exit(0);
1644
1667
  });
1645
1668
  this.log('āœ… FRAIM Local MCP Server ready');
1646
1669
  }
1670
+ /**
1671
+ * Upload collected usage data to the remote server
1672
+ */
1673
+ async uploadUsageData() {
1674
+ const events = this.usageCollector.getEventsForUpload();
1675
+ if (events.length === 0) {
1676
+ return; // Nothing to upload
1677
+ }
1678
+ try {
1679
+ const response = await axios_1.default.post(`${this.remoteUrl}/api/analytics/events`, { events }, {
1680
+ headers: {
1681
+ 'Content-Type': 'application/json',
1682
+ 'x-api-key': this.apiKey
1683
+ },
1684
+ timeout: 10000
1685
+ });
1686
+ if (response.status === 200) {
1687
+ this.log(`šŸ“Š Uploaded ${events.length} usage events`);
1688
+ }
1689
+ else {
1690
+ this.log(`āš ļø Usage upload failed: ${response.status}`);
1691
+ }
1692
+ }
1693
+ catch (error) {
1694
+ this.log(`āŒ Usage upload error: ${error.message}`);
1695
+ // Don't re-queue on failure - keep it simple
1696
+ }
1697
+ }
1647
1698
  }
1648
1699
  exports.FraimLocalMCPServer = FraimLocalMCPServer;
1649
1700
  FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE = [
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UsageCollector = void 0;
4
+ /**
5
+ * Usage event collector for local MCP proxy
6
+ * Collects usage events and batches them for upload
7
+ */
8
+ class UsageCollector {
9
+ constructor() {
10
+ this.events = [];
11
+ this.apiKeyId = null;
12
+ }
13
+ /**
14
+ * Set the API key ID for this session
15
+ */
16
+ setApiKeyId(apiKeyId) {
17
+ this.apiKeyId = apiKeyId;
18
+ }
19
+ /**
20
+ * Collect MCP tool call event
21
+ */
22
+ collectMCPCall(toolName, args, sessionId, success = true, duration) {
23
+ if (!this.apiKeyId) {
24
+ return; // Silently skip if no API key
25
+ }
26
+ const parsed = this.parseMCPCall(toolName, args);
27
+ if (!parsed)
28
+ return;
29
+ const event = {
30
+ type: parsed.type,
31
+ name: parsed.name,
32
+ apiKeyId: this.apiKeyId,
33
+ sessionId,
34
+ success
35
+ };
36
+ if (duration !== undefined) {
37
+ event.duration = duration;
38
+ }
39
+ this.events.push(event);
40
+ }
41
+ /**
42
+ * Collect usage event directly (for backward compatibility with tests)
43
+ */
44
+ collectEvent(type, name, sessionId, success = true, duration) {
45
+ if (!this.apiKeyId) {
46
+ return; // Silently skip if no API key
47
+ }
48
+ const event = {
49
+ type,
50
+ name,
51
+ apiKeyId: this.apiKeyId,
52
+ sessionId,
53
+ success
54
+ };
55
+ if (duration !== undefined) {
56
+ event.duration = duration;
57
+ }
58
+ this.events.push(event);
59
+ }
60
+ /**
61
+ * Parse MCP tool call to extract usage analytics info
62
+ */
63
+ parseMCPCall(toolName, args) {
64
+ switch (toolName) {
65
+ case 'get_fraim_job':
66
+ return { type: 'job', name: args.job || 'unknown' };
67
+ case 'get_fraim_file':
68
+ if (args.path) {
69
+ return UsageCollector.parseComponentName(args.path);
70
+ }
71
+ return null;
72
+ case 'seekMentoring':
73
+ return { type: 'mentoring', name: args.workflowType || 'unknown' };
74
+ default:
75
+ return null;
76
+ }
77
+ }
78
+ /**
79
+ * Get collected events for upload and clear the queue
80
+ */
81
+ getEventsForUpload() {
82
+ const eventsToUpload = [...this.events];
83
+ this.events = [];
84
+ return eventsToUpload;
85
+ }
86
+ /**
87
+ * Get current event count
88
+ */
89
+ getEventCount() {
90
+ return this.events.length;
91
+ }
92
+ /**
93
+ * Shutdown the collector
94
+ */
95
+ shutdown() {
96
+ this.events = [];
97
+ }
98
+ /**
99
+ * Parse component name from file path
100
+ */
101
+ static parseComponentName(path) {
102
+ if (path.includes('/jobs/')) {
103
+ const jobMatch = path.match(/\/jobs\/[^/]+\/[^/]+\/([^/]+)\.md$/);
104
+ if (jobMatch) {
105
+ return { type: 'job', name: jobMatch[1] };
106
+ }
107
+ }
108
+ if (path.includes('/skills/')) {
109
+ const skillMatch = path.match(/\/skills\/[^/]+\/([^/]+)\.md$/);
110
+ if (skillMatch) {
111
+ return { type: 'skill', name: skillMatch[1] };
112
+ }
113
+ }
114
+ if (path.includes('/rules/')) {
115
+ const ruleMatch = path.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
116
+ if (ruleMatch) {
117
+ return { type: 'rule', name: ruleMatch[1] };
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ }
123
+ exports.UsageCollector = UsageCollector;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.89",
3
+ "version": "2.0.91",
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": {
@@ -1,171 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.testMCPCommand = exports.runTestMCP = void 0;
40
- const commander_1 = require("commander");
41
- const chalk_1 = __importDefault(require("chalk"));
42
- const fs_1 = __importDefault(require("fs"));
43
- const path_1 = __importDefault(require("path"));
44
- const ide_detector_1 = require("../setup/ide-detector");
45
- const script_sync_utils_1 = require("../utils/script-sync-utils");
46
- const testIDEConfig = async (ide) => {
47
- const result = {
48
- ide: ide.name,
49
- configExists: false,
50
- configValid: false,
51
- mcpServers: [],
52
- errors: []
53
- };
54
- const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
55
- if (!fs_1.default.existsSync(configPath)) {
56
- result.errors.push('Config file does not exist');
57
- return result;
58
- }
59
- result.configExists = true;
60
- try {
61
- if (ide.configFormat === 'json') {
62
- const configContent = fs_1.default.readFileSync(configPath, 'utf8');
63
- const config = JSON.parse(configContent);
64
- const servers = ide.configType === 'vscode' ? config.servers : config.mcpServers;
65
- if (servers) {
66
- result.configValid = true;
67
- result.mcpServers = Object.keys(servers);
68
- }
69
- else {
70
- const expectedKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
71
- result.errors.push(`No ${expectedKey} section found`);
72
- }
73
- }
74
- else if (ide.configFormat === 'toml') {
75
- const configContent = fs_1.default.readFileSync(configPath, 'utf8');
76
- // Simple TOML parsing for MCP servers
77
- const serverMatches = configContent.match(/\[mcp_servers\.(\w+)\]/g);
78
- if (serverMatches) {
79
- result.configValid = true;
80
- result.mcpServers = serverMatches.map(match => match.replace(/\[mcp_servers\.(\w+)\]/, '$1'));
81
- }
82
- else {
83
- result.errors.push('No mcp_servers sections found');
84
- }
85
- }
86
- }
87
- catch (error) {
88
- result.errors.push(`Failed to parse config: ${error instanceof Error ? error.message : 'Unknown error'}`);
89
- }
90
- return result;
91
- };
92
- const checkGlobalSetup = () => {
93
- const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
94
- return fs_1.default.existsSync(globalConfigPath);
95
- };
96
- const runTestMCP = async () => {
97
- console.log(chalk_1.default.blue('šŸ” Testing MCP configuration...\n'));
98
- // Check global setup
99
- if (!checkGlobalSetup()) {
100
- console.log(chalk_1.default.red('āŒ Global FRAIM setup not found.'));
101
- console.log(chalk_1.default.yellow('Please run: fraim setup --key=<your-fraim-key>'));
102
- return;
103
- }
104
- console.log(chalk_1.default.green('āœ… Global FRAIM setup found'));
105
- // Detect IDEs
106
- const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
107
- if (detectedIDEs.length === 0) {
108
- console.log(chalk_1.default.yellow('āš ļø No supported IDEs detected.'));
109
- return;
110
- }
111
- console.log(chalk_1.default.blue(`\nšŸ” Testing ${detectedIDEs.length} detected IDEs...\n`));
112
- const results = await Promise.all(detectedIDEs.map(ide => testIDEConfig(ide)));
113
- let totalConfigured = 0;
114
- let totalWithFRAIM = 0;
115
- for (const result of results) {
116
- console.log(chalk_1.default.white(`šŸ“± ${result.ide}`));
117
- if (!result.configExists) {
118
- console.log(chalk_1.default.red(' āŒ No MCP config found'));
119
- console.log(chalk_1.default.gray(` šŸ’” Run: fraim setup --ide=${result.ide.toLowerCase()}`));
120
- }
121
- else if (!result.configValid) {
122
- console.log(chalk_1.default.yellow(' āš ļø Config exists but invalid'));
123
- result.errors.forEach(error => {
124
- console.log(chalk_1.default.red(` āŒ ${error}`));
125
- });
126
- }
127
- else {
128
- totalConfigured++;
129
- console.log(chalk_1.default.green(` āœ… MCP config valid (${result.mcpServers.length} servers)`));
130
- // Check for essential servers
131
- const { BASE_MCP_SERVERS } = await Promise.resolve().then(() => __importStar(require('../mcp/mcp-server-registry')));
132
- const essentialServers = BASE_MCP_SERVERS.map(s => s.id); // fraim, git, playwright
133
- const hasEssential = essentialServers.filter(server => result.mcpServers.includes(server));
134
- if (hasEssential.includes('fraim')) {
135
- totalWithFRAIM++;
136
- console.log(chalk_1.default.green(' āœ… FRAIM server configured'));
137
- }
138
- else {
139
- console.log(chalk_1.default.yellow(' āš ļø FRAIM server missing'));
140
- }
141
- if (hasEssential.length > 1) {
142
- console.log(chalk_1.default.green(` āœ… ${hasEssential.length - 1} additional servers: ${hasEssential.filter(s => s !== 'fraim').join(', ')}`));
143
- }
144
- const missingEssential = essentialServers.filter(server => !result.mcpServers.includes(server));
145
- if (missingEssential.length > 0) {
146
- console.log(chalk_1.default.yellow(` āš ļø Missing servers: ${missingEssential.join(', ')}`));
147
- }
148
- }
149
- console.log(); // Empty line
150
- }
151
- // Summary
152
- console.log(chalk_1.default.blue('šŸ“Š Summary:'));
153
- console.log(chalk_1.default.green(` āœ… ${totalConfigured}/${detectedIDEs.length} IDEs have valid MCP configs`));
154
- console.log(chalk_1.default.green(` āœ… ${totalWithFRAIM}/${detectedIDEs.length} IDEs have FRAIM configured`));
155
- if (totalWithFRAIM === 0) {
156
- console.log(chalk_1.default.red('\nāŒ No IDEs have FRAIM configured!'));
157
- console.log(chalk_1.default.yellow('šŸ’” Run: fraim setup --key=<your-fraim-key>'));
158
- }
159
- else if (totalWithFRAIM < detectedIDEs.length) {
160
- console.log(chalk_1.default.yellow(`\nāš ļø ${detectedIDEs.length - totalWithFRAIM} IDEs missing FRAIM configuration`));
161
- console.log(chalk_1.default.yellow('šŸ’” Run: fraim setup to configure remaining IDEs'));
162
- }
163
- else {
164
- console.log(chalk_1.default.green('\nšŸŽ‰ All detected IDEs have FRAIM configured!'));
165
- console.log(chalk_1.default.blue('šŸ’” Try running: fraim init-project in any project'));
166
- }
167
- };
168
- exports.runTestMCP = runTestMCP;
169
- exports.testMCPCommand = new commander_1.Command('test-mcp')
170
- .description('Test MCP server configurations for all detected IDEs')
171
- .action(exports.runTestMCP);