fraim-framework 2.0.87 → 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/README.md +30 -0
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-provider.js +16 -6
- package/dist/src/cli/commands/init-project.js +103 -1
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/setup.js +135 -13
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -10
- package/dist/src/cli/providers/local-provider-registry.js +22 -1
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +72 -32
- package/dist/src/core/ai-mentor.js +248 -0
- package/dist/src/core/utils/git-utils.js +6 -6
- package/dist/src/core/utils/include-resolver.js +45 -0
- package/dist/src/core/utils/inheritance-parser.js +154 -16
- package/dist/src/core/utils/local-registry-resolver.js +326 -22
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +62 -55
- package/dist/src/core/utils/workflow-parser.js +103 -46
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +27 -6
- package/package.json +14 -5
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPortConflictError = createPortConflictError;
|
|
4
|
+
exports.startListening = startListening;
|
|
5
|
+
exports.logStartupError = logStartupError;
|
|
6
|
+
function createPortConflictError(port) {
|
|
7
|
+
const error = new Error(`Port ${port} is already in use. Another process is already listening on http://localhost:${port}. Stop that process or set FRAIM_MCP_PORT/PORT to a different port.`);
|
|
8
|
+
error.code = 'EADDRINUSE';
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
async function startListening(app, port, onListening) {
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
const server = app.listen(port);
|
|
14
|
+
server.once('listening', () => {
|
|
15
|
+
onListening();
|
|
16
|
+
resolve();
|
|
17
|
+
});
|
|
18
|
+
server.once('error', (error) => {
|
|
19
|
+
if (error?.code === 'EADDRINUSE') {
|
|
20
|
+
reject(createPortConflictError(port));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function logStartupError(error) {
|
|
28
|
+
const maybePortError = error;
|
|
29
|
+
if (maybePortError?.code === 'EADDRINUSE') {
|
|
30
|
+
console.error(`[FRAIM PORT] ${maybePortError.message}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.error('❌ Failed to start FRAIM MCP Server:', error);
|
|
34
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.generateRuleStub = generateRuleStub;
|
|
|
8
8
|
exports.parseRegistryJob = parseRegistryJob;
|
|
9
9
|
exports.parseRegistrySkill = parseRegistrySkill;
|
|
10
10
|
exports.parseRegistryRule = parseRegistryRule;
|
|
11
|
+
const STUB_MARKER = '<!-- FRAIM_DISCOVERY_STUB -->';
|
|
11
12
|
function extractSection(content, headingPatterns) {
|
|
12
13
|
for (const heading of headingPatterns) {
|
|
13
14
|
const pattern = new RegExp(`(?:^|\\n)#{2,3}\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n#{2,3}\\s|$)`, 'i');
|
|
@@ -49,10 +50,11 @@ function extractLeadParagraph(content) {
|
|
|
49
50
|
* These stubs are committed to the user's repo for discoverability.
|
|
50
51
|
*/
|
|
51
52
|
function generateWorkflowStub(workflowName, workflowPath, intent, principles) {
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
>
|
|
53
|
+
return `${STUB_MARKER}
|
|
54
|
+
# FRAIM Workflow: ${workflowName}
|
|
55
|
+
|
|
56
|
+
> [!IMPORTANT]
|
|
57
|
+
> This is a **FRAIM-managed workflow stub**.
|
|
56
58
|
> To load the full context (rules, templates, and execution steps), ask your AI agent to:
|
|
57
59
|
> \`@fraim get_fraim_workflow("${workflowName}")\`
|
|
58
60
|
>
|
|
@@ -78,16 +80,20 @@ function parseRegistryWorkflow(content) {
|
|
|
78
80
|
/**
|
|
79
81
|
* Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
|
|
80
82
|
*/
|
|
81
|
-
function generateJobStub(jobName, _jobPath, intent, outcome) {
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
---
|
|
91
97
|
|
|
92
98
|
> [!IMPORTANT]
|
|
93
99
|
> **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
|
|
@@ -103,72 +109,73 @@ ${outcome}
|
|
|
103
109
|
/**
|
|
104
110
|
* Generates a lightweight markdown stub for a skill.
|
|
105
111
|
*/
|
|
106
|
-
function generateSkillStub(skillName, skillPath,
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
>
|
|
119
|
-
>
|
|
120
|
-
>
|
|
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}" })\`
|
|
121
129
|
`;
|
|
122
130
|
}
|
|
123
131
|
/**
|
|
124
132
|
* Generates a lightweight markdown stub for a rule.
|
|
125
133
|
*/
|
|
126
|
-
function generateRuleStub(ruleName, rulePath, intent
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
>
|
|
138
|
-
>
|
|
139
|
-
>
|
|
140
|
-
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
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}" })\`
|
|
141
148
|
`;
|
|
142
149
|
}
|
|
143
150
|
/**
|
|
144
|
-
* Parses a job file from the registry to extract its intent and
|
|
151
|
+
* Parses a job file from the registry to extract its intent, outcome, and steps for the stub.
|
|
145
152
|
*/
|
|
146
153
|
function parseRegistryJob(content) {
|
|
147
154
|
const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
|
|
148
155
|
const outcomeMatch = content.match(/##\s*outcome\s+([\s\S]*?)(?=\n##|$)/i);
|
|
156
|
+
const stepsMatch = content.match(/##\s*steps\s+([\s\S]*?)(?=\n##|$)/i);
|
|
149
157
|
const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
|
|
150
158
|
const outcome = outcomeMatch ? outcomeMatch[1].trim() : 'No outcome defined.';
|
|
151
|
-
|
|
159
|
+
const steps = stepsMatch ? stepsMatch[1].trim() : 'Use get_fraim_job to retrieve the full phase-by-phase execution steps.';
|
|
160
|
+
return { intent, outcome, steps };
|
|
152
161
|
}
|
|
153
162
|
/**
|
|
154
163
|
* Parses a skill file from the registry to extract intent and expected outcome for stubs.
|
|
155
164
|
*/
|
|
156
165
|
function parseRegistrySkill(content) {
|
|
157
|
-
const
|
|
166
|
+
const skillInput = extractSection(content, ['skill input', 'input', 'intent', 'skill intent']) ||
|
|
158
167
|
extractLeadParagraph(content) ||
|
|
159
|
-
'
|
|
160
|
-
const
|
|
161
|
-
'Produce the
|
|
162
|
-
return {
|
|
168
|
+
'Use this skill only when the task matches the skill trigger, context, and required inputs.';
|
|
169
|
+
const skillOutput = extractSection(content, ['skill output', 'output', 'outcome', 'expected behavior']) ||
|
|
170
|
+
'Produce the concrete deliverable or decision this skill is designed to return.';
|
|
171
|
+
return { skillInput, skillOutput };
|
|
163
172
|
}
|
|
164
173
|
/**
|
|
165
|
-
* Parses a rule file from the registry to extract intent
|
|
174
|
+
* Parses a rule file from the registry to extract intent for stubs.
|
|
166
175
|
*/
|
|
167
176
|
function parseRegistryRule(content) {
|
|
168
177
|
const intent = extractSection(content, ['intent']) ||
|
|
169
178
|
extractLeadParagraph(content) ||
|
|
170
179
|
'Follow this rule when executing related FRAIM workflows and jobs.';
|
|
171
|
-
|
|
172
|
-
'Consistently apply this rule throughout execution.';
|
|
173
|
-
return { intent, outcome };
|
|
180
|
+
return { intent };
|
|
174
181
|
}
|
|
@@ -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,34 +106,28 @@ 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
|
-
|
|
111
|
+
if (!metadata.phases) {
|
|
112
|
+
metadata.phases = {};
|
|
113
|
+
}
|
|
60
114
|
for (let i = 1; i < phaseSections.length; i++) {
|
|
61
115
|
const section = phaseSections[i];
|
|
62
116
|
const sectionLines = section.split('\n');
|
|
63
117
|
const firstLine = sectionLines[0].trim();
|
|
64
|
-
// Extract phase ID (slug before any (Phase X) or space)
|
|
65
118
|
const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
|
|
66
|
-
// Store the whole section with header
|
|
67
119
|
phases.set(id, `## Phase: ${section.trim()}`);
|
|
68
120
|
}
|
|
69
121
|
return {
|
|
70
122
|
metadata,
|
|
71
123
|
overview,
|
|
72
124
|
phases,
|
|
73
|
-
isSimple: false
|
|
125
|
+
isSimple: false,
|
|
126
|
+
path: filePath
|
|
74
127
|
};
|
|
75
128
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Parse a simple workflow without frontmatter (bootstrap-style)
|
|
78
|
-
*/
|
|
79
129
|
static parseSimpleWorkflow(filePath, content) {
|
|
80
|
-
// Extract workflow name from filename
|
|
81
130
|
const workflowName = (0, path_1.basename)(filePath, '.md');
|
|
82
|
-
// For simple workflows, the entire content is the overview
|
|
83
|
-
// No phases, just execution steps
|
|
84
131
|
const metadata = {
|
|
85
132
|
name: workflowName
|
|
86
133
|
};
|
|
@@ -88,28 +135,38 @@ class WorkflowParser {
|
|
|
88
135
|
metadata,
|
|
89
136
|
overview: content.trim(),
|
|
90
137
|
phases: new Map(),
|
|
91
|
-
isSimple: true
|
|
138
|
+
isSimple: true,
|
|
139
|
+
path: filePath
|
|
92
140
|
};
|
|
93
141
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
142
|
+
static parseContent(content, name, path) {
|
|
143
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
144
|
+
content = content.slice(1);
|
|
145
|
+
}
|
|
146
|
+
const metadataBlock = this.extractMetadataBlock(content);
|
|
147
|
+
if (metadataBlock.state === 'invalid') {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (metadataBlock.state === 'valid') {
|
|
151
|
+
return this.parsePhaseBasedWorkflow(path || `content:${name}`, content, metadataBlock.metadata, metadataBlock.bodyStartIndex);
|
|
152
|
+
}
|
|
153
|
+
return this.parseSimpleWorkflow(path || `content:${name}`, content);
|
|
154
|
+
}
|
|
155
|
+
static getOverviewFromContent(content, name) {
|
|
156
|
+
const wf = this.parseContent(content, name);
|
|
157
|
+
return wf ? wf.overview : null;
|
|
158
|
+
}
|
|
97
159
|
static getOverview(filePath) {
|
|
98
160
|
const wf = this.parse(filePath);
|
|
99
161
|
return wf ? wf.overview : null;
|
|
100
162
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Extract description for listing workflows (intent or first para of overview)
|
|
103
|
-
*/
|
|
104
163
|
static extractDescription(filePath) {
|
|
105
164
|
const wf = this.parse(filePath);
|
|
106
165
|
if (!wf)
|
|
107
166
|
return '';
|
|
108
|
-
// Try to find Intent section in overview
|
|
109
167
|
const intentMatch = wf.overview.match(/## Intent\s+([\s\S]+?)(?:\r?\n##|$)/);
|
|
110
168
|
if (intentMatch)
|
|
111
169
|
return intentMatch[1].trim().split(/\r?\n/)[0];
|
|
112
|
-
// Fallback to first non-header line
|
|
113
170
|
const firstPara = wf.overview.split(/\r?\n/).find(l => l.trim() !== '' && !l.startsWith('#'));
|
|
114
171
|
return firstPara ? firstPara.trim() : '';
|
|
115
172
|
}
|