fraim-framework 2.0.94 → 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
@@ -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 = {
@@ -859,6 +867,17 @@ class FraimLocalMCPServer {
859
867
  }
860
868
  // 2. Resolve includes within the content (for all registry tools)
861
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
+ }
862
881
  return this.processResponseWithHydration(finalizedResponse, requestSessionId);
863
882
  }
864
883
  async finalizeLocalToolTextResponse(request, requestSessionId, requestId, text) {
@@ -1395,8 +1414,12 @@ class FraimLocalMCPServer {
1395
1414
  // Single point for usage tracking - log all tool calls
1396
1415
  if (injectedRequest.method === 'tools/call' && requestSessionId && toolName) {
1397
1416
  const success = !processedResponse.error;
1417
+ this.log(`šŸ“Š Collecting usage: ${toolName} (session: ${requestSessionId}, success: ${success})`);
1398
1418
  this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1399
1419
  }
1420
+ else if (injectedRequest.method === 'tools/call') {
1421
+ this.log(`āš ļø Skipping usage collection: toolName=${toolName}, sessionId=${requestSessionId}`);
1422
+ }
1400
1423
  this.log(`šŸ“¤ ${injectedRequest.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1401
1424
  return processedResponse;
1402
1425
  }
@@ -1574,13 +1597,14 @@ class FraimLocalMCPServer {
1574
1597
  * Flush collected usage data to the local database
1575
1598
  */
1576
1599
  async uploadUsageData() {
1577
- if (this.usageCollector.getEventCount() === 0) {
1600
+ const eventCount = this.usageCollector.getEventCount();
1601
+ this.log(`šŸ“Š Upload check: ${eventCount} events queued`);
1602
+ if (eventCount === 0) {
1578
1603
  return; // Nothing to flush
1579
1604
  }
1580
1605
  try {
1581
- const count = this.usageCollector.getEventCount();
1582
1606
  await this.usageCollector.flush(this.remoteUrl, this.apiKey);
1583
- this.log(`šŸ“Š Flushed ${count} usage events to remote server`);
1607
+ this.log(`šŸ“Š Flushed ${eventCount} usage events to remote server`);
1584
1608
  }
1585
1609
  catch (error) {
1586
1610
  this.log(`āŒ Usage flushing error: ${error.message}`);
@@ -29,8 +29,10 @@ class UsageCollector {
29
29
  */
30
30
  collectMCPCall(toolName, args, sessionId, success = true, duration) {
31
31
  const parsed = this.parseMCPCall(toolName, args);
32
- if (!parsed)
32
+ if (!parsed) {
33
+ console.error(`[UsageCollector] 🚫 Tool not tracked: ${toolName}`);
33
34
  return;
35
+ }
34
36
  const event = {
35
37
  type: parsed.type,
36
38
  name: parsed.name,
@@ -44,6 +46,7 @@ class UsageCollector {
44
46
  event.duration = duration;
45
47
  }
46
48
  this.events.push(event);
49
+ console.error(`[UsageCollector] āœ… Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`);
47
50
  }
48
51
  /**
49
52
  * Collect usage event directly (for backward compatibility with tests)
@@ -99,12 +102,15 @@ class UsageCollector {
99
102
  * Flush events to the remote server via API
100
103
  */
101
104
  async flush(remoteUrl, apiKey) {
102
- if (this.events.length === 0)
105
+ if (this.events.length === 0) {
106
+ console.error(`[UsageCollector] šŸ“Š No events to flush`);
103
107
  return;
108
+ }
104
109
  const events = [...this.events];
105
110
  this.events = [];
111
+ console.error(`[UsageCollector] šŸ“¤ Flushing ${events.length} events to ${remoteUrl}/api/analytics/events`);
106
112
  try {
107
- await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
113
+ const response = await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
108
114
  events,
109
115
  apiKey
110
116
  }, {
@@ -114,12 +120,13 @@ class UsageCollector {
114
120
  'Content-Type': 'application/json'
115
121
  }
116
122
  });
123
+ console.error(`[UsageCollector] āœ… Successfully flushed ${events.length} events (HTTP ${response.status})`);
117
124
  // Success - events are already cleared from the queue
118
125
  }
119
126
  catch (error) {
120
127
  const status = error.response?.status;
121
128
  const message = error.response?.data?.error || error.message;
122
- console.error(`āŒ Failed to flush usage events (HTTP ${status}): ${message}`);
129
+ console.error(`[UsageCollector] āŒ Failed to flush usage events (HTTP ${status}): ${message}`);
123
130
  // Put events back at the beginning of the queue for next try
124
131
  this.events = [...events, ...this.events];
125
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.94",
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": {
@@ -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;