fraim 2.0.100

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.
Files changed (70) hide show
  1. package/README.md +445 -0
  2. package/bin/fraim.js +23 -0
  3. package/dist/src/cli/api/get-provider-client.js +41 -0
  4. package/dist/src/cli/api/provider-client.js +107 -0
  5. package/dist/src/cli/commands/add-ide.js +430 -0
  6. package/dist/src/cli/commands/add-provider.js +233 -0
  7. package/dist/src/cli/commands/doctor.js +149 -0
  8. package/dist/src/cli/commands/init-project.js +301 -0
  9. package/dist/src/cli/commands/list-overridable.js +184 -0
  10. package/dist/src/cli/commands/list.js +57 -0
  11. package/dist/src/cli/commands/login.js +84 -0
  12. package/dist/src/cli/commands/mcp.js +15 -0
  13. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  14. package/dist/src/cli/commands/override.js +177 -0
  15. package/dist/src/cli/commands/setup.js +651 -0
  16. package/dist/src/cli/commands/sync.js +162 -0
  17. package/dist/src/cli/commands/test-mcp.js +171 -0
  18. package/dist/src/cli/doctor/check-runner.js +199 -0
  19. package/dist/src/cli/doctor/checks/global-setup-checks.js +220 -0
  20. package/dist/src/cli/doctor/checks/ide-config-checks.js +250 -0
  21. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +381 -0
  22. package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
  23. package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
  24. package/dist/src/cli/doctor/checks/workflow-checks.js +251 -0
  25. package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
  26. package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
  27. package/dist/src/cli/doctor/types.js +6 -0
  28. package/dist/src/cli/fraim.js +100 -0
  29. package/dist/src/cli/internal/device-flow-service.js +83 -0
  30. package/dist/src/cli/mcp/ide-formats.js +243 -0
  31. package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
  32. package/dist/src/cli/mcp/mcp-server-registry.js +160 -0
  33. package/dist/src/cli/mcp/types.js +3 -0
  34. package/dist/src/cli/providers/local-provider-registry.js +166 -0
  35. package/dist/src/cli/providers/provider-registry.js +230 -0
  36. package/dist/src/cli/setup/auto-mcp-setup.js +331 -0
  37. package/dist/src/cli/setup/codex-local-config.js +37 -0
  38. package/dist/src/cli/setup/first-run.js +242 -0
  39. package/dist/src/cli/setup/ide-detector.js +179 -0
  40. package/dist/src/cli/setup/mcp-config-generator.js +192 -0
  41. package/dist/src/cli/setup/provider-prompts.js +339 -0
  42. package/dist/src/cli/utils/agent-adapters.js +126 -0
  43. package/dist/src/cli/utils/digest-utils.js +47 -0
  44. package/dist/src/cli/utils/fraim-gitignore.js +40 -0
  45. package/dist/src/cli/utils/platform-detection.js +258 -0
  46. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  47. package/dist/src/cli/utils/remote-sync.js +315 -0
  48. package/dist/src/cli/utils/script-sync-utils.js +221 -0
  49. package/dist/src/cli/utils/version-utils.js +32 -0
  50. package/dist/src/core/ai-mentor.js +230 -0
  51. package/dist/src/core/config-loader.js +114 -0
  52. package/dist/src/core/config-writer.js +75 -0
  53. package/dist/src/core/types.js +23 -0
  54. package/dist/src/core/utils/git-utils.js +95 -0
  55. package/dist/src/core/utils/include-resolver.js +92 -0
  56. package/dist/src/core/utils/inheritance-parser.js +288 -0
  57. package/dist/src/core/utils/job-parser.js +176 -0
  58. package/dist/src/core/utils/local-registry-resolver.js +616 -0
  59. package/dist/src/core/utils/object-utils.js +11 -0
  60. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  61. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  62. package/dist/src/core/utils/provider-utils.js +18 -0
  63. package/dist/src/core/utils/server-startup.js +34 -0
  64. package/dist/src/core/utils/stub-generator.js +147 -0
  65. package/dist/src/core/utils/workflow-parser.js +174 -0
  66. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  67. package/dist/src/local-mcp-server/stdio-server.js +1698 -0
  68. package/dist/src/local-mcp-server/usage-collector.js +264 -0
  69. package/index.js +85 -0
  70. package/package.json +139 -0
