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.
@@ -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 `# FRAIM Workflow: ${workflowName}
53
-
54
- > [!IMPORTANT]
55
- > 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**.
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 `# FRAIM Job: ${jobName}
83
-
84
- ## Intent
85
- ${intent}
86
-
87
- ## Outcome
88
- ${outcome}
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, intent, outcome) {
107
- return `# FRAIM Skill: ${skillName}
108
-
109
- ## Intent
110
- ${intent}
111
-
112
- ## Outcome
113
- ${outcome}
114
-
115
- ---
116
-
117
- > [!IMPORTANT]
118
- > **For AI Agents:** This is a skill stub for discoverability.
119
- > To retrieve the complete skill instructions, call:
120
- > \`get_fraim_file({ path: "skills/${skillPath}" })\`
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, outcome) {
127
- return `# FRAIM Rule: ${ruleName}
128
-
129
- ## Intent
130
- ${intent}
131
-
132
- ## Outcome
133
- ${outcome}
134
-
135
- ---
136
-
137
- > [!IMPORTANT]
138
- > **For AI Agents:** This is a rule stub for discoverability.
139
- > To retrieve the complete rule instructions, call:
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 outcome for the stub.
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
- return { intent, outcome };
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 intent = extractSection(content, ['intent', 'skill intent']) ||
166
+ const skillInput = extractSection(content, ['skill input', 'input', 'intent', 'skill intent']) ||
158
167
  extractLeadParagraph(content) ||
159
- 'Apply the skill correctly using the provided inputs and constraints.';
160
- const outcome = extractSection(content, ['outcome', 'expected behavior', 'skill output']) ||
161
- 'Produce the expected skill output while following skill guardrails.';
162
- return { intent, outcome };
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 and expected behavior for stubs.
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
- const outcome = extractSection(content, ['outcome', 'expected behavior', 'principles']) ||
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 two formats:
10
- * 1. Phase-based workflows with JSON frontmatter (e.g., implement, spec)
11
- * 2. Simple workflows without frontmatter (e.g., bootstrap workflows)
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
- // Try to extract JSON Metadata (frontmatter)
22
- const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
23
- if (metadataMatch) {
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
- else {
28
- // Simple workflow without frontmatter
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
- * Parse a phase-based workflow with JSON frontmatter
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
- // Skip the first part (empty or overview overlap)
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
- * Get just the overview for an agent starting a workflow
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
  }