fraim-framework 2.0.93 → 2.0.95

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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework CLI Entry Point
@@ -41,7 +41,6 @@ const commander_1 = require("commander");
41
41
  const fs_1 = __importDefault(require("fs"));
42
42
  const path_1 = __importDefault(require("path"));
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
- const child_process_1 = require("child_process");
45
44
  const config_loader_1 = require("../../core/config-loader");
46
45
  const version_utils_1 = require("../utils/version-utils");
47
46
  const script_sync_utils_1 = require("../utils/script-sync-utils");
@@ -85,14 +84,13 @@ const runSync = async (options) => {
85
84
  const projectRoot = process.cwd();
86
85
  const config = (0, config_loader_1.loadFraimConfig)();
87
86
  const fraimDir = path_1.default.join(projectRoot, '.fraim');
88
- // Check for CLI updates first (but skip during installation to prevent loops)
89
- const isPostInstall = process.env.npm_lifecycle_event === 'postinstall' ||
90
- process.env.npm_lifecycle_script === 'postinstall';
91
- if (!options.skipUpdates && !isPostInstall) {
92
- await checkAndUpdateCLI();
93
- }
94
- else if (isPostInstall) {
95
- console.log(chalk_1.default.gray('⏭️ Skipping auto-update check during installation to prevent loops.'));
87
+ // Check if running via npx
88
+ const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
89
+ const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
90
+ if (isGlobal && !options.skipUpdates) {
91
+ console.log(chalk_1.default.yellow('⚠️ You are running a global installation of FRAIM.'));
92
+ console.log(chalk_1.default.gray(' Updates are not automatic in this mode.'));
93
+ console.log(chalk_1.default.cyan(' 💡 Recommended: Use "npx fraim-framework@latest sync" instead.\n'));
96
94
  }
97
95
  const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
98
96
  // Path 1: --local flag → hit localhost MCP server
@@ -149,118 +147,9 @@ const runSync = async (options) => {
149
147
  updateVersionInConfig(fraimDir);
150
148
  };
151
149
  exports.runSync = runSync;
152
- async function checkAndUpdateCLI() {
153
- try {
154
- // Additional safety: check if we're in a global npm install context
155
- const isGlobalInstall = process.env.npm_config_global === 'true' ||
156
- process.env.npm_config_prefix ||
157
- process.cwd().includes('node_modules');
158
- if (isGlobalInstall) {
159
- console.log(chalk_1.default.gray('⏭️ Skipping auto-update during global installation.'));
160
- return;
161
- }
162
- console.log(chalk_1.default.blue('🔍 Checking for FRAIM CLI updates...'));
163
- const currentVersion = (0, version_utils_1.getFraimVersion)();
164
- const latestVersion = await getLatestNpmVersionHttp('fraim-framework');
165
- if (!latestVersion) {
166
- console.log(chalk_1.default.yellow('⚠️ Could not check for updates. Continuing with current version.'));
167
- return;
168
- }
169
- if (isVersionUpToDate(currentVersion, latestVersion)) {
170
- console.log(chalk_1.default.green(`✅ FRAIM CLI is up to date (${currentVersion})`));
171
- return;
172
- }
173
- console.log(chalk_1.default.yellow(`📦 New version available: ${currentVersion} → ${latestVersion}`));
174
- console.log(chalk_1.default.blue('🔄 Auto-updating FRAIM CLI...'));
175
- const success = await updateGlobalPackageHttp('fraim-framework', latestVersion);
176
- if (success) {
177
- console.log(chalk_1.default.green(`✅ Successfully updated to FRAIM ${latestVersion}`));
178
- console.log(chalk_1.default.gray(' Restart may be required for some changes to take effect.'));
179
- }
180
- else {
181
- console.log(chalk_1.default.yellow('⚠️ Auto-update failed. Please run: npm install -g fraim-framework@latest'));
182
- }
183
- }
184
- catch (error) {
185
- console.log(chalk_1.default.yellow('⚠️ Could not check for updates. Continuing with current version.'));
186
- }
187
- }
188
- async function getLatestNpmVersionHttp(packageName) {
189
- try {
190
- // Use Node.js built-in https module instead of spawn
191
- const https = require('https');
192
- return new Promise((resolve) => {
193
- const req = https.get(`https://registry.npmjs.org/${packageName}/latest`, {
194
- timeout: 5000,
195
- headers: {
196
- 'User-Agent': 'fraim-framework-cli'
197
- }
198
- }, (res) => {
199
- let data = '';
200
- res.on('data', (chunk) => {
201
- data += chunk;
202
- });
203
- res.on('end', () => {
204
- try {
205
- const packageInfo = JSON.parse(data);
206
- resolve(packageInfo.version || null);
207
- }
208
- catch (e) {
209
- resolve(null);
210
- }
211
- });
212
- });
213
- req.on('error', () => {
214
- resolve(null);
215
- });
216
- req.on('timeout', () => {
217
- req.destroy();
218
- resolve(null);
219
- });
220
- });
221
- }
222
- catch (error) {
223
- return null;
224
- }
225
- }
226
- function isVersionUpToDate(current, latest) {
227
- // Simple version comparison - assumes semantic versioning
228
- const currentParts = current.split('.').map(n => parseInt(n, 10));
229
- const latestParts = latest.split('.').map(n => parseInt(n, 10));
230
- for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
231
- const currentPart = currentParts[i] || 0;
232
- const latestPart = latestParts[i] || 0;
233
- if (currentPart < latestPart)
234
- return false;
235
- if (currentPart > latestPart)
236
- return true;
237
- }
238
- return true; // Versions are equal
239
- }
240
- async function updateGlobalPackageHttp(packageName, version) {
241
- return new Promise((resolve) => {
242
- console.log(chalk_1.default.gray(` Running: npm install -g ${packageName}@${version}`));
243
- // Use npm.cmd on Windows, npm on Unix
244
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
245
- const npmProcess = (0, child_process_1.spawn)(npmCommand, ['install', '-g', `${packageName}@${version}`], {
246
- stdio: ['ignore', 'ignore', 'ignore'] // Suppress output for cleaner experience
247
- });
248
- npmProcess.on('close', (code) => {
249
- resolve(code === 0);
250
- });
251
- npmProcess.on('error', () => {
252
- resolve(false);
253
- });
254
- // Timeout after 30 seconds
255
- setTimeout(() => {
256
- npmProcess.kill();
257
- resolve(false);
258
- }, 30000);
259
- });
260
- }
261
150
  exports.syncCommand = new commander_1.Command('sync')
262
151
  .description('Sync workflow stubs from the framework registry')
263
152
  .option('-f, --force', 'Force sync even if digest matches')
264
- .option('--skip-updates', 'Skip checking for CLI updates')
153
+ .option('--skip-updates', 'Skip checking for CLI updates (legacy)')
265
154
  .option('--local', 'Sync from local development server (port derived from git branch)')
266
155
  .action(exports.runSync);
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- // MCP Server Registry - Builds MCP servers from provider definitions
3
- // This file NO LONGER contains hardcoded server configurations
4
- // All provider MCP server configs come from the provider registry (server or local fallback)
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.BASE_MCP_SERVERS = void 0;
7
4
  exports.buildProviderMCPServer = buildProviderMCPServer;
@@ -324,7 +324,7 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
324
324
  console.log(chalk_1.default.blue('\n🎯 Next steps:'));
325
325
  console.log(chalk_1.default.cyan(' 1. Restart your configured IDEs'));
326
326
  console.log(chalk_1.default.cyan(' 2. Go to any project directory'));
327
- console.log(chalk_1.default.cyan(' 3. Run: fraim init-project'));
327
+ console.log(chalk_1.default.cyan(' 3. Run: npx fraim-framework@latest init-project'));
328
328
  console.log(chalk_1.default.cyan(' 4. Ask your AI agent: "list fraim workflows"'));
329
329
  }