@@ -0,0 +1,103 @@
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.LEGACY_WORKSPACE_FRAIM_DIRNAME = void 0;
7
+ exports.migrateLegacyProjectFraimDir = migrateLegacyProjectFraimDir;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const project_fraim_paths_1 = require("./project-fraim-paths");
11
+ exports.LEGACY_WORKSPACE_FRAIM_DIRNAME = '.fraim';
12
+ const MIGRATABLE_LEGACY_ENTRIES = [
13
+ 'config.json',
14
+ 'personalized-employee'
15
+ ];
16
+ const PRUNABLE_LEGACY_SYNCED_ENTRIES = [
17
+ 'ai-employee',
18
+ 'ai-manager',
19
+ 'docs',
20
+ 'jobs',
21
+ 'workflows'
22
+ ];
23
+ function getLegacyWorkspaceFraimDir(projectRoot) {
24
+ return path_1.default.join(projectRoot, exports.LEGACY_WORKSPACE_FRAIM_DIRNAME);
25
+ }
26
+ function areFilesEqual(leftPath, rightPath) {
27
+ try {
28
+ return fs_1.default.readFileSync(leftPath, 'utf8') === fs_1.default.readFileSync(rightPath, 'utf8');
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function removeDirectoryIfEmpty(dirPath) {
35
+ if (!fs_1.default.existsSync(dirPath)) {
36
+ return;
37
+ }
38
+ const entries = fs_1.default.readdirSync(dirPath);
39
+ if (entries.length === 0) {
40
+ fs_1.default.rmdirSync(dirPath);
41
+ }
42
+ }
43
+ function isDirectoryEmpty(dirPath) {
44
+ if (!fs_1.default.existsSync(dirPath)) {
45
+ return true;
46
+ }
47
+ return fs_1.default.readdirSync(dirPath).length === 0;
48
+ }
49
+ function moveOrMergePath(sourcePath, targetPath, relativePath, result) {
50
+ if (!fs_1.default.existsSync(sourcePath)) {
51
+ return;
52
+ }
53
+ const sourceStats = fs_1.default.statSync(sourcePath);
54
+ if (!fs_1.default.existsSync(targetPath)) {
55
+ fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
56
+ fs_1.default.renameSync(sourcePath, targetPath);
57
+ result.migratedPaths.push(relativePath.replace(/\\/g, '/'));
58
+ return;
59
+ }
60
+ const targetStats = fs_1.default.statSync(targetPath);
61
+ if (sourceStats.isDirectory() && targetStats.isDirectory()) {
62
+ for (const entry of fs_1.default.readdirSync(sourcePath, { withFileTypes: true })) {
63
+ moveOrMergePath(path_1.default.join(sourcePath, entry.name), path_1.default.join(targetPath, entry.name), path_1.default.posix.join(relativePath.replace(/\\/g, '/'), entry.name), result);
64
+ }
65
+ removeDirectoryIfEmpty(sourcePath);
66
+ return;
67
+ }
68
+ if (!sourceStats.isDirectory() && !targetStats.isDirectory() && areFilesEqual(sourcePath, targetPath)) {
69
+ fs_1.default.unlinkSync(sourcePath);
70
+ return;
71
+ }
72
+ result.conflictPaths.push(relativePath.replace(/\\/g, '/'));
73
+ }
74
+ function migrateLegacyProjectFraimDir(projectRoot = process.cwd()) {
75
+ const workspaceDirPath = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
76
+ const legacyDirPath = getLegacyWorkspaceFraimDir(projectRoot);
77
+ const result = {
78
+ legacyDirPath,
79
+ workspaceDirPath,
80
+ legacyDirFound: fs_1.default.existsSync(legacyDirPath),
81
+ migratedPaths: [],
82
+ prunedLegacyPaths: [],
83
+ conflictPaths: [],
84
+ legacyDirReadyForDeletion: false
85
+ };
86
+ if (!result.legacyDirFound) {
87
+ return result;
88
+ }
89
+ fs_1.default.mkdirSync(workspaceDirPath, { recursive: true });
90
+ for (const entry of MIGRATABLE_LEGACY_ENTRIES) {
91
+ moveOrMergePath(path_1.default.join(legacyDirPath, entry), path_1.default.join(workspaceDirPath, entry), entry, result);
92
+ }
93
+ for (const entry of PRUNABLE_LEGACY_SYNCED_ENTRIES) {
94
+ const legacyEntryPath = path_1.default.join(legacyDirPath, entry);
95
+ if (!fs_1.default.existsSync(legacyEntryPath)) {
96
+ continue;
97
+ }
98
+ fs_1.default.rmSync(legacyEntryPath, { recursive: true, force: true });
99
+ result.prunedLegacyPaths.push(entry);
100
+ }
101
+ result.legacyDirReadyForDeletion = result.conflictPaths.length === 0 && isDirectoryEmpty(legacyDirPath);
102
+ return result;
103
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WORKSPACE_SYNCED_CONTENT_DIRS = exports.WORKSPACE_FRAIM_DIRNAME = void 0;
4
+ exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
5
+ exports.workspaceFraimExists = workspaceFraimExists;
6
+ exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
7
+ exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
8
+ exports.getWorkspaceFraimDisplayPath = getWorkspaceFraimDisplayPath;
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ exports.WORKSPACE_FRAIM_DIRNAME = 'fraim';
12
+ exports.WORKSPACE_SYNCED_CONTENT_DIRS = [
13
+ 'workflows',
14
+ 'ai-employee',
15
+ 'ai-manager',
16
+ 'docs'
17
+ ];
18
+ function normalizeRelativePath(relativePath) {
19
+ return relativePath.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '/');
20
+ }
21
+ function getWorkspaceFraimDir(projectRoot = process.cwd()) {
22
+ return (0, path_1.join)(projectRoot, exports.WORKSPACE_FRAIM_DIRNAME);
23
+ }
24
+ function workspaceFraimExists(projectRoot = process.cwd()) {
25
+ return (0, fs_1.existsSync)(getWorkspaceFraimDir(projectRoot));
26
+ }
27
+ function getWorkspaceConfigPath(projectRoot = process.cwd()) {
28
+ return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), 'config.json');
29
+ }
30
+ function getWorkspaceFraimPath(projectRoot = process.cwd(), ...parts) {
31
+ return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), ...parts);
32
+ }
33
+ function getWorkspaceFraimDisplayPath(relativePath = '') {
34
+ const normalized = normalizeRelativePath(relativePath);
35
+ return normalized.length > 0
36
+ ? `${exports.WORKSPACE_FRAIM_DIRNAME}/${normalized}`
37
+ : `${exports.WORKSPACE_FRAIM_DIRNAME}/`;
38
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectProvider = detectProvider;
4
+ /**
5
+ * Detect git provider from URL
6
+ */
7
+ function detectProvider(url) {
8
+ if (!url)
9
+ return 'github';
10
+ const normalized = url.toLowerCase();
11
+ if (normalized.includes('gitlab.com') || normalized.includes('gitlab.')) {
12
+ return 'gitlab';
13
+ }
14
+ if (normalized.includes('dev.azure.com') || normalized.includes('visualstudio.com')) {
15
+ return 'ado';
16
+ }
17
+ return 'github';
18
+ }
@@ -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
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateJobStub = generateJobStub;
4
+ exports.generateSkillStub = generateSkillStub;
5
+ exports.generateRuleStub = generateRuleStub;
6
+ exports.parseRegistryJob = parseRegistryJob;
7
+ exports.parseRegistrySkill = parseRegistrySkill;
8
+ exports.parseRegistryRule = parseRegistryRule;
9
+ const STUB_MARKER = '<!-- FRAIM_DISCOVERY_STUB -->';
10
+ function extractSection(content, headingPatterns) {
11
+ for (const heading of headingPatterns) {
12
+ const pattern = new RegExp(`(?:^|\\n)#{2,3}\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n#{2,3}\\s|$)`, 'i');
13
+ const match = content.match(pattern);
14
+ if (match?.[1]) {
15
+ const section = match[1].trim();
16
+ if (section.length > 0)
17
+ return section;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ function extractLeadParagraph(content) {
23
+ const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/m, '');
24
+ const lines = withoutFrontmatter.split(/\r?\n/);
25
+ const paragraphLines = [];
26
+ let started = false;
27
+ for (const rawLine of lines) {
28
+ const line = rawLine.trim();
29
+ if (!started) {
30
+ if (!line || line.startsWith('#'))
31
+ continue;
32
+ if (line.startsWith('-') || /^\d+\./.test(line))
33
+ continue;
34
+ started = true;
35
+ }
36
+ if (!line)
37
+ break;
38
+ if (line.startsWith('#'))
39
+ break;
40
+ paragraphLines.push(line);
41
+ }
42
+ if (paragraphLines.length === 0)
43
+ return null;
44
+ return paragraphLines.join(' ').trim();
45
+ }
46
+ /**
47
+ * Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
48
+ */
49
+ function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
50
+ return `${STUB_MARKER}
51
+ # FRAIM Job: ${jobName}
52
+
53
+ ## Intent
54
+ ${intent}
55
+
56
+ ## Outcome
57
+ ${outcome}
58
+
59
+ ## Steps
60
+ ${steps}
61
+
62
+ ---
63
+
64
+ > [!IMPORTANT]
65
+ > **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
66
+ > This stub is for discoverability only. The actual job has multiple phases with
67
+ > detailed steps, validation criteria, and required skills.
68
+ >
69
+ > To get full phase-by-phase instructions, call:
70
+ > \`get_fraim_job({ job: "${jobName}" })\`
71
+ >
72
+ > Then follow all phases using \`seekMentoring\` at each phase transition.
73
+ `;
74
+ }
75
+ /**
76
+ * Generates a lightweight markdown stub for a skill.
77
+ */
78
+ function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
79
+ return `${STUB_MARKER}
80
+ # FRAIM Skill: ${skillName}
81
+
82
+ ## Skill Input
83
+ ${skillInput}
84
+
85
+ ## Skill Output
86
+ ${skillOutput}
87
+
88
+ ---
89
+
90
+ > [!IMPORTANT]
91
+ > **For AI Agents:** This is a discoverability stub for the skill.
92
+ > All execution details must be fetched from MCP before use.
93
+ > To retrieve the complete skill instructions, call:
94
+ > \`get_fraim_file({ path: "skills/${skillPath}" })\`
95
+ `;
96
+ }
97
+ /**
98
+ * Generates a lightweight markdown stub for a rule.
99
+ */
100
+ function generateRuleStub(ruleName, rulePath, intent) {
101
+ return `${STUB_MARKER}
102
+ # FRAIM Rule: ${ruleName}
103
+
104
+ ## Intent
105
+ ${intent}
106
+
107
+ ---
108
+
109
+ > [!IMPORTANT]
110
+ > **For AI Agents:** This is a discoverability stub for the rule.
111
+ > All rule details must be fetched from MCP before use.
112
+ > To retrieve the complete rule instructions, call:
113
+ > \`get_fraim_file({ path: "rules/${rulePath}" })\`
114
+ `;
115
+ }
116
+ /**
117
+ * Parses a job file from the registry to extract its intent, outcome, and steps for the stub.
118
+ */
119
+ function parseRegistryJob(content) {
120
+ const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
121
+ const outcomeMatch = content.match(/##\s*outcome\s+([\s\S]*?)(?=\n##|$)/i);
122
+ const stepsMatch = content.match(/##\s*steps\s+([\s\S]*?)(?=\n##|$)/i);
123
+ const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
124
+ const outcome = outcomeMatch ? outcomeMatch[1].trim() : 'No outcome defined.';
125
+ const steps = stepsMatch ? stepsMatch[1].trim() : 'Use get_fraim_job to retrieve the full phase-by-phase execution steps.';
126
+ return { intent, outcome, steps };
127
+ }
128
+ /**
129
+ * Parses a skill file from the registry to extract intent and expected outcome for stubs.
130
+ */
131
+ function parseRegistrySkill(content) {
132
+ const skillInput = extractSection(content, ['skill input', 'input', 'intent', 'skill intent']) ||
133
+ extractLeadParagraph(content) ||
134
+ 'Use this skill only when the task matches the skill trigger, context, and required inputs.';
135
+ const skillOutput = extractSection(content, ['skill output', 'output', 'outcome', 'expected behavior']) ||
136
+ 'Produce the concrete deliverable or decision this skill is designed to return.';
137
+ return { skillInput, skillOutput };
138
+ }
139
+ /**
140
+ * Parses a rule file from the registry to extract intent for stubs.
141
+ */
142
+ function parseRegistryRule(content) {
143
+ const intent = extractSection(content, ['intent']) ||
144
+ extractLeadParagraph(content) ||
145
+ 'Follow this rule when executing related FRAIM workflows and jobs.';
146
+ return { intent };
147
+ }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkflowParser = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
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
+ }
74
+ /**
75
+ * Parse a workflow markdown file into a structured definition
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
80
+ */
81
+ static parse(filePath) {
82
+ if (!(0, fs_1.existsSync)(filePath))
83
+ return null;
84
+ let content = (0, fs_1.readFileSync)(filePath, 'utf-8');
85
+ if (content.charCodeAt(0) === 0xfeff) {
86
+ content = content.slice(1);
87
+ }
88
+ const metadataBlock = this.extractMetadataBlock(content);
89
+ if (metadataBlock.state === 'invalid') {
90
+ return null;
91
+ }
92
+ if (metadataBlock.state === 'valid') {
93
+ return this.parsePhaseBasedWorkflow(filePath, content, metadataBlock.metadata, metadataBlock.bodyStartIndex);
94
+ }
95
+ return this.parseSimpleWorkflow(filePath, content);
96
+ }
97
+ static parsePhaseBasedWorkflow(filePath, content, metadata, bodyStartIndex) {
98
+ const contentAfterMetadata = content.substring(bodyStartIndex).trim();
99
+ const firstPhaseIndex = contentAfterMetadata.search(/^##\s+Phase:/m);
100
+ let overview = '';
101
+ let restOfContent = '';
102
+ if (firstPhaseIndex !== -1) {
103
+ overview = contentAfterMetadata.substring(0, firstPhaseIndex).trim();
104
+ restOfContent = contentAfterMetadata.substring(firstPhaseIndex);
105
+ }
106
+ else {
107
+ overview = contentAfterMetadata;
108
+ }
109
+ const phases = new Map();
110
+ const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
111
+ if (!metadata.phases) {
112
+ metadata.phases = {};
113
+ }
114
+ for (let i = 1; i < phaseSections.length; i++) {
115
+ const section = phaseSections[i];
116
+ const sectionLines = section.split('\n');
117
+ const firstLine = sectionLines[0].trim();
118
+ const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
119
+ phases.set(id, `## Phase: ${section.trim()}`);
120
+ }
121
+ return {
122
+ metadata,
123
+ overview,
124
+ phases,
125
+ isSimple: false,
126
+ path: filePath
127
+ };
128
+ }
129
+ static parseSimpleWorkflow(filePath, content) {
130
+ const workflowName = (0, path_1.basename)(filePath, '.md');
131
+ const metadata = {
132
+ name: workflowName
133
+ };
134
+ return {
135
+ metadata,
136
+ overview: content.trim(),
137
+ phases: new Map(),
138
+ isSimple: true,
139
+ path: filePath
140
+ };
141
+ }
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
+ }
159
+ static getOverview(filePath) {
160
+ const wf = this.parse(filePath);
161
+ return wf ? wf.overview : null;
162
+ }
163
+ static extractDescription(filePath) {
164
+ const wf = this.parse(filePath);
165
+ if (!wf)
166
+ return '';
167
+ const intentMatch = wf.overview.match(/## Intent\s+([\s\S]+?)(?:\r?\n##|$)/);
168
+ if (intentMatch)
169
+ return intentMatch[1].trim().split(/\r?\n/)[0];
170
+ const firstPara = wf.overview.split(/\r?\n/).find(l => l.trim() !== '' && !l.startsWith('#'));
171
+ return firstPara ? firstPara.trim() : '';
172
+ }
173
+ }
174
+ exports.WorkflowParser = WorkflowParser;