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 +1 -1
- package/dist/src/core/utils/stub-generator.js +47 -47
- package/dist/src/local-mcp-server/stdio-server.js +27 -3
- package/dist/src/local-mcp-server/usage-collector.js +11 -4
- package/index.js +1 -1
- package/package.json +1 -1
- package/dist/src/cli/services/device-flow-service.js +0 -83
package/bin/fraim.js
CHANGED
|
@@ -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
|
-
|
|
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 ${
|
|
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(
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
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;
|