fraim-framework 2.0.88 → 2.0.90

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
  /**
@@ -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,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
- // Try to extract JSON Metadata (frontmatter)
109
- const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
110
- if (metadataMatch) {
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
- else {
114
- return this.parseSimpleWorkflow(path || `content:${name}`, content);
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
@@ -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.88",
3
+ "version": "2.0.90",
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;