fraim-framework 2.0.70 → 2.0.72

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.
@@ -8,6 +8,7 @@ const commander_1 = require("commander");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
+ const inheritance_parser_1 = require("../../core/utils/inheritance-parser");
11
12
  exports.doctorCommand = new commander_1.Command('doctor')
12
13
  .description('Validate FRAIM installation and configuration')
13
14
  .action(async () => {
@@ -71,4 +72,58 @@ exports.doctorCommand = new commander_1.Command('doctor')
71
72
  else {
72
73
  console.log(chalk_1.default.red(`\n🩹 Found ${issuesFound} issues. See recommendations above.`));
73
74
  }
75
+ // 5. Check for overrides
76
+ const overridesDir = path_1.default.join(fraimDir, 'overrides');
77
+ if (fs_1.default.existsSync(overridesDir)) {
78
+ console.log(chalk_1.default.blue('\nšŸ“ Override Diagnostics:\n'));
79
+ const overrides = [];
80
+ const scanDir = (dir, base = '') => {
81
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ const relativePath = path_1.default.join(base, entry.name);
84
+ if (entry.isDirectory()) {
85
+ scanDir(path_1.default.join(dir, entry.name), relativePath);
86
+ }
87
+ else {
88
+ overrides.push(relativePath.replace(/\\/g, '/'));
89
+ }
90
+ }
91
+ };
92
+ scanDir(overridesDir);
93
+ if (overrides.length === 0) {
94
+ console.log(chalk_1.default.gray(' No active overrides found.'));
95
+ }
96
+ else {
97
+ console.log(chalk_1.default.green(` Found ${overrides.length} active override(s):\n`));
98
+ const parser = new inheritance_parser_1.InheritanceParser();
99
+ for (const override of overrides) {
100
+ const overridePath = path_1.default.join(overridesDir, override);
101
+ const content = fs_1.default.readFileSync(overridePath, 'utf-8');
102
+ const parseResult = parser.parse(content);
103
+ if (parseResult.hasImports) {
104
+ console.log(chalk_1.default.white(` šŸ“„ ${override}`));
105
+ console.log(chalk_1.default.gray(` Inherits from: ${parseResult.imports.join(', ')}`));
106
+ // Validate import syntax
107
+ let hasErrors = false;
108
+ for (const importPath of parseResult.imports) {
109
+ try {
110
+ parser.sanitizePath(importPath);
111
+ }
112
+ catch (error) {
113
+ console.log(chalk_1.default.red(` āš ļø Invalid import: ${error.message}`));
114
+ hasErrors = true;
115
+ }
116
+ }
117
+ if (!hasErrors) {
118
+ console.log(chalk_1.default.green(` āœ… Syntax valid`));
119
+ }
120
+ }
121
+ else {
122
+ console.log(chalk_1.default.white(` šŸ“„ ${override}`));
123
+ console.log(chalk_1.default.gray(` Full override (no inheritance)`));
124
+ }
125
+ console.log('');
126
+ }
127
+ }
128
+ }
74
129
  });
