fraim 2.0.164 → 2.0.166

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,242 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runFirstRunExperience = void 0;
40
+ const prompts_1 = __importDefault(require("prompts"));
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const fs_1 = __importDefault(require("fs"));
43
+ const path_1 = __importDefault(require("path"));
44
+ const os_1 = __importDefault(require("os"));
45
+ const script_sync_utils_1 = require("../utils/script-sync-utils");
46
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
47
+ const loadGlobalConfig = () => {
48
+ const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
49
+ if (!fs_1.default.existsSync(globalConfigPath)) {
50
+ return null;
51
+ }
52
+ try {
53
+ const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
54
+ return {
55
+ fraimKey: config.apiKey,
56
+ githubToken: config.githubToken
57
+ };
58
+ }
59
+ catch (e) {
60
+ return null;
61
+ }
62
+ };
63
+ const checkMCPConfigurations = () => {
64
+ const sources = [];
65
+ // Check Kiro MCP config
66
+ const kiroConfigPath = path_1.default.join(os_1.default.homedir(), '.kiro', 'settings', 'mcp.json');
67
+ if (fs_1.default.existsSync(kiroConfigPath)) {
68
+ try {
69
+ const kiroConfig = JSON.parse(fs_1.default.readFileSync(kiroConfigPath, 'utf8'));
70
+ if (kiroConfig.mcpServers?.fraim) {
71
+ sources.push('Kiro');
72
+ }
73
+ }
74
+ catch (e) {
75
+ // Ignore parsing errors
76
+ }
77
+ }
78
+ // Check Claude Desktop / Cowork config
79
+ const claudeConfigPath = path_1.default.join(os_1.default.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
80
+ if (fs_1.default.existsSync(claudeConfigPath)) {
81
+ try {
82
+ const claudeConfig = JSON.parse(fs_1.default.readFileSync(claudeConfigPath, 'utf8'));
83
+ if (claudeConfig.mcpServers?.fraim) {
84
+ sources.push('Claude Desktop / Cowork');
85
+ }
86
+ }
87
+ catch (e) {
88
+ // Ignore parsing errors
89
+ }
90
+ }
91
+ // Check macOS Claude Desktop / Cowork config path
92
+ const claudeMacPath = path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
93
+ if (fs_1.default.existsSync(claudeMacPath)) {
94
+ try {
95
+ const claudeConfig = JSON.parse(fs_1.default.readFileSync(claudeMacPath, 'utf8'));
96
+ if (claudeConfig.mcpServers?.fraim) {
97
+ sources.push('Claude Desktop / Cowork (macOS)');
98
+ }
99
+ }
100
+ catch (e) {
101
+ // Ignore parsing errors
102
+ }
103
+ }
104
+ // Check VS Code MCP config (uses "servers" key)
105
+ const vscodePaths = [
106
+ path_1.default.join(os_1.default.homedir(), 'AppData', 'Roaming', 'Code', 'User', 'mcp.json'),
107
+ path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json'),
108
+ path_1.default.join(os_1.default.homedir(), '.config', 'Code', 'User', 'mcp.json')
109
+ ];
110
+ for (const vscodePath of vscodePaths) {
111
+ if (fs_1.default.existsSync(vscodePath)) {
112
+ try {
113
+ const vscodeConfig = JSON.parse(fs_1.default.readFileSync(vscodePath, 'utf8'));
114
+ if (vscodeConfig.servers?.fraim) {
115
+ sources.push('VSCode');
116
+ break;
117
+ }
118
+ }
119
+ catch (e) {
120
+ // Ignore parsing errors
121
+ }
122
+ }
123
+ }
124
+ return {
125
+ found: sources.length > 0,
126
+ sources
127
+ };
128
+ };
129
+ const runFirstRunExperience = async () => {
130
+ // Skip interactive setup in CI/Test environments
131
+ if (process.env.CI === 'true' || process.env.TEST_MODE === 'true') {
132
+ console.log(chalk_1.default.yellow('ℹ️ Skipping interactive first-run experience (CI/TEST_MODE detected)'));
133
+ return;
134
+ }
135
+ console.log(chalk_1.default.blue('\n👋 Welcome to FRAIM! Let\'s get you set up.\n'));
136
+ // Check for existing configuration
137
+ const globalConfig = loadGlobalConfig();
138
+ const mcpCheck = checkMCPConfigurations();
139
+ if (globalConfig && globalConfig.fraimKey) {
140
+ console.log(chalk_1.default.green('✅ Found existing FRAIM configuration'));
141
+ if (mcpCheck.found) {
142
+ console.log(chalk_1.default.green(`✅ Found FRAIM MCP configuration in: ${mcpCheck.sources.join(', ')}`));
143
+ console.log(chalk_1.default.blue('🎉 You\'re all set! FRAIM is ready to use.\n'));
144
+ }
145
+ else {
146
+ console.log(chalk_1.default.yellow('⚠️ FRAIM key found but no MCP configuration detected.'));
147
+ console.log(chalk_1.default.cyan('💡 Consider running "fraim add-ide" to configure your IDE.\n'));
148
+ }
149
+ }
150
+ else if (mcpCheck.found) {
151
+ console.log(chalk_1.default.green(`✅ Found FRAIM MCP configuration in: ${mcpCheck.sources.join(', ')}`));
152
+ console.log(chalk_1.default.yellow('⚠️ But no global FRAIM configuration found.'));
153
+ console.log(chalk_1.default.cyan('💡 Your MCP setup looks good! Skipping key setup.\n'));
154
+ }
155
+ else {
156
+ // No existing configuration found - ask for FRAIM key
157
+ console.log(chalk_1.default.yellow('🔍 No existing FRAIM configuration detected.\n'));
158
+ let response;
159
+ try {
160
+ response = await (0, prompts_1.default)({
161
+ type: 'text',
162
+ name: 'fraimKey',
163
+ message: 'Do you have a FRAIM key? (Input key or press Enter to skip)',
164
+ validate: (value) => true // Optional input
165
+ });
166
+ }
167
+ catch (e) {
168
+ console.warn(chalk_1.default.yellow('\n⚠️ Interactive prompts experienced an issue. Skipping setup.\n'));
169
+ return;
170
+ }
171
+ if (response.fraimKey && response.fraimKey.trim().length > 0) {
172
+ const key = response.fraimKey.trim();
173
+ console.log(chalk_1.default.green('\n✅ Key received.'));
174
+ console.log(chalk_1.default.yellow('\nPlease add the following to your IDE\'s MCP config (e.g. claude_desktop_config.json):'));
175
+ const mcpConfig = {
176
+ mcpServers: {
177
+ fraim: {
178
+ serverUrl: "https://fraim.wellnessatwork.me",
179
+ headers: {
180
+ "x-api-key": key
181
+ }
182
+ }
183
+ }
184
+ };
185
+ console.log(chalk_1.default.cyan(JSON.stringify(mcpConfig, null, 2)));
186
+ console.log('\n');
187
+ }
188
+ else {
189
+ console.log(chalk_1.default.yellow('\nℹ️ If you need a key, please email sid.mathur@gmail.com to request one.\n'));
190
+ }
191
+ }
192
+ // 2. Ask about Architecture Document
193
+ const archResponse = await (0, prompts_1.default)({
194
+ type: 'confirm',
195
+ name: 'hasArchDoc',
196
+ message: 'Do you have an architecture document for this project?',
197
+ initial: false
198
+ });
199
+ if (!archResponse.hasArchDoc) {
200
+ console.log(chalk_1.default.yellow('\n💡 To create an architecture document, please ask your IDE Agent:'));
201
+ console.log(chalk_1.default.cyan(' "Run the bootstrap/create-architecture workflow"'));
202
+ console.log(chalk_1.default.gray(' (This ensures your agent understands your codebase structure)\n'));
203
+ }
204
+ else {
205
+ const pathResponse = await (0, prompts_1.default)({
206
+ type: 'text',
207
+ name: 'archDocPath',
208
+ message: 'Please provide the relative path to your architecture document:',
209
+ initial: 'docs/architecture.md',
210
+ validate: (value) => value.length > 0 ? true : 'Path cannot be empty'
211
+ });
212
+ if (pathResponse.archDocPath) {
213
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
214
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
215
+ const resolvedPath = path.resolve(process.cwd(), pathResponse.archDocPath);
216
+ if (fs.existsSync(resolvedPath)) {
217
+ try {
218
+ const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(process.cwd());
219
+ if (fs.existsSync(configPath)) {
220
+ const configContent = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
221
+ if (!configContent.customizations)
222
+ configContent.customizations = {};
223
+ configContent.customizations.architectureDoc = pathResponse.archDocPath;
224
+ fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
225
+ console.log(chalk_1.default.green(`\n✅ Linked architecture document: ${pathResponse.archDocPath}`));
226
+ }
227
+ else {
228
+ console.log(chalk_1.default.red(`\n❌ ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')} not found. Could not save preference.`));
229
+ }
230
+ }
231
+ catch (e) {
232
+ console.error(chalk_1.default.red('\n❌ Failed to update config:'), e);
233
+ }
234
+ }
235
+ else {
236
+ console.log(chalk_1.default.yellow(`\n⚠️ File not found at ${pathResponse.archDocPath}. We'll skip linking it for now.`));
237
+ }
238
+ }
239
+ }
240
+ console.log(chalk_1.default.green(`\n✅ Jobs are installed in ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('ai-employee/jobs')} and ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('ai-manager/jobs')}.\n`));
241
+ };
242
+ exports.runFirstRunExperience = runFirstRunExperience;
@@ -0,0 +1,98 @@
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.packRelativePathFor = packRelativePathFor;
7
+ exports.publishOrgArtifacts = publishOrgArtifacts;
8
+ /**
9
+ * Publish org context/rules/learnings to the configured org backend (issue #563).
10
+ *
11
+ * This is the first-class write mechanism the organization-onboarding and
12
+ * organizational-learning-synthesis jobs use instead of hand-rolling an API
13
+ * call: the read side is the synced `~/.fraim/org/` cache, the write side is
14
+ * here. Supports both backends:
15
+ * - fraim-cloud: POST /api/org/publish (authenticated by the user's API key)
16
+ * - git: shallow-clone the org repo, write the files on a branch, push it,
17
+ * and return the PR compare URL for the team to merge.
18
+ */
19
+ const axios_1 = __importDefault(require("axios"));
20
+ const child_process_1 = require("child_process");
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const os_1 = __importDefault(require("os"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const user_config_1 = require("./user-config");
25
+ const PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
26
+ const ALLOWED_GIT_URL = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[\w.-]+@[\w.-]+:)/;
27
+ /**
28
+ * Infer the org-pack location for a local file by its name:
29
+ * org_context.md -> context/org_context.md
30
+ * org_rules.md -> rules/org_rules.md
31
+ * org-*.md -> learnings/org-*.md
32
+ */
33
+ function packRelativePathFor(filePath) {
34
+ const base = path_1.default.basename(filePath);
35
+ if (base === 'org_context.md')
36
+ return 'context/org_context.md';
37
+ if (base === 'org_rules.md')
38
+ return 'rules/org_rules.md';
39
+ if (/^org-[\w.-]*\.md$/.test(base))
40
+ return `learnings/${base}`;
41
+ throw new Error(`Cannot infer org-pack location for '${base}' (expected org_context.md, org_rules.md, or org-*.md)`);
42
+ }
43
+ function gitCompareUrl(gitUrl, branch) {
44
+ const httpUrl = gitUrl.replace(/\.git$/, '').replace(/^git@([^:]+):/, 'https://$1/');
45
+ if (/^https?:\/\//.test(httpUrl))
46
+ return `${httpUrl}/compare/${branch}?expand=1`;
47
+ return undefined;
48
+ }
49
+ /**
50
+ * Publish a set of org artifacts to the configured backend. Throws with a clear
51
+ * message when no backend is configured, a path is outside the pack layout, or
52
+ * the backend operation fails.
53
+ */
54
+ async function publishOrgArtifacts(artifacts, opts) {
55
+ const organization = (0, user_config_1.getOrganizationConfig)();
56
+ if (!organization) {
57
+ throw new Error('No organization backend configured. Run organization-onboarding or set the organization block in ~/.fraim/config.json.');
58
+ }
59
+ if (artifacts.length === 0)
60
+ throw new Error('No artifacts to publish.');
61
+ for (const a of artifacts) {
62
+ if (!PACK_RELATIVE_PATH.test(a.relativePath)) {
63
+ throw new Error(`Invalid org-pack path '${a.relativePath}' (must be context|rules|learnings/<name>.md).`);
64
+ }
65
+ }
66
+ if (organization.backend === 'fraim-cloud') {
67
+ const remoteUrl = opts?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
68
+ const apiKey = opts?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
69
+ if (!apiKey)
70
+ throw new Error('No FRAIM API key available for the fraim-cloud org backend.');
71
+ const res = await axios_1.default.post(`${remoteUrl.replace(/\/$/, '')}/api/org/publish`, { artifacts }, { headers: { 'x-api-key': apiKey }, timeout: 30_000 });
72
+ return { backend: 'fraim-cloud', version: String(res.data?.version ?? '') };
73
+ }
74
+ // git backend
75
+ const gitUrl = organization.gitUrl;
76
+ if (!ALLOWED_GIT_URL.test(gitUrl))
77
+ throw new Error(`Org repo URL has an unsupported scheme: ${gitUrl}`);
78
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-org-pub-'));
79
+ try {
80
+ (0, child_process_1.execFileSync)('git', ['clone', '--depth=1', '--quiet', '--', gitUrl, '.'], {
81
+ cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000
82
+ });
83
+ const branch = `fraim-org-update-${opts?.branchSuffix || `${process.pid}-${Date.now()}`}`;
84
+ (0, child_process_1.execFileSync)('git', ['checkout', '-b', branch], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
85
+ for (const a of artifacts) {
86
+ const dest = path_1.default.join(dir, a.relativePath);
87
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
88
+ fs_1.default.writeFileSync(dest, a.content);
89
+ }
90
+ (0, child_process_1.execFileSync)('git', ['add', '-A'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
91
+ (0, child_process_1.execFileSync)('git', ['commit', '-m', 'Update organization context'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
92
+ (0, child_process_1.execFileSync)('git', ['push', '--quiet', 'origin', branch], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 });
93
+ return { backend: 'git', branch, prUrl: gitCompareUrl(gitUrl, branch) };
94
+ }
95
+ finally {
96
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
97
+ }
98
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readFraimConfigRaw = readFraimConfigRaw;
4
+ exports.mergeFraimConfig = mergeFraimConfig;
5
+ exports.writeFraimConfigUpdate = writeFraimConfigUpdate;
6
+ exports.writeFraimConfig = writeFraimConfig;
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const config_loader_1 = require("./config-loader");
10
+ const types_1 = require("./types");
11
+ function isPlainObject(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function deepMerge(baseValue, updateValue) {
15
+ if (updateValue === undefined) {
16
+ return baseValue;
17
+ }
18
+ if (Array.isArray(updateValue)) {
19
+ return [...updateValue];
20
+ }
21
+ if (!isPlainObject(updateValue)) {
22
+ return updateValue;
23
+ }
24
+ const baseObject = isPlainObject(baseValue) ? baseValue : {};
25
+ const merged = { ...baseObject };
26
+ for (const [key, value] of Object.entries(updateValue)) {
27
+ merged[key] = deepMerge(baseObject[key], value);
28
+ }
29
+ return merged;
30
+ }
31
+ function ensureWritableFraimConfigShape(rawConfig) {
32
+ const config = { ...rawConfig };
33
+ if (typeof config.version !== 'string' || config.version.trim().length === 0) {
34
+ config.version = types_1.DEFAULT_FRAIM_CONFIG.version;
35
+ }
36
+ const projectConfig = isPlainObject(config.project) ? config.project : {};
37
+ config.project = {
38
+ ...types_1.DEFAULT_FRAIM_CONFIG.project,
39
+ ...projectConfig
40
+ };
41
+ const customizationsConfig = isPlainObject(config.customizations) ? config.customizations : {};
42
+ config.customizations = {
43
+ ...types_1.DEFAULT_FRAIM_CONFIG.customizations,
44
+ ...customizationsConfig
45
+ };
46
+ return config;
47
+ }
48
+ function readFraimConfigRaw(configPath) {
49
+ if (!(0, fs_1.existsSync)(configPath)) {
50
+ return {};
51
+ }
52
+ const parsed = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
53
+ if (!isPlainObject(parsed)) {
54
+ throw new Error('FRAIM config must contain a JSON object at the top level.');
55
+ }
56
+ return parsed;
57
+ }
58
+ function mergeFraimConfig(existingRawConfig, update) {
59
+ const rawConfig = ensureWritableFraimConfigShape(deepMerge(existingRawConfig, update));
60
+ return {
61
+ config: (0, config_loader_1.normalizeFraimConfig)(rawConfig),
62
+ created: Object.keys(existingRawConfig).length === 0,
63
+ rawConfig
64
+ };
65
+ }
66
+ function writeFraimConfigUpdate(configPath, update) {
67
+ const existingRawConfig = readFraimConfigRaw(configPath);
68
+ const result = mergeFraimConfig(existingRawConfig, update);
69
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(configPath), { recursive: true });
70
+ (0, fs_1.writeFileSync)(configPath, JSON.stringify(result.rawConfig, null, 2));
71
+ return result;
72
+ }
73
+ function writeFraimConfig(configPath, config) {
74
+ return writeFraimConfigUpdate(configPath, config);
75
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEPRECATED_FRAIM_JOB_NAMES = void 0;
4
+ exports.resolveFraimJobName = resolveFraimJobName;
5
+ exports.isListableFraimJob = isListableFraimJob;
6
+ const DEPRECATED_TO_CANONICAL_JOB_MAP = {
7
+ 'learn-and-scale': 'upskill-employee',
8
+ 'model-behavior': 'upskill-employee',
9
+ 'promote-learning': 'upskill-employee',
10
+ 'refine-jobs': 'upskill-employee',
11
+ 'refine-skills': 'upskill-employee'
12
+ };
13
+ const DIRECT_JOB_ALIASES = {
14
+ 'sleep on learnings': 'sleep-on-learnings'
15
+ };
16
+ exports.DEPRECATED_FRAIM_JOB_NAMES = new Set(Object.keys(DEPRECATED_TO_CANONICAL_JOB_MAP));
17
+ function normalizeJobLookupInput(input) {
18
+ return input.trim().toLowerCase().replace(/[_\s]+/g, '-');
19
+ }
20
+ function resolveFraimJobName(input) {
21
+ const normalizedJobName = normalizeJobLookupInput(input);
22
+ const directAliasTarget = DIRECT_JOB_ALIASES[input.trim().toLowerCase()];
23
+ if (directAliasTarget) {
24
+ return {
25
+ requestedJobName: input,
26
+ normalizedJobName,
27
+ canonicalJobName: directAliasTarget
28
+ };
29
+ }
30
+ const deprecatedAliasTarget = DEPRECATED_TO_CANONICAL_JOB_MAP[normalizedJobName];
31
+ if (deprecatedAliasTarget) {
32
+ return {
33
+ requestedJobName: input,
34
+ normalizedJobName,
35
+ canonicalJobName: deprecatedAliasTarget,
36
+ deprecatedAliasTarget
37
+ };
38
+ }
39
+ return {
40
+ requestedJobName: input,
41
+ normalizedJobName,
42
+ canonicalJobName: normalizedJobName
43
+ };
44
+ }
45
+ function isListableFraimJob(jobName) {
46
+ return !exports.DEPRECATED_FRAIM_JOB_NAMES.has(normalizeJobLookupInput(jobName));
47
+ }
@@ -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;
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",
3
- "version": "2.0.164",
3
+ "version": "2.0.166",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {