fraim-framework 2.0.88 ā 2.0.89
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
|
@@ -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
|
/**
|
|
@@ -4,45 +4,98 @@ exports.WorkflowParser = void 0;
|
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
class WorkflowParser {
|
|
7
|
+
static extractMetadataBlock(content) {
|
|
8
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
9
|
+
if (frontmatterMatch) {
|
|
10
|
+
try {
|
|
11
|
+
return {
|
|
12
|
+
state: 'valid',
|
|
13
|
+
metadata: JSON.parse(frontmatterMatch[1]),
|
|
14
|
+
bodyStartIndex: frontmatterMatch[0].length
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { state: 'invalid' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const trimmedStart = content.search(/\S/);
|
|
22
|
+
if (trimmedStart === -1 || content[trimmedStart] !== '{') {
|
|
23
|
+
return { state: 'none' };
|
|
24
|
+
}
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let inString = false;
|
|
27
|
+
let escaping = false;
|
|
28
|
+
for (let i = trimmedStart; i < content.length; i++) {
|
|
29
|
+
const ch = content[i];
|
|
30
|
+
if (inString) {
|
|
31
|
+
if (escaping) {
|
|
32
|
+
escaping = false;
|
|
33
|
+
}
|
|
34
|
+
else if (ch === '\\') {
|
|
35
|
+
escaping = true;
|
|
36
|
+
}
|
|
37
|
+
else if (ch === '"') {
|
|
38
|
+
inString = false;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === '"') {
|
|
43
|
+
inString = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '{') {
|
|
47
|
+
depth++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === '}') {
|
|
51
|
+
depth--;
|
|
52
|
+
if (depth === 0) {
|
|
53
|
+
const bodyStartIndex = i + 1;
|
|
54
|
+
const remainder = content.slice(bodyStartIndex).trimStart();
|
|
55
|
+
// `{...}\n---` is usually malformed frontmatter, not bare JSON metadata.
|
|
56
|
+
if (remainder.startsWith('---')) {
|
|
57
|
+
return { state: 'none' };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return {
|
|
61
|
+
state: 'valid',
|
|
62
|
+
metadata: JSON.parse(content.slice(trimmedStart, bodyStartIndex)),
|
|
63
|
+
bodyStartIndex
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { state: 'invalid' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { state: 'none' };
|
|
73
|
+
}
|
|
7
74
|
/**
|
|
8
75
|
* Parse a workflow markdown file into a structured definition
|
|
9
|
-
* Supports
|
|
10
|
-
* 1. Phase-based workflows with JSON frontmatter
|
|
11
|
-
* 2.
|
|
76
|
+
* Supports three formats:
|
|
77
|
+
* 1. Phase-based workflows with JSON frontmatter
|
|
78
|
+
* 2. Phase-based workflows with bare leading JSON metadata
|
|
79
|
+
* 3. Simple workflows without metadata
|
|
12
80
|
*/
|
|
13
81
|
static parse(filePath) {
|
|
14
82
|
if (!(0, fs_1.existsSync)(filePath))
|
|
15
83
|
return null;
|
|
16
84
|
let content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
17
|
-
// Strip optional UTF-8 BOM so frontmatter regex remains stable across editors.
|
|
18
85
|
if (content.charCodeAt(0) === 0xfeff) {
|
|
19
86
|
content = content.slice(1);
|
|
20
87
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Phase-based workflow with frontmatter
|
|
25
|
-
return this.parsePhaseBasedWorkflow(filePath, content, metadataMatch);
|
|
88
|
+
const metadataBlock = this.extractMetadataBlock(content);
|
|
89
|
+
if (metadataBlock.state === 'invalid') {
|
|
90
|
+
return null;
|
|
26
91
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return this.parseSimpleWorkflow(filePath, content);
|
|
92
|
+
if (metadataBlock.state === 'valid') {
|
|
93
|
+
return this.parsePhaseBasedWorkflow(filePath, content, metadataBlock.metadata, metadataBlock.bodyStartIndex);
|
|
30
94
|
}
|
|
95
|
+
return this.parseSimpleWorkflow(filePath, content);
|
|
31
96
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
static parsePhaseBasedWorkflow(filePath, content, metadataMatch) {
|
|
36
|
-
let metadata;
|
|
37
|
-
try {
|
|
38
|
-
metadata = JSON.parse(metadataMatch[1]);
|
|
39
|
-
}
|
|
40
|
-
catch (e) {
|
|
41
|
-
console.error(`ā Failed to parse JSON metadata in ${filePath}:`, e);
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
// Extract Overview (Content after metadata but before first phase header)
|
|
45
|
-
const contentAfterMetadata = content.substring(metadataMatch[0].length).trim();
|
|
97
|
+
static parsePhaseBasedWorkflow(filePath, content, metadata, bodyStartIndex) {
|
|
98
|
+
const contentAfterMetadata = content.substring(bodyStartIndex).trim();
|
|
46
99
|
const firstPhaseIndex = contentAfterMetadata.search(/^##\s+Phase:/m);
|
|
47
100
|
let overview = '';
|
|
48
101
|
let restOfContent = '';
|
|
@@ -53,21 +106,16 @@ class WorkflowParser {
|
|
|
53
106
|
else {
|
|
54
107
|
overview = contentAfterMetadata;
|
|
55
108
|
}
|
|
56
|
-
// Extract Phases (id -> content)
|
|
57
109
|
const phases = new Map();
|
|
58
110
|
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
59
|
-
// Ensure metadata has a phases object
|
|
60
111
|
if (!metadata.phases) {
|
|
61
112
|
metadata.phases = {};
|
|
62
113
|
}
|
|
63
|
-
// Skip the first part (empty or overview overlap)
|
|
64
114
|
for (let i = 1; i < phaseSections.length; i++) {
|
|
65
115
|
const section = phaseSections[i];
|
|
66
116
|
const sectionLines = section.split('\n');
|
|
67
117
|
const firstLine = sectionLines[0].trim();
|
|
68
|
-
// Extract phase ID (slug before any (Phase X) or space)
|
|
69
118
|
const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
|
|
70
|
-
// Store the whole section with header
|
|
71
119
|
phases.set(id, `## Phase: ${section.trim()}`);
|
|
72
120
|
}
|
|
73
121
|
return {
|
|
@@ -78,14 +126,8 @@ class WorkflowParser {
|
|
|
78
126
|
path: filePath
|
|
79
127
|
};
|
|
80
128
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Parse a simple workflow without frontmatter (bootstrap-style)
|
|
83
|
-
*/
|
|
84
129
|
static parseSimpleWorkflow(filePath, content) {
|
|
85
|
-
// Extract workflow name from filename
|
|
86
130
|
const workflowName = (0, path_1.basename)(filePath, '.md');
|
|
87
|
-
// For simple workflows, the entire content is the overview
|
|
88
|
-
// No phases, just execution steps
|
|
89
131
|
const metadata = {
|
|
90
132
|
name: workflowName
|
|
91
133
|
};
|
|
@@ -97,49 +139,34 @@ class WorkflowParser {
|
|
|
97
139
|
path: filePath
|
|
98
140
|
};
|
|
99
141
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Parse workflow content from a string
|
|
102
|
-
*/
|
|
103
142
|
static parseContent(content, name, path) {
|
|
104
|
-
// Strip optional UTF-8 BOM
|
|
105
143
|
if (content.charCodeAt(0) === 0xfeff) {
|
|
106
144
|
content = content.slice(1);
|
|
107
145
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return this.parsePhaseBasedWorkflow(path || `content:${name}`, content, metadataMatch);
|
|
146
|
+
const metadataBlock = this.extractMetadataBlock(content);
|
|
147
|
+
if (metadataBlock.state === 'invalid') {
|
|
148
|
+
return null;
|
|
112
149
|
}
|
|
113
|
-
|
|
114
|
-
return this.
|
|
150
|
+
if (metadataBlock.state === 'valid') {
|
|
151
|
+
return this.parsePhaseBasedWorkflow(path || `content:${name}`, content, metadataBlock.metadata, metadataBlock.bodyStartIndex);
|
|
115
152
|
}
|
|
153
|
+
return this.parseSimpleWorkflow(path || `content:${name}`, content);
|
|
116
154
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Get just the overview from content
|
|
119
|
-
*/
|
|
120
155
|
static getOverviewFromContent(content, name) {
|
|
121
156
|
const wf = this.parseContent(content, name);
|
|
122
157
|
return wf ? wf.overview : null;
|
|
123
158
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Get just the overview for an agent starting a workflow
|
|
126
|
-
*/
|
|
127
159
|
static getOverview(filePath) {
|
|
128
160
|
const wf = this.parse(filePath);
|
|
129
161
|
return wf ? wf.overview : null;
|
|
130
162
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Extract description for listing workflows (intent or first para of overview)
|
|
133
|
-
*/
|
|
134
163
|
static extractDescription(filePath) {
|
|
135
164
|
const wf = this.parse(filePath);
|
|
136
165
|
if (!wf)
|
|
137
166
|
return '';
|
|
138
|
-
// Try to find Intent section in overview
|
|
139
167
|
const intentMatch = wf.overview.match(/## Intent\s+([\s\S]+?)(?:\r?\n##|$)/);
|
|
140
168
|
if (intentMatch)
|
|
141
169
|
return intentMatch[1].trim().split(/\r?\n/)[0];
|
|
142
|
-
// Fallback to first non-header line
|
|
143
170
|
const firstPara = wf.overview.split(/\r?\n/).find(l => l.trim() !== '' && !l.startsWith('#'));
|
|
144
171
|
return firstPara ? firstPara.trim() : '';
|
|
145
172
|
}
|
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.89",
|
|
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;
|