@@ -0,0 +1,97 @@
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.listOverridableCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ exports.listOverridableCommand = new commander_1.Command('list-overridable')
12
+ .description('List all FRAIM registry paths that can be overridden')
13
+ .action(async () => {
14
+ const projectRoot = process.cwd();
15
+ const fraimDir = path_1.default.join(projectRoot, '.fraim');
16
+ const configPath = path_1.default.join(fraimDir, 'config.json');
17
+ const overridesDir = path_1.default.join(fraimDir, 'overrides');
18
+ // Validate .fraim directory exists
19
+ if (!fs_1.default.existsSync(fraimDir)) {
20
+ console.log(chalk_1.default.red('āŒ .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
21
+ process.exit(1);
22
+ }
23
+ console.log(chalk_1.default.blue('šŸ“‹ Overridable FRAIM Registry Paths:\n'));
24
+ // Define overridable categories
25
+ const categories = [
26
+ {
27
+ name: 'Workflows',
28
+ paths: [
29
+ 'workflows/product-building/spec.md',
30
+ 'workflows/product-building/design.md',
31
+ 'workflows/product-building/implement.md',
32
+ 'workflows/product-building/test.md',
33
+ 'workflows/product-building/resolve.md',
34
+ 'workflows/product-building/prep-issue.md'
35
+ ]
36
+ },
37
+ {
38
+ name: 'Templates',
39
+ paths: [
40
+ 'templates/specs/FEATURESPEC-TEMPLATE.md',
41
+ 'templates/design/DESIGN-TEMPLATE.md'
42
+ ]
43
+ },
44
+ {
45
+ name: 'Rules',
46
+ paths: [
47
+ 'rules/coding-standards.md',
48
+ 'rules/communication.md'
49
+ ]
50
+ }
51
+ ];
52
+ // Get list of existing overrides
53
+ const existingOverrides = new Set();
54
+ if (fs_1.default.existsSync(overridesDir)) {
55
+ const scanDir = (dir, base = '') => {
56
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ const relativePath = path_1.default.join(base, entry.name);
59
+ if (entry.isDirectory()) {
60
+ scanDir(path_1.default.join(dir, entry.name), relativePath);
61
+ }
62
+ else {
63
+ existingOverrides.add(relativePath.replace(/\\/g, '/'));
64
+ }
65
+ }
66
+ };
67
+ scanDir(overridesDir);
68
+ }
69
+ // Display categories
70
+ for (const category of categories) {
71
+ console.log(chalk_1.default.bold.cyan(`${category.name}:`));
72
+ for (const filePath of category.paths) {
73
+ const hasOverride = existingOverrides.has(filePath);
74
+ const status = hasOverride
75
+ ? chalk_1.default.green('[OVERRIDDEN]')
76
+ : chalk_1.default.gray('[OVERRIDABLE]');
77
+ console.log(` ${status} ${filePath}`);
78
+ }
79
+ console.log('');
80
+ }
81
+ // Show existing overrides not in the standard list
82
+ const standardPaths = new Set(categories.flatMap(c => c.paths));
83
+ const customOverrides = Array.from(existingOverrides)
84
+ .filter(p => !standardPaths.has(p));
85
+ if (customOverrides.length > 0) {
86
+ console.log(chalk_1.default.bold.cyan('Custom Overrides:'));
87
+ for (const filePath of customOverrides) {
88
+ console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${filePath}`);
89
+ }
90
+ console.log('');
91
+ }
92
+ // Show tips
93
+ console.log(chalk_1.default.gray('Tips:'));
94
+ console.log(chalk_1.default.gray(' • Use "fraim override <path> --inherit" to inherit from global'));
95
+ console.log(chalk_1.default.gray(' • Use "fraim override <path> --copy" to copy current content'));
96
+ console.log(chalk_1.default.gray(' • Overrides are stored in .fraim/overrides/'));
97
+ });
@@ -0,0 +1,115 @@
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.overrideCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const axios_1 = __importDefault(require("axios"));
12
+ exports.overrideCommand = new commander_1.Command('override')
13
+ .description('Create a local override for a FRAIM registry file')
14
+ .argument('<path>', 'Registry path to override (e.g., workflows/product-building/spec.md)')
15
+ .option('--inherit', 'Create override with {{ import }} directive (inherits from global)')
16
+ .option('--copy', 'Copy current content from remote to local')
17
+ .action(async (registryPath, options) => {
18
+ const projectRoot = process.cwd();
19
+ const fraimDir = path_1.default.join(projectRoot, '.fraim');
20
+ const configPath = path_1.default.join(fraimDir, 'config.json');
21
+ // Validate .fraim directory exists
22
+ if (!fs_1.default.existsSync(fraimDir)) {
23
+ console.log(chalk_1.default.red('āŒ .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
24
+ process.exit(1);
25
+ }
26
+ // Validate path format
27
+ if (registryPath.includes('..') || path_1.default.isAbsolute(registryPath)) {
28
+ console.log(chalk_1.default.red('āŒ Invalid path. Path must be relative and cannot contain ".."'));
29
+ process.exit(1);
30
+ }
31
+ // Ensure exactly one option is specified
32
+ if (options.inherit && options.copy) {
33
+ console.log(chalk_1.default.red('āŒ Cannot use both --inherit and --copy. Choose one.'));
34
+ process.exit(1);
35
+ }
36
+ if (!options.inherit && !options.copy) {
37
+ console.log(chalk_1.default.red('āŒ Must specify either --inherit or --copy.'));
38
+ process.exit(1);
39
+ }
40
+ // Create overrides directory structure
41
+ const overridePath = path_1.default.join(fraimDir, 'overrides', registryPath);
42
+ const overrideDir = path_1.default.dirname(overridePath);
43
+ if (!fs_1.default.existsSync(overrideDir)) {
44
+ fs_1.default.mkdirSync(overrideDir, { recursive: true });
45
+ }
46
+ // Check if override already exists
47
+ if (fs_1.default.existsSync(overridePath)) {
48
+ console.log(chalk_1.default.yellow(`āš ļø Override already exists: ${registryPath}`));
49
+ console.log(chalk_1.default.gray(` Location: ${overridePath}`));
50
+ process.exit(0);
51
+ }
52
+ let content;
53
+ if (options.inherit) {
54
+ // Create file with import directive
55
+ content = `{{ import: ${registryPath} }}\n\n<!-- Add your custom content below -->\n`;
56
+ console.log(chalk_1.default.blue(`šŸ“ Creating inherited override for: ${registryPath}`));
57
+ }
58
+ else {
59
+ // Fetch current content from remote
60
+ console.log(chalk_1.default.blue(`šŸ“„ Fetching current content from remote: ${registryPath}`));
61
+ try {
62
+ // Read config to get remote URL
63
+ let config = {};
64
+ if (fs_1.default.existsSync(configPath)) {
65
+ config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
66
+ }
67
+ const remoteUrl = config.remoteUrl || 'https://fraim-registry.azurewebsites.net';
68
+ const apiKey = config.apiKey;
69
+ // Determine endpoint based on path
70
+ let endpoint;
71
+ if (registryPath.startsWith('workflows/')) {
72
+ const workflowName = registryPath.replace('workflows/', '').replace('.md', '');
73
+ endpoint = `${remoteUrl}/api/workflows/${workflowName}`;
74
+ }
75
+ else {
76
+ endpoint = `${remoteUrl}/api/files/${registryPath}`;
77
+ }
78
+ const headers = {};
79
+ if (apiKey) {
80
+ headers['x-api-key'] = apiKey;
81
+ }
82
+ const response = await axios_1.default.get(endpoint, {
83
+ headers,
84
+ timeout: 30000
85
+ });
86
+ // Extract content from response
87
+ if (response.data.content) {
88
+ content = response.data.content;
89
+ }
90
+ else if (typeof response.data === 'string') {
91
+ content = response.data;
92
+ }
93
+ else {
94
+ throw new Error('Unexpected response format');
95
+ }
96
+ console.log(chalk_1.default.green(`āœ… Fetched ${content.length} bytes from remote`));
97
+ }
98
+ catch (error) {
99
+ console.log(chalk_1.default.red(`āŒ Failed to fetch content from remote: ${error.message}`));
100
+ console.log(chalk_1.default.gray(' Tip: Check your network connection and .fraim/config.json settings'));
101
+ process.exit(1);
102
+ }
103
+ }
104
+ // Write override file
105
+ try {
106
+ fs_1.default.writeFileSync(overridePath, content, 'utf-8');
107
+ console.log(chalk_1.default.green(`āœ… Created override: ${registryPath}`));
108
+ console.log(chalk_1.default.gray(` Location: ${overridePath}`));
109
+ console.log(chalk_1.default.gray(` Edit this file to customize for your project`));
110
+ }
111
+ catch (error) {
112
+ console.log(chalk_1.default.red(`āŒ Failed to write override file: ${error.message}`));
113
+ process.exit(1);
114
+ }
115
+ });
@@ -12,6 +12,8 @@ const setup_1 = require("./commands/setup");
12
12
  const init_project_1 = require("./commands/init-project");
13
13
  const test_mcp_1 = require("./commands/test-mcp");
14
14
  const add_ide_1 = require("./commands/add-ide");
15
+ const override_1 = require("./commands/override");
16
+ const list_overridable_1 = require("./commands/list-overridable");
15
17
  const fs_1 = __importDefault(require("fs"));
16
18
  const path_1 = __importDefault(require("path"));
17
19
  const program = new commander_1.Command();
@@ -46,4 +48,6 @@ program.addCommand(setup_1.setupCommand);
46
48
  program.addCommand(init_project_1.initProjectCommand);
47
49
  program.addCommand(test_mcp_1.testMCPCommand);
48
50
  program.addCommand(add_ide_1.addIDECommand);
51
+ program.addCommand(override_1.overrideCommand);
52
+ program.addCommand(list_overridable_1.listOverridableCommand);
49
53
  program.parse(process.argv);
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ /**
3
+ * InheritanceParser
4
+ *
5
+ * Parses and resolves {{ import: path }} directives in registry files,
6
+ * enabling local overrides to inherit from global registry files.
7
+ *
8
+ * Security features:
9
+ * - Path traversal protection (rejects .. and absolute paths)
10
+ * - Circular import detection
11
+ * - Max depth limit (5 levels)
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.InheritanceParser = exports.InheritanceError = void 0;
15
+ class InheritanceError extends Error {
16
+ constructor(message, path) {
17
+ super(message);
18
+ this.path = path;
19
+ this.name = 'InheritanceError';
20
+ }
21
+ }
22
+ exports.InheritanceError = InheritanceError;
23
+ /**
24
+ * Regular expression to match {{ import: path }} directives
25
+ */
26
+ const IMPORT_REGEX = /\{\{\s*import:\s*([^\}]+)\s*\}\}/g;
27
+ class InheritanceParser {
28
+ constructor(maxDepth = 5) {
29
+ this.maxDepth = maxDepth;
30
+ }
31
+ /**
32
+ * Extract import directives from content without resolving them
33
+ */
34
+ extractImports(content) {
35
+ const imports = [];
36
+ let match;
37
+ // Reset regex state
38
+ IMPORT_REGEX.lastIndex = 0;
39
+ while ((match = IMPORT_REGEX.exec(content)) !== null) {
40
+ imports.push(match[1].trim());
41
+ }
42
+ return imports;
43
+ }
44
+ /**
45
+ * Sanitize and validate import path
46
+ *
47
+ * @throws {InheritanceError} If path is invalid
48
+ */
49
+ sanitizePath(path) {
50
+ const trimmed = path.trim();
51
+ // Reject empty paths
52
+ if (!trimmed) {
53
+ throw new InheritanceError('Import path cannot be empty');
54
+ }
55
+ // Reject absolute paths (Unix and Windows)
56
+ if (trimmed.startsWith('/') || trimmed.match(/^[A-Za-z]:\\/)) {
57
+ throw new InheritanceError(`Absolute paths not allowed: ${trimmed}`, trimmed);
58
+ }
59
+ // Reject path traversal attempts
60
+ if (trimmed.includes('..')) {
61
+ throw new InheritanceError(`Path traversal not allowed: ${trimmed}`, trimmed);
62
+ }
63
+ return trimmed;
64
+ }
65
+ /**
66
+ * Detect circular imports
67
+ *
68
+ * Special case: If the import path is the same as the current path,
69
+ * it's not a circular import - it's importing the parent/remote version.
70
+ * This allows local overrides to inherit from their remote counterparts.
71
+ *
72
+ * @throws {InheritanceError} If circular import detected
73
+ */
74
+ detectCircularImport(path, visited, isParentImport = false) {
75
+ // If this is a parent import (same path as current), allow it
76
+ if (isParentImport) {
77
+ return;
78
+ }
79
+ if (visited.has(path)) {
80
+ throw new InheritanceError(`Circular import detected: ${path}`, path);
81
+ }
82
+ }
83
+ /**
84
+ * Resolve all import directives in content recursively
85
+ *
86
+ * @param content - Content with {{ import }} directives
87
+ * @param currentPath - Path of current file (for circular detection)
88
+ * @param options - Resolution options
89
+ * @returns Resolved content with all imports replaced
90
+ *
91
+ * @throws {InheritanceError} If circular import, path traversal, or max depth exceeded
92
+ */
93
+ async resolve(content, currentPath, options) {
94
+ const depth = options.currentDepth || 0;
95
+ const visited = options.visited || new Set();
96
+ const maxDepth = options.maxDepth || this.maxDepth;
97
+ // Check depth limit
98
+ if (depth > maxDepth) {
99
+ throw new InheritanceError(`Max import depth exceeded (${maxDepth})`, currentPath);
100
+ }
101
+ // Check circular imports (but allow importing the same path as parent)
102
+ this.detectCircularImport(currentPath, visited, false);
103
+ visited.add(currentPath);
104
+ // Extract imports
105
+ const imports = this.extractImports(content);
106
+ if (imports.length === 0) {
107
+ return content;
108
+ }
109
+ // Resolve each import
110
+ let resolved = content;
111
+ for (const importPath of imports) {
112
+ // Sanitize path
113
+ const sanitized = this.sanitizePath(importPath);
114
+ // Check if this is a parent import (same path as current)
115
+ const isParentImport = sanitized === currentPath;
116
+ // Fetch parent content
117
+ let parentContent;
118
+ try {
119
+ parentContent = await options.fetchParent(sanitized);
120
+ }
121
+ catch (error) {
122
+ throw new InheritanceError(`Failed to fetch parent content: ${sanitized}. ${error.message}`, sanitized);
123
+ }
124
+ // Recursively resolve parent imports
125
+ // For parent imports, use a fresh visited set to allow the same path
126
+ const parentVisited = isParentImport ? new Set() : new Set(visited);
127
+ const resolvedParent = await this.resolve(parentContent, sanitized, {
128
+ ...options,
129
+ currentDepth: depth + 1,
130
+ visited: parentVisited
131
+ });
132
+ // Replace import directive with resolved parent content
133
+ const importDirective = `{{ import: ${importPath} }}`;
134
+ resolved = resolved.replace(importDirective, resolvedParent);
135
+ }
136
+ return resolved;
137
+ }
138
+ /**
139
+ * Parse content and return detailed information about imports
140
+ */
141
+ parse(content) {
142
+ const imports = this.extractImports(content);
143
+ return {
144
+ content,
145
+ imports,
146
+ hasImports: imports.length > 0
147
+ };
148
+ }
149
+ }
150
+ exports.InheritanceParser = InheritanceParser;
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * LocalRegistryResolver
4
+ *
5
+ * Resolves registry file requests by checking for local overrides first,
6
+ * then falling back to remote registry. Handles inheritance via InheritanceParser.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.LocalRegistryResolver = void 0;
10
+ const fs_1 = require("fs");
11
+ const path_1 = require("path");
12
+ const inheritance_parser_1 = require("./inheritance-parser");
13
+ class LocalRegistryResolver {
14
+ constructor(options) {
15
+ this.workspaceRoot = options.workspaceRoot;
16
+ this.remoteContentResolver = options.remoteContentResolver;
17
+ this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
18
+ }
19
+ /**
20
+ * Check if a local override exists for the given path
21
+ */
22
+ hasLocalOverride(path) {
23
+ const overridePath = this.getOverridePath(path);
24
+ const exists = (0, fs_1.existsSync)(overridePath);
25
+ console.error(`[LocalRegistryResolver] hasLocalOverride(${path}) -> ${overridePath} -> ${exists}`);
26
+ return exists;
27
+ }
28
+ /**
29
+ * Get the full path to a local override file
30
+ */
31
+ getOverridePath(path) {
32
+ return (0, path_1.join)(this.workspaceRoot, '.fraim/overrides', path);
33
+ }
34
+ /**
35
+ * Read local override file content
36
+ */
37
+ readLocalOverride(path) {
38
+ const overridePath = this.getOverridePath(path);
39
+ try {
40
+ return (0, fs_1.readFileSync)(overridePath, 'utf-8');
41
+ }
42
+ catch (error) {
43
+ throw new Error(`Failed to read local override: ${path}. ${error.message}`);
44
+ }
45
+ }
46
+ /**
47
+ * Resolve inheritance in local override content
48
+ */
49
+ async resolveInheritance(content, currentPath) {
50
+ // Check if content has imports
51
+ const parseResult = this.parser.parse(content);
52
+ if (!parseResult.hasImports) {
53
+ return { content, imports: [] };
54
+ }
55
+ // Resolve imports
56
+ try {
57
+ const resolved = await this.parser.resolve(content, currentPath, {
58
+ fetchParent: this.remoteContentResolver,
59
+ maxDepth: 5
60
+ });
61
+ return {
62
+ content: resolved,
63
+ imports: parseResult.imports
64
+ };
65
+ }
66
+ catch (error) {
67
+ if (error instanceof inheritance_parser_1.InheritanceError) {
68
+ throw error;
69
+ }
70
+ throw new Error(`Failed to resolve inheritance for ${currentPath}: ${error.message}`);
71
+ }
72
+ }
73
+ /**
74
+ * Generate metadata comment for resolved content
75
+ */
76
+ generateMetadata(result) {
77
+ if (result.source === 'remote') {
78
+ return '';
79
+ }
80
+ if (result.inherited && result.imports && result.imports.length > 0) {
81
+ return `<!-- šŸ“ Using local override (inherited from: ${result.imports.join(', ')}) -->\n\n`;
82
+ }
83
+ return `<!-- šŸ“ Using local override -->\n\n`;
84
+ }
85
+ /**
86
+ * Resolve a registry file request
87
+ *
88
+ * Resolution order:
89
+ * 1. Check for local override in .fraim/overrides/
90
+ * 2. If found, read and resolve inheritance
91
+ * 3. If not found, fetch from remote
92
+ *
93
+ * @param path - Registry path (e.g., "workflows/product-building/spec.md")
94
+ * @returns Resolved file with metadata
95
+ */
96
+ async resolveFile(path) {
97
+ console.error(`[LocalRegistryResolver] ===== resolveFile called for: ${path} =====`);
98
+ // Check for local override
99
+ if (!this.hasLocalOverride(path)) {
100
+ // No override, fetch from remote
101
+ try {
102
+ const content = await this.remoteContentResolver(path);
103
+ return {
104
+ content,
105
+ source: 'remote',
106
+ inherited: false
107
+ };
108
+ }
109
+ catch (error) {
110
+ throw new Error(`Failed to fetch from remote: ${path}. ${error.message}`);
111
+ }
112
+ }
113
+ // Read local override
114
+ let localContent;
115
+ try {
116
+ localContent = this.readLocalOverride(path);
117
+ }
118
+ catch (error) {
119
+ // If local read fails, fall back to remote
120
+ console.warn(`Local override read failed, falling back to remote: ${path}`);
121
+ const content = await this.remoteContentResolver(path);
122
+ return {
123
+ content,
124
+ source: 'remote',
125
+ inherited: false
126
+ };
127
+ }
128
+ // Resolve inheritance
129
+ let resolved;
130
+ try {
131
+ console.error(`[LocalRegistryResolver] Resolving inheritance for ${path}`);
132
+ console.error(`[LocalRegistryResolver] Local content length: ${localContent.length} chars`);
133
+ console.error(`[LocalRegistryResolver] Local content preview: ${localContent.substring(0, 200)}`);
134
+ resolved = await this.resolveInheritance(localContent, path);
135
+ console.error(`[LocalRegistryResolver] Inheritance resolved: ${resolved.imports.length} imports`);
136
+ console.error(`[LocalRegistryResolver] Resolved content length: ${resolved.content.length} chars`);
137
+ console.error(`[LocalRegistryResolver] Resolved content preview: ${resolved.content.substring(0, 200)}`);
138
+ }
139
+ catch (error) {
140
+ // If inheritance resolution fails, fall back to remote
141
+ console.error(`āŒ Inheritance resolution failed for ${path}:`, error);
142
+ const content = await this.remoteContentResolver(path);
143
+ return {
144
+ content,
145
+ source: 'remote',
146
+ inherited: false
147
+ };
148
+ }
149
+ // Build result
150
+ const result = {
151
+ content: resolved.content,
152
+ source: 'local',
153
+ inherited: resolved.imports.length > 0,
154
+ imports: resolved.imports.length > 0 ? resolved.imports : undefined
155
+ };
156
+ // Add metadata comment
157
+ const metadata = this.generateMetadata(result);
158
+ if (metadata) {
159
+ result.metadata = metadata;
160
+ result.content = metadata + result.content;
161
+ }
162
+ return result;
163
+ }
164
+ }
165
+ exports.LocalRegistryResolver = LocalRegistryResolver;
@@ -27,6 +27,7 @@ const crypto_1 = require("crypto");
27
27
  const axios_1 = __importDefault(require("axios"));
28
28
  const provider_utils_1 = require("../core/utils/provider-utils");
29
29
  const object_utils_1 = require("../core/utils/object-utils");
30
+ const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
30
31
  /**
31
32
  * Handle template substitution logic separately for better testability
32
33
  */
@@ -80,22 +81,35 @@ class FraimTemplateEngine {
80
81
  const filename = this.workingStyle === 'Conversation' ? 'delivery-conversation.json' : 'delivery-pr.json';
81
82
  try {
82
83
  let content = null;
83
- if (this.projectRoot) {
84
- const deliveryPath = (0, path_1.join)(this.projectRoot, 'registry', 'providers', filename);
85
- if ((0, fs_1.existsSync)(deliveryPath)) {
86
- content = (0, fs_1.readFileSync)(deliveryPath, 'utf-8');
87
- }
88
- }
84
+ // Try framework installation directory first (relative to this file)
85
+ // This file is in dist/src/local-mcp-server/, so go up to framework root
86
+ const frameworkRoot = (0, path_1.join)(__dirname, '..', '..', '..');
87
+ const frameworkPath = (0, path_1.join)(frameworkRoot, 'registry', 'providers', filename);
88
+ if ((0, fs_1.existsSync)(frameworkPath)) {
89
+ content = (0, fs_1.readFileSync)(frameworkPath, 'utf-8');
90
+ this.logFn(`āœ… Loaded delivery templates from framework: ${frameworkPath}`);
91
+ }
92
+ // Fallback: try node_modules if not found in framework root
89
93
  if (!content) {
90
94
  const nodeModulesPath = (0, path_1.join)(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry', 'providers', filename);
91
95
  if ((0, fs_1.existsSync)(nodeModulesPath)) {
92
96
  content = (0, fs_1.readFileSync)(nodeModulesPath, 'utf-8');
97
+ this.logFn(`āœ… Loaded delivery templates from node_modules: ${nodeModulesPath}`);
98
+ }
99
+ }
100
+ // Last resort: try project root (for custom overrides)
101
+ if (!content && this.projectRoot) {
102
+ const deliveryPath = (0, path_1.join)(this.projectRoot, 'registry', 'providers', filename);
103
+ if ((0, fs_1.existsSync)(deliveryPath)) {
104
+ content = (0, fs_1.readFileSync)(deliveryPath, 'utf-8');
105
+ this.logFn(`āœ… Loaded delivery templates from project: ${deliveryPath}`);
93
106
  }
94
107
  }
95
108
  if (content) {
96
109
  this.deliveryTemplatesCache = JSON.parse(content);
97
110
  return this.deliveryTemplatesCache;
98
111
  }
112
+ this.logFn(`āš ļø Could not find delivery templates: ${filename}`);
99
113
  return null;
100
114
  }
101
115
  catch (error) {
@@ -121,18 +135,26 @@ class FraimTemplateEngine {
121
135
  return this.providerTemplatesCache[provider];
122
136
  try {
123
137
  let content = null;
124
- if (this.projectRoot) {
125
- const providerPath = (0, path_1.join)(this.projectRoot, 'registry', 'providers', `${provider}.json`);
126
- if ((0, fs_1.existsSync)(providerPath)) {
127
- content = (0, fs_1.readFileSync)(providerPath, 'utf-8');
128
- }
138
+ // Try framework installation directory first (relative to this file)
139
+ const frameworkRoot = (0, path_1.join)(__dirname, '..', '..', '..');
140
+ const frameworkPath = (0, path_1.join)(frameworkRoot, 'registry', 'providers', `${provider}.json`);
141
+ if ((0, fs_1.existsSync)(frameworkPath)) {
142
+ content = (0, fs_1.readFileSync)(frameworkPath, 'utf-8');
129
143
  }
144
+ // Fallback: try node_modules
130
145
  if (!content) {
131
146
  const nodeModulesPath = (0, path_1.join)(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry', 'providers', `${provider}.json`);
132
147
  if ((0, fs_1.existsSync)(nodeModulesPath)) {
133
148
  content = (0, fs_1.readFileSync)(nodeModulesPath, 'utf-8');
134
149
  }
135
150
  }
151
+ // Last resort: try project root (for custom overrides)
152
+ if (!content && this.projectRoot) {
153
+ const providerPath = (0, path_1.join)(this.projectRoot, 'registry', 'providers', `${provider}.json`);
154
+ if ((0, fs_1.existsSync)(providerPath)) {
155
+ content = (0, fs_1.readFileSync)(providerPath, 'utf-8');
156
+ }
157
+ }
136
158
  if (content) {
137
159
  const templates = JSON.parse(content);
138
160
  this.providerTemplatesCache[provider] = templates;
@@ -178,6 +200,7 @@ class FraimLocalMCPServer {
178
200
  this.machineInfo = null;
179
201
  this.repoInfo = null;
180
202
  this.engine = null;
203
+ this.registryResolver = null;
181
204
  this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
182
205
  this.apiKey = process.env.FRAIM_API_KEY || '';
183
206
  this.localVersion = this.detectLocalVersion();
@@ -349,6 +372,10 @@ class FraimLocalMCPServer {
349
372
  if (this.repoInfo) {
350
373
  return this.repoInfo;
351
374
  }
375
+ // Ensure config is loaded before trying to detect repo info
376
+ if (!this.config) {
377
+ this.loadConfig();
378
+ }
352
379
  try {
353
380
  const projectDir = this.findProjectRoot() || process.cwd();
354
381
  // Try to get git remote URL
@@ -464,6 +491,112 @@ class FraimLocalMCPServer {
464
491
  }
465
492
  return this.engine.substituteTemplates(content);
466
493
  }
494
+ /**
495
+ * Initialize the LocalRegistryResolver for override resolution
496
+ */
497
+ getRegistryResolver() {
498
+ if (!this.registryResolver) {
499
+ const projectRoot = this.findProjectRoot();
500
+ this.log(`šŸ” getRegistryResolver: projectRoot = ${projectRoot}`);
501
+ if (!projectRoot) {
502
+ this.log('āš ļø No project root found, override resolution disabled');
503
+ // Return a resolver that always falls back to remote
504
+ this.registryResolver = new local_registry_resolver_1.LocalRegistryResolver({
505
+ workspaceRoot: process.cwd(),
506
+ remoteContentResolver: async (path) => {
507
+ throw new Error('No project root available');
508
+ }
509
+ });
510
+ }
511
+ else {
512
+ this.registryResolver = new local_registry_resolver_1.LocalRegistryResolver({
513
+ workspaceRoot: projectRoot,
514
+ remoteContentResolver: async (path) => {
515
+ // Fetch parent content from remote for inheritance
516
+ this.log(`šŸ”„ Remote content resolver: fetching ${path}`);
517
+ let request;
518
+ if (path.startsWith('workflows/')) {
519
+ // Extract workflow name from path: workflows/category/name.md -> name
520
+ const pathParts = path.replace('workflows/', '').replace('.md', '').split('/');
521
+ const workflowName = pathParts[pathParts.length - 1]; // Get last part (name)
522
+ this.log(`šŸ”„ Fetching workflow: ${workflowName}`);
523
+ request = {
524
+ jsonrpc: '2.0',
525
+ id: (0, crypto_1.randomUUID)(),
526
+ method: 'tools/call',
527
+ params: {
528
+ name: 'get_fraim_workflow',
529
+ arguments: { workflow: workflowName }
530
+ }
531
+ };
532
+ }
533
+ else {
534
+ // For non-workflow files (templates, rules, etc.), use get_fraim_file
535
+ this.log(`šŸ”„ Fetching file: ${path}`);
536
+ request = {
537
+ jsonrpc: '2.0',
538
+ id: (0, crypto_1.randomUUID)(),
539
+ method: 'tools/call',
540
+ params: {
541
+ name: 'get_fraim_file',
542
+ arguments: { path }
543
+ }
544
+ };
545
+ }
546
+ const response = await this.proxyToRemote(request);
547
+ if (response.error) {
548
+ this.logError(`āŒ Remote content resolver failed: ${response.error.message}`);
549
+ throw new Error(`Failed to fetch parent: ${response.error.message}`);
550
+ }
551
+ // Extract content from MCP response format
552
+ if (response.result?.content && Array.isArray(response.result.content)) {
553
+ const textContent = response.result.content.find((c) => c.type === 'text');
554
+ if (textContent?.text) {
555
+ this.log(`āœ… Remote content resolver: fetched ${textContent.text.length} chars`);
556
+ return textContent.text;
557
+ }
558
+ }
559
+ this.logError(`āŒ Remote content resolver: no content in response for ${path}`);
560
+ this.logError(`Response: ${JSON.stringify(response, null, 2)}`);
561
+ throw new Error(`No content in remote response for ${path}`);
562
+ }
563
+ });
564
+ }
565
+ }
566
+ return this.registryResolver;
567
+ }
568
+ /**
569
+ * Determine workflow category from workflow name
570
+ */
571
+ getWorkflowCategory(workflowName) {
572
+ // Product development workflows
573
+ const productWorkflows = [
574
+ 'prep-issue', 'spec', 'design', 'implement', 'test', 'resolve',
575
+ 'prototype', 'refactor', 'iterate-on-pr-comments', 'retrospect'
576
+ ];
577
+ // Customer development workflows
578
+ const customerWorkflows = [
579
+ 'linkedin-outreach', 'customer-interview', 'insight-analysis',
580
+ 'insight-triage', 'interview-preparation', 'strategic-brainstorming',
581
+ 'thank-customers', 'user-survey-dispatch', 'users-to-target', 'weekly-newsletter'
582
+ ];
583
+ // Business development workflows
584
+ const businessWorkflows = [
585
+ 'partnership-outreach', 'investor-pitch', 'create-business-plan',
586
+ 'ideate-business-opportunity', 'price-product'
587
+ ];
588
+ if (productWorkflows.includes(workflowName)) {
589
+ return 'product-building';
590
+ }
591
+ else if (customerWorkflows.includes(workflowName)) {
592
+ return 'customer-development';
593
+ }
594
+ else if (businessWorkflows.includes(workflowName)) {
595
+ return 'business-development';
596
+ }
597
+ // Default to product-building for unknown workflows
598
+ return 'product-building';
599
+ }
467
600
  /**
468
601
  * Process template substitution in MCP response
469
602
  */
@@ -522,16 +655,20 @@ class FraimLocalMCPServer {
522
655
  this.log(`[req:${requestId}] Auto-detected and injected repo info: ${args.repo.owner}/${args.repo.name}`);
523
656
  }
524
657
  else {
525
- // If detection fails completely, return error instead of sending garbage
526
- this.logError(`[req:${requestId}] Could not detect repo info and no config available`);
527
- return {
528
- jsonrpc: '2.0',
529
- id: request.id,
530
- error: {
531
- code: -32603,
532
- message: 'Failed to detect repository information. Please ensure you are in a git repository or have .fraim/config.json configured with repository details.'
533
- }
534
- };
658
+ // If detection fails, use agent-provided values (if any)
659
+ if (!args.repo || !args.repo.url) {
660
+ // Only return error if agent didn't provide repo info either
661
+ this.logError(`[req:${requestId}] Could not detect repo info and no repo info provided by agent`);
662
+ return {
663
+ jsonrpc: '2.0',
664
+ id: request.id,
665
+ error: {
666
+ code: -32603,
667
+ message: 'Failed to detect repository information. Please ensure you are in a git repository or have .fraim/config.json configured with repository details, or provide repo info in fraim_connect arguments.'
668
+ }
669
+ };
670
+ }
671
+ this.log(`[req:${requestId}] Using agent-provided repo info: ${args.repo.owner}/${args.repo.name}`);
535
672
  }
536
673
  // Update the request with injected info
537
674
  request.params.arguments = args;
@@ -640,6 +777,94 @@ class FraimLocalMCPServer {
640
777
  this.log(`šŸ“¤ ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
641
778
  return processedResponse;
642
779
  }
780
+ // Intercept get_fraim_workflow and get_fraim_file for override resolution
781
+ if (request.method === 'tools/call' &&
782
+ (request.params?.name === 'get_fraim_workflow' ||
783
+ request.params?.name === 'get_fraim_file')) {
784
+ try {
785
+ const toolName = request.params.name;
786
+ const args = request.params.arguments || {};
787
+ // Extract the requested path
788
+ let requestedPath;
789
+ if (toolName === 'get_fraim_workflow') {
790
+ // Convert workflow name to path (e.g., "spec" -> "workflows/product-building/spec.md")
791
+ const workflowName = args.workflow;
792
+ if (!workflowName) {
793
+ this.log('āš ļø No workflow name provided in get_fraim_workflow');
794
+ }
795
+ else {
796
+ // Determine workflow category from name
797
+ const category = this.getWorkflowCategory(workflowName);
798
+ requestedPath = `workflows/${category}/${workflowName}.md`;
799
+ this.log(`šŸ” Checking for override: ${requestedPath}`);
800
+ const resolver = this.getRegistryResolver();
801
+ const hasOverride = resolver.hasLocalOverride(requestedPath);
802
+ this.log(`šŸ” hasLocalOverride(${requestedPath}) = ${hasOverride}`);
803
+ if (hasOverride) {
804
+ this.log(`āœ… Local override found: ${requestedPath}`);
805
+ const resolved = await resolver.resolveFile(requestedPath);
806
+ this.log(`šŸ“ Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
807
+ // Build MCP response with resolved content
808
+ const response = {
809
+ jsonrpc: '2.0',
810
+ id: request.id,
811
+ result: {
812
+ content: [
813
+ {
814
+ type: 'text',
815
+ text: resolved.content
816
+ }
817
+ ]
818
+ }
819
+ };
820
+ // Apply template substitution
821
+ const processedResponse = this.processResponse(response);
822
+ this.log(`šŸ“¤ ${request.method} → OK`);
823
+ return processedResponse;
824
+ }
825
+ }
826
+ }
827
+ else if (toolName === 'get_fraim_file') {
828
+ requestedPath = args.path;
829
+ if (!requestedPath) {
830
+ this.log('āš ļø No path provided in get_fraim_file');
831
+ }
832
+ else {
833
+ this.log(`šŸ” Checking for override: ${requestedPath}`);
834
+ const resolver = this.getRegistryResolver();
835
+ const hasOverride = resolver.hasLocalOverride(requestedPath);
836
+ this.log(`šŸ” hasLocalOverride(${requestedPath}) = ${hasOverride}`);
837
+ if (hasOverride) {
838
+ this.log(`āœ… Local override found: ${requestedPath}`);
839
+ const resolved = await resolver.resolveFile(requestedPath);
840
+ this.log(`šŸ“ Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
841
+ // Build MCP response with resolved content
842
+ const response = {
843
+ jsonrpc: '2.0',
844
+ id: request.id,
845
+ result: {
846
+ content: [
847
+ {
848
+ type: 'text',
849
+ text: resolved.content
850
+ }
851
+ ]
852
+ }
853
+ };
854
+ // Apply template substitution
855
+ const processedResponse = this.processResponse(response);
856
+ this.log(`šŸ“¤ ${request.method} → OK`);
857
+ return processedResponse;
858
+ }
859
+ }
860
+ }
861
+ }
862
+ catch (error) {
863
+ this.logError(`Override resolution failed: ${error.message}`);
864
+ this.log('āš ļø Falling back to remote');
865
+ // Fall through to proxy to remote
866
+ }
867
+ }
643
868
  // Proxy to remote server
644
869
  const response = await this.proxyToRemote(request);
645
870
  // Process template substitution (config vars, platform actions, delivery templates)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.70",
3
+ "version": "2.0.72",
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": {
@@ -66,13 +66,10 @@
66
66
  "@types/express": "^5.0.6",
67
67
  "@types/node": "^20.0.0",
68
68
  "@types/prompts": "^2.4.9",
69
- "cors": "^2.8.5",
70
- "express": "^5.2.1",
71
69
  "fast-glob": "^3.3.3",
72
70
  "html-to-docx": "^1.8.0",
73
71
  "markdown-it": "^14.1.0",
74
72
  "markdown-it-highlightjs": "^4.2.0",
75
- "mongodb": "^7.0.0",
76
73
  "pptxgenjs": "^4.0.1",
77
74
  "puppeteer": "^24.36.1",
78
75
  "qrcode": "^1.5.4",
@@ -101,6 +98,9 @@
101
98
  "commander": "^14.0.2",
102
99
  "dotenv": "^16.4.7",
103
100
  "prompts": "^2.4.2",
104
- "tree-kill": "^1.2.2"
101
+ "tree-kill": "^1.2.2",
102
+ "express": "^5.2.1",
103
+ "cors": "^2.8.5",
104
+ "mongodb": "^7.0.0"
105
105
  }
106
106
  }
package/CHANGELOG.md DELETED
@@ -1,76 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
-
6
- ## [2.1.0] - 2026-02-04
7
-
8
- ### šŸ”„ Workflow Refinement
9
- - **Consolidated Workflows**: Inlined all phase content into main workflow files (`spec.md`, `design.md`, `implement.md`) for better context retention and reduced file scatter.
10
- - **Removed Redundant Files**: Deleted separate phase folders (`design-phases`, `implement-phases`, `spec-phases`, etc.) to streamline repository structure.
11
- - **Terminology Update**: Renamed "AI Coach" to "AI Mentor" across all workflows to align with new branding.
12
- - **Enhanced Spec Workflow**: Added `spec-competitor-analysis` phase and inlined validation steps.
13
- - **Retrospective Integration**: Inlined retrospective phase content into all major workflows.
14
-
15
- ## [2.0.0] - 2024-12-19
16
-
17
- ### šŸš€ Major Release: FRAIM v2 - The Future of AI-Assisted Development
18
-
19
- #### ✨ New Features
20
- - **Complete Generic Framework**: Removed all Ashley-specific IP, making FRAIM truly universal
21
- - **Enhanced Rule System**: 13 comprehensive rule files covering all aspects of AI agent management
22
- - **Simplified Label System**: Streamlined to 9 essential labels matching real-world usage
23
- - **Spec Workflow**: Added specification phase for requirements and user experience definition
24
- - **Timeout Management**: Advanced timeout scripts with output visibility for long-running tasks
25
- - **Evidence-Based Validation**: Mandatory test evidence collection and validation
26
- - **Systematic Debugging**: Structured debugging patterns with learning capture
27
-
28
- #### šŸ”§ Improvements
29
- - **Single Install Method**: Simplified to `npm install -g fraim-framework`
30
- - **Better Documentation**: Comprehensive README with marketing-style positioning
31
- - **Example Test Cases**: Complete example with tagging system and documentation
32
- - **TypeScript Support**: Full TypeScript compilation and type safety
33
- - **Git Safety**: Safe Git commands preventing agent hangs
34
- - **Merge Requirements**: Advanced branch management with conflict resolution
35
-
36
- #### šŸ›”ļø Security & Reliability
37
- - **Agent Integrity Rules**: Prevents "fake it till you make it" behavior
38
- - **Test Ethics**: Mandatory evidence collection and validation
39
- - **Communication Standards**: Clear accountability and progress reporting
40
- - **Architecture Discipline**: Clean separation of concerns and technical debt prevention
41
-
42
- #### šŸ“š Documentation
43
- - **Marketing README**: Compelling documentation positioning FRAIM as the future
44
- - **Problem-Solution Mapping**: Each rule clearly mapped to specific problems
45
- - **Human-Agent Parallels**: Clear comparison between human and AI development
46
- - **Success Stories**: Realistic testimonials and quantified benefits
47
-
48
- #### šŸŽÆ Workflow Enhancements
49
- - **RIGOR Methodology**: Reviews, Isolation, GitOps, Observability, Retrospectives
50
- - **Phase-Based Development**: Spec → Design → Implementation → Testing → Resolution
51
- - **Agent Coordination**: Seamless handoffs between multiple AI agents
52
- - **Continuous Learning**: Retrospective-driven improvement system
53
-
54
- #### šŸ”„ Breaking Changes
55
- - **Simplified Labels**: Reduced from 15 to 9 essential labels
56
- - **Install Method**: Single npm install method (removed curl, Python options)
57
- - **Generic Examples**: All Ashley-specific examples replaced with universal patterns
58
-
59
- #### šŸ› Bug Fixes
60
- - **TypeScript Compilation**: Fixed all compilation issues
61
- - **Import Dependencies**: Resolved missing dependencies
62
- - **Test Utilities**: Made generic and framework-agnostic
63
- - **Repository References**: Updated all URLs to point to FRAIM repository
64
-
65
- #### šŸ“¦ Dependencies
66
- - Added TypeScript support with proper type definitions
67
- - Added tsx for TypeScript execution
68
- - Added dotenv for environment management
69
- - Updated Node.js engine requirement to >=16.0.0
70
-
71
- ---
72
-
73
- ## [1.0.12] - Previous Release
74
- - Initial FRAIM framework with Ashley-specific implementations
75
- - Basic rule system and workflows
76
- - GitHub integration and automation