330
330
  };
@@ -2,8 +2,6 @@
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
7
5
  */
8
6
  Object.defineProperty(exports, "__esModule", { value: true });
9
7
  exports.DEFAULT_FRAIM_CONFIG = void 0;
@@ -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
  /**
@@ -35,6 +35,7 @@ const usage_collector_js_1 = require("./usage-collector.js");
35
35
  */
36
36
  class FraimTemplateEngine {
37
37
  constructor(opts) {
38
+ this.userEmail = null;
38
39
  this.deliveryTemplatesCache = null;
39
40
  this.providerTemplatesCache = {};
40
41
  this.deliveryTemplatesLoadAttempted = false;
@@ -71,8 +72,15 @@ class FraimTemplateEngine {
71
72
  setConfig(config) {
72
73
  this.config = config;
73
74
  }
75
+ setUserEmail(email) {
76
+ this.userEmail = email;
77
+ }
74
78
  substituteTemplates(content) {
75
79
  let result = content;
80
+ // Substitute {{proxy.user.email}} with the email captured from fraim_connect
81
+ if (this.userEmail) {
82
+ result = result.replace(/\{\{proxy\.user\.email\}\}/g, this.userEmail);
83
+ }
76
84
  // 0. Substitute runtime context tokens: {{agent.*}}, {{machine.*}}, {{repository.*}}
77
85
  // These come from the fraim_connect payload captured during handshake.
78
86
  const contexts = {
@@ -243,14 +251,6 @@ class FraimLocalMCPServer {
243
251
  this.machineInfo = null;
244
252
  this.repoInfo = null;
245
253
  this.engine = null;
246
- this.fallbackRewriteCount = 0;
247
- this.fallbackRewriteRequestCount = 0;
248
- this.fallbackRewriteTokens = new Map();
249
- this.pendingFallbackRequestCount = 0;
250
- this.pendingFallbackTokenRewriteCount = 0;
251
- this.pendingFallbackTokenCounts = new Map();
252
- this.pendingFallbackEvents = [];
253
- this.fallbackSummaryEmitted = false;
254
254
  this.writer = writer || process.stdout.write.bind(process.stdout);
255
255
  this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
256
256
  this.apiKey = this.loadApiKey();
@@ -867,6 +867,17 @@ class FraimLocalMCPServer {
867
867
  }
868
868
  // 2. Resolve includes within the content (for all registry tools)
869
869
  finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId, requestId);
870
+ // 3. After fraim_connect succeeds, capture user email for {{proxy.user.email}} substitution
871
+ if (toolName === 'fraim_connect' && !finalizedResponse.error) {
872
+ const text = finalizedResponse.result?.content?.[0]?.text;
873
+ if (typeof text === 'string') {
874
+ const emailMatch = text.match(/Your identity for this session: \*\*([^*]+)\*\*/);
875
+ if (emailMatch) {
876
+ this.ensureEngine().setUserEmail(emailMatch[1].trim());
877
+ this.log(`[req:${requestId}] Captured user email for template substitution: ${emailMatch[1].trim()}`);
878
+ }
879
+ }
880
+ }
870
881
  return this.processResponseWithHydration(finalizedResponse, requestSessionId);
871
882
  }
872
883
  async finalizeLocalToolTextResponse(request, requestSessionId, requestId, text) {
@@ -1043,7 +1054,6 @@ class FraimLocalMCPServer {
1043
1054
  return response;
1044
1055
  const rewritten = this.rewriteUnresolvedProxyPlaceholders(response.result);
1045
1056
  if (rewritten.tokens.length > 0) {
1046
- this.recordFallbackRewrite(rewritten.tokens);
1047
1057
  this.logError(`[${FraimLocalMCPServer.FALLBACK_ALERT_MARKER}] Rewrote unresolved proxy placeholders to agent placeholders: ${rewritten.tokens.join(', ')}`);
1048
1058
  }
1049
1059
  return {
@@ -1051,79 +1061,6 @@ class FraimLocalMCPServer {
1051
1061
  result: rewritten.value
1052
1062
  };
1053
1063
  }
1054
- recordFallbackRewrite(tokens) {
1055
- this.fallbackRewriteRequestCount += 1;
1056
- this.fallbackRewriteCount += tokens.length;
1057
- for (const token of tokens) {
1058
- const existing = this.fallbackRewriteTokens.get(token) || 0;
1059
- this.fallbackRewriteTokens.set(token, existing + 1);
1060
- }
1061
- this.pendingFallbackRequestCount += 1;
1062
- this.pendingFallbackTokenRewriteCount += tokens.length;
1063
- for (const token of tokens) {
1064
- const existing = this.pendingFallbackTokenCounts.get(token) || 0;
1065
- this.pendingFallbackTokenCounts.set(token, existing + 1);
1066
- }
1067
- this.pendingFallbackEvents.push({
1068
- at: new Date().toISOString(),
1069
- tokenCount: tokens.length,
1070
- tokens: [...tokens]
1071
- });
1072
- if (this.pendingFallbackEvents.length > 250) {
1073
- this.pendingFallbackEvents = this.pendingFallbackEvents.slice(-250);
1074
- }
1075
- }
1076
- buildPendingFallbackTelemetryPayload() {
1077
- if (this.pendingFallbackRequestCount === 0 &&
1078
- this.pendingFallbackTokenRewriteCount === 0 &&
1079
- this.pendingFallbackEvents.length === 0) {
1080
- return null;
1081
- }
1082
- const tokenCounts = {};
1083
- for (const [token, count] of this.pendingFallbackTokenCounts.entries()) {
1084
- tokenCounts[token] = count;
1085
- }
1086
- return {
1087
- requestFallbacks: this.pendingFallbackRequestCount,
1088
- tokenRewrites: this.pendingFallbackTokenRewriteCount,
1089
- tokenCounts,
1090
- events: this.pendingFallbackEvents.map((event) => ({
1091
- at: event.at,
1092
- tokenCount: event.tokenCount,
1093
- tokens: [...event.tokens]
1094
- }))
1095
- };
1096
- }
1097
- acknowledgePendingFallbackTelemetry(payload) {
1098
- this.pendingFallbackRequestCount = Math.max(0, this.pendingFallbackRequestCount - payload.requestFallbacks);
1099
- this.pendingFallbackTokenRewriteCount = Math.max(0, this.pendingFallbackTokenRewriteCount - payload.tokenRewrites);
1100
- for (const [token, sentCount] of Object.entries(payload.tokenCounts)) {
1101
- const current = this.pendingFallbackTokenCounts.get(token) || 0;
1102
- const next = Math.max(0, current - Math.max(0, sentCount));
1103
- if (next === 0) {
1104
- this.pendingFallbackTokenCounts.delete(token);
1105
- }
1106
- else {
1107
- this.pendingFallbackTokenCounts.set(token, next);
1108
- }
1109
- }
1110
- if (payload.events.length > 0) {
1111
- this.pendingFallbackEvents.splice(0, Math.min(payload.events.length, this.pendingFallbackEvents.length));
1112
- }
1113
- }
1114
- emitFallbackSummary(reason) {
1115
- if (this.fallbackSummaryEmitted)
1116
- return;
1117
- this.fallbackSummaryEmitted = true;
1118
- if (this.fallbackRewriteCount === 0)
1119
- return;
1120
- const topTokens = Array.from(this.fallbackRewriteTokens.entries())
1121
- .sort((a, b) => b[1] - a[1])
1122
- .slice(0, 10)
1123
- .map(([token, count]) => `${token}:${count}`)
1124
- .join(', ');
1125
- this.logError(`[${FraimLocalMCPServer.FALLBACK_SUMMARY_MARKER}] reason=${reason} requestFallbacks=${this.fallbackRewriteRequestCount} tokenRewrites=${this.fallbackRewriteCount} uniqueTokens=${this.fallbackRewriteTokens.size}${topTokens ? ` topTokens=${topTokens}` : ''}`);
1126
- }
1127
1064
  extractSessionIdFromRequest(request) {
1128
1065
  const sessionIdFromParams = request.params?.sessionId;
1129
1066
  if (typeof sessionIdFromParams === 'string' && sessionIdFromParams.trim().length > 0) {
@@ -1197,7 +1134,6 @@ class FraimLocalMCPServer {
1197
1134
  * tool calls and recursive inclusion resolution.
1198
1135
  */
1199
1136
  async _doProxyToRemote(request, requestId = (0, crypto_1.randomUUID)()) {
1200
- let sentFallbackTelemetry = null;
1201
1137
  try {
1202
1138
  // Special handling for fraim_connect - automatically inject machine and repo info
1203
1139
  if (request.method === 'tools/call' && request.params?.name === 'fraim_connect') {
@@ -1269,7 +1205,6 @@ class FraimLocalMCPServer {
1269
1205
  // Update the request with injected info
1270
1206
  request.params.arguments = args;
1271
1207
  }
1272
- sentFallbackTelemetry = this.buildPendingFallbackTelemetryPayload();
1273
1208
  const headers = {
1274
1209
  'Content-Type': 'application/json',
1275
1210
  'x-api-key': this.apiKey,
@@ -1280,11 +1215,6 @@ class FraimLocalMCPServer {
1280
1215
  if (sessionId) {
1281
1216
  headers[FraimLocalMCPServer.SESSION_HEADER] = sessionId;
1282
1217
  }
1283
- if (sentFallbackTelemetry) {
1284
- headers[FraimLocalMCPServer.FALLBACK_HEADER] = Buffer
1285
- .from(JSON.stringify(sentFallbackTelemetry), 'utf8')
1286
- .toString('base64');
1287
- }
1288
1218
  this.log(`[req:${requestId}] Proxying ${request.method} to ${this.remoteUrl}/mcp`);
1289
1219
  // Resolve templates in the outgoing request so the remote server
1290
1220
  // only ever sees finalized values.
@@ -1295,9 +1225,6 @@ class FraimLocalMCPServer {
1295
1225
  headers,
1296
1226
  timeout: 30000
1297
1227
  });
1298
- if (sentFallbackTelemetry) {
1299
- this.acknowledgePendingFallbackTelemetry(sentFallbackTelemetry);
1300
- }
1301
1228
  return response.data;
1302
1229
  }
1303
1230
  catch (error) {
@@ -1322,10 +1249,6 @@ class FraimLocalMCPServer {
1322
1249
  localMcpVersion: this.localVersion
1323
1250
  };
1324
1251
  }
1325
- if (sentFallbackTelemetry) {
1326
- // Request reached the server and produced a response; clear sent fallback delta.
1327
- this.acknowledgePendingFallbackTelemetry(sentFallbackTelemetry);
1328
- }
1329
1252
  return forwarded;
1330
1253
  }
1331
1254
  return {
@@ -1491,8 +1414,12 @@ class FraimLocalMCPServer {
1491
1414
  // Single point for usage tracking - log all tool calls
1492
1415
  if (injectedRequest.method === 'tools/call' && requestSessionId && toolName) {
1493
1416
  const success = !processedResponse.error;
1417
+ this.log(`📊 Collecting usage: ${toolName} (session: ${requestSessionId}, success: ${success})`);
1494
1418
  this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1495
1419
  }
1420
+ else if (injectedRequest.method === 'tools/call') {
1421
+ this.log(`⚠️ Skipping usage collection: toolName=${toolName}, sessionId=${requestSessionId}`);
1422
+ }
1496
1423
  this.log(`📤 ${injectedRequest.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1497
1424
  return processedResponse;
1498
1425
  }
@@ -1652,19 +1579,16 @@ class FraimLocalMCPServer {
1652
1579
  process.stdin.on('end', () => {
1653
1580
  this.log('🛑 Stdin closed, shutting down...');
1654
1581
  cleanup();
1655
- this.emitFallbackSummary('stdin_end');
1656
1582
  process.exit(0);
1657
1583
  });
1658
1584
  process.on('SIGTERM', () => {
1659
1585
  this.log('🛑 SIGTERM received, shutting down...');
1660
1586
  cleanup();
1661
- this.emitFallbackSummary('sigterm');
1662
1587
  process.exit(0);
1663
1588
  });
1664
1589
  process.on('SIGINT', () => {
1665
1590
  this.log('🛑 SIGINT received, shutting down...');
1666
1591
  cleanup();
1667
- this.emitFallbackSummary('sigint');
1668
1592
  process.exit(0);
1669
1593
  });
1670
1594
  this.log('✅ FRAIM Local MCP Server ready');
@@ -1673,13 +1597,14 @@ class FraimLocalMCPServer {
1673
1597
  * Flush collected usage data to the local database
1674
1598
  */
1675
1599
  async uploadUsageData() {
1676
- if (this.usageCollector.getEventCount() === 0) {
1600
+ const eventCount = this.usageCollector.getEventCount();
1601
+ this.log(`📊 Upload check: ${eventCount} events queued`);
1602
+ if (eventCount === 0) {
1677
1603
  return; // Nothing to flush
1678
1604
  }
1679
1605
  try {
1680
- const count = this.usageCollector.getEventCount();
1681
1606
  await this.usageCollector.flush(this.remoteUrl, this.apiKey);
1682
- this.log(`📊 Flushed ${count} usage events to remote server`);
1607
+ this.log(`📊 Flushed ${eventCount} usage events to remote server`);
1683
1608
  }
1684
1609
  catch (error) {
1685
1610
  this.log(`❌ Usage flushing error: ${error.message}`);
@@ -1692,8 +1617,6 @@ FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE = [
1692
1617
  'Replace all `agent.*` placeholders with the best value from repository context, user intent, and available project config.'
1693
1618
  ].join('\n');
1694
1619
  FraimLocalMCPServer.FALLBACK_ALERT_MARKER = 'PROXY_FALLBACK_ALERT';
1695
- FraimLocalMCPServer.FALLBACK_SUMMARY_MARKER = 'PROXY_FALLBACK_SUMMARY';
1696
- FraimLocalMCPServer.FALLBACK_HEADER = 'x-fraim-proxy-fallback-telemetry';
1697
1620
  FraimLocalMCPServer.SESSION_HEADER = 'x-fraim-session-id';
1698
1621
  // Start server if run directly
1699
1622
  if (require.main === module) {
@@ -4,7 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.UsageCollector = void 0;
7
+ const mongodb_1 = require("mongodb");
7
8
  const axios_1 = __importDefault(require("axios"));
9
+ // A placeholder ObjectId used when the real API key ID is not yet known.
10
+ // The server will override this with the correct ID from the authenticated API key.
11
+ const PLACEHOLDER_API_KEY_ID = new mongodb_1.ObjectId('000000000000000000000000');
8
12
  /**
9
13
  * Usage event collector for local MCP proxy
10
14
  * Collects usage events and batches them for upload
@@ -24,16 +28,17 @@ class UsageCollector {
24
28
  * Collect MCP tool call event
25
29
  */
26
30
  collectMCPCall(toolName, args, sessionId, success = true, duration) {
27
- if (!this.apiKeyId) {
28
- return; // Silently skip if no API key
29
- }
30
31
  const parsed = this.parseMCPCall(toolName, args);
31
- if (!parsed)
32
+ if (!parsed) {
33
+ console.error(`[UsageCollector] 🚫 Tool not tracked: ${toolName}`);
32
34
  return;
35
+ }
33
36
  const event = {
34
37
  type: parsed.type,
35
38
  name: parsed.name,
36
- apiKeyId: this.apiKeyId,
39
+ // Use set apiKeyId if available, otherwise a placeholder.
40
+ // The server will override this with the correct value from the auth token.
41
+ apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
37
42
  sessionId,
38
43
  success
39
44
  };
@@ -41,18 +46,18 @@ class UsageCollector {
41
46
  event.duration = duration;
42
47
  }
43
48
  this.events.push(event);
49
+ console.error(`[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`);
44
50
  }
45
51
  /**
46
52
  * Collect usage event directly (for backward compatibility with tests)
47
53
  */
48
54
  collectEvent(type, name, sessionId, success = true, duration) {
49
- if (!this.apiKeyId) {
50
- return; // Silently skip if no API key
51
- }
52
55
  const event = {
53
56
  type,
54
57
  name,
55
- apiKeyId: this.apiKeyId,
58
+ // Use set apiKeyId if available, otherwise a placeholder.
59
+ // The server will override this with the correct value from the auth token.
60
+ apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
56
61
  sessionId,
57
62
  success
58
63
  };
@@ -97,12 +102,15 @@ class UsageCollector {
97
102
  * Flush events to the remote server via API
98
103
  */
99
104
  async flush(remoteUrl, apiKey) {
100
- if (this.events.length === 0)
105
+ if (this.events.length === 0) {
106
+ console.error(`[UsageCollector] 📊 No events to flush`);
101
107
  return;
108
+ }
102
109
  const events = [...this.events];
103
110
  this.events = [];
111
+ console.error(`[UsageCollector] 📤 Flushing ${events.length} events to ${remoteUrl}/api/analytics/events`);
104
112
  try {
105
- await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
113
+ const response = await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
106
114
  events,
107
115
  apiKey
108
116
  }, {
@@ -112,12 +120,13 @@ class UsageCollector {
112
120
  'Content-Type': 'application/json'
113
121
  }
114
122
  });
123
+ console.error(`[UsageCollector] ✅ Successfully flushed ${events.length} events (HTTP ${response.status})`);
115
124
  // Success - events are already cleared from the queue
116
125
  }
117
126
  catch (error) {
118
127
  const status = error.response?.status;
119
128
  const message = error.response?.data?.error || error.message;
120
- console.error(`❌ Failed to flush usage events (HTTP ${status}): ${message}`);
129
+ console.error(`[UsageCollector] Failed to flush usage events (HTTP ${status}): ${message}`);
121
130
  // Put events back at the beginning of the queue for next try
122
131
  this.events = [...events, ...this.events];
123
132
  }
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
@@ -36,13 +36,13 @@ function runCLI() {
36
36
  // Solution: On Windows with shell: true, quote paths containing spaces.
37
37
  // On Unix with shell: false, pass the path unquoted (spawnSync handles it correctly).
38
38
  const isWindows = process.platform === 'win32';
39
-
39
+
40
40
  // On Windows with shell, quote paths with spaces to prevent shell misinterpretation
41
41
  // On Unix without shell, pass path as-is (spawnSync handles spaces correctly)
42
- const processedSrcPath = (isWindows && srcPath.includes(' '))
43
- ? `"${srcPath}"`
42
+ const processedSrcPath = (isWindows && srcPath.includes(' '))
43
+ ? `"${srcPath}"`
44
44
  : srcPath;
45
-
45
+
46
46
  const result = spawnSync(
47
47
  'npx',
48
48
  ['tsx', processedSrcPath, ...process.argv.slice(2)],
@@ -66,7 +66,7 @@ function runCLI() {
66
66
  module.exports = {
67
67
  FRAIM_INFO: {
68
68
  name: 'FRAIM',
69
- version: '2.0.54',
69
+ version: '2.0.93',
70
70
  repository: 'https://github.com/mathursrus/FRAIM'
71
71
  }
72
72
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.93",
3
+ "version": "2.0.95",
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": {
@@ -10,10 +10,9 @@
10
10
  "scripts": {
11
11
  "dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
12
12
  "dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
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",
13
+ "build": "tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && node -e \"require('fs').copyFileSync('src/core/types.ts', 'registry/templates/manager/fraim-config-schema.ts')\" && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
14
14
  "build:stubs": "tsx scripts/build-stub-registry.ts",
15
15
  "build:fraim-brain": "node scripts/generate-fraim-brain.js",
16
- "build:config-schema": "tsx scripts/generate-config-schema-template.ts",
17
16
  "test-all": "npm run test && npm run test:isolated && npm run test:ui",
18
17
  "test": "node scripts/test-with-server.js",
19
18
  "test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
@@ -1,83 +0,0 @@
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;