fraim-framework 2.0.71 ā 2.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/doctor.js +55 -0
- package/dist/src/cli/commands/list-overridable.js +202 -0
- package/dist/src/cli/commands/override.js +181 -0
- package/dist/src/cli/fraim.js +4 -0
- package/dist/src/core/utils/inheritance-parser.js +150 -0
- package/dist/src/core/utils/local-registry-resolver.js +165 -0
- package/dist/src/local-mcp-server/stdio-server.js +246 -21
- package/index.js +1 -1
- package/package.json +1 -1
- package/CHANGELOG.md +0 -76
- package/dist/src/cli/commands/init.js +0 -148
- package/dist/src/cli/commands/mcp.js +0 -65
- package/dist/src/cli/commands/wizard.js +0 -35
package/bin/fraim.js
CHANGED
|
@@ -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,202 @@
|
|
|
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
|
+
.option('--job-category <category>', 'Filter by workflow category (e.g., product-building, customer-development, marketing)')
|
|
14
|
+
.option('--rules', 'Show all overridable rules')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const projectRoot = process.cwd();
|
|
17
|
+
const fraimDir = path_1.default.join(projectRoot, '.fraim');
|
|
18
|
+
const configPath = path_1.default.join(fraimDir, 'config.json');
|
|
19
|
+
const overridesDir = path_1.default.join(fraimDir, 'overrides');
|
|
20
|
+
// Validate .fraim directory exists
|
|
21
|
+
if (!fs_1.default.existsSync(fraimDir)) {
|
|
22
|
+
console.log(chalk_1.default.red('ā .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Determine registry location (try framework root first, then node_modules)
|
|
26
|
+
let registryRoot = null;
|
|
27
|
+
const frameworkRoot = path_1.default.join(__dirname, '..', '..', '..');
|
|
28
|
+
const frameworkRegistry = path_1.default.join(frameworkRoot, 'registry');
|
|
29
|
+
if (fs_1.default.existsSync(frameworkRegistry)) {
|
|
30
|
+
registryRoot = frameworkRegistry;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const nodeModulesRegistry = path_1.default.join(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry');
|
|
34
|
+
if (fs_1.default.existsSync(nodeModulesRegistry)) {
|
|
35
|
+
registryRoot = nodeModulesRegistry;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!registryRoot) {
|
|
39
|
+
console.log(chalk_1.default.red('ā Could not find FRAIM registry. Please ensure @fraim/framework is installed.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk_1.default.blue('š Overridable FRAIM Registry Paths:\n'));
|
|
43
|
+
// Get list of existing overrides
|
|
44
|
+
const existingOverrides = new Set();
|
|
45
|
+
if (fs_1.default.existsSync(overridesDir)) {
|
|
46
|
+
const scanDir = (dir, base = '') => {
|
|
47
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const relativePath = path_1.default.join(base, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
scanDir(path_1.default.join(dir, entry.name), relativePath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
existingOverrides.add(relativePath.replace(/\\/g, '/'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
scanDir(overridesDir);
|
|
59
|
+
}
|
|
60
|
+
// Handle --rules flag
|
|
61
|
+
if (options.rules) {
|
|
62
|
+
const rulesDir = path_1.default.join(registryRoot, 'rules');
|
|
63
|
+
if (fs_1.default.existsSync(rulesDir)) {
|
|
64
|
+
console.log(chalk_1.default.bold.cyan('Rules:\n'));
|
|
65
|
+
const ruleFiles = fs_1.default.readdirSync(rulesDir)
|
|
66
|
+
.filter(f => f.endsWith('.md'))
|
|
67
|
+
.sort();
|
|
68
|
+
for (const file of ruleFiles) {
|
|
69
|
+
const filePath = `rules/${file}`;
|
|
70
|
+
const hasOverride = existingOverrides.has(filePath);
|
|
71
|
+
const status = hasOverride
|
|
72
|
+
? chalk_1.default.green('[OVERRIDDEN]')
|
|
73
|
+
: chalk_1.default.gray('[OVERRIDABLE]');
|
|
74
|
+
console.log(` ${status} ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
78
|
+
showTips();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Handle --job-category flag
|
|
82
|
+
if (options.jobCategory) {
|
|
83
|
+
const category = options.jobCategory;
|
|
84
|
+
const workflowsDir = path_1.default.join(registryRoot, 'workflows', category);
|
|
85
|
+
const templatesDir = path_1.default.join(registryRoot, 'templates', category);
|
|
86
|
+
if (!fs_1.default.existsSync(workflowsDir)) {
|
|
87
|
+
console.log(chalk_1.default.red(`ā Category "${category}" not found.`));
|
|
88
|
+
console.log(chalk_1.default.gray('\nAvailable categories:'));
|
|
89
|
+
const categoriesDir = path_1.default.join(registryRoot, 'workflows');
|
|
90
|
+
const categories = fs_1.default.readdirSync(categoriesDir, { withFileTypes: true })
|
|
91
|
+
.filter(d => d.isDirectory())
|
|
92
|
+
.map(d => d.name)
|
|
93
|
+
.sort();
|
|
94
|
+
categories.forEach(c => console.log(chalk_1.default.gray(` - ${c}`)));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
// Show workflows for this category
|
|
98
|
+
console.log(chalk_1.default.bold.cyan(`Workflows (${category}):\n`));
|
|
99
|
+
const workflowFiles = fs_1.default.readdirSync(workflowsDir)
|
|
100
|
+
.filter(f => f.endsWith('.md'))
|
|
101
|
+
.sort();
|
|
102
|
+
for (const file of workflowFiles) {
|
|
103
|
+
const filePath = `workflows/${category}/${file}`;
|
|
104
|
+
const hasOverride = existingOverrides.has(filePath);
|
|
105
|
+
const status = hasOverride
|
|
106
|
+
? chalk_1.default.green('[OVERRIDDEN]')
|
|
107
|
+
: chalk_1.default.gray('[OVERRIDABLE]');
|
|
108
|
+
console.log(` ${status} ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
// Show templates for this category (if they exist)
|
|
112
|
+
if (fs_1.default.existsSync(templatesDir)) {
|
|
113
|
+
console.log(chalk_1.default.bold.cyan(`Templates (${category}):\n`));
|
|
114
|
+
const templateFiles = fs_1.default.readdirSync(templatesDir)
|
|
115
|
+
.filter(f => f.endsWith('.md') || f.endsWith('.html') || f.endsWith('.csv') || f.endsWith('.yml'))
|
|
116
|
+
.sort();
|
|
117
|
+
for (const file of templateFiles) {
|
|
118
|
+
const filePath = `templates/${category}/${file}`;
|
|
119
|
+
const hasOverride = existingOverrides.has(filePath);
|
|
120
|
+
const status = hasOverride
|
|
121
|
+
? chalk_1.default.green('[OVERRIDDEN]')
|
|
122
|
+
: chalk_1.default.gray('[OVERRIDABLE]');
|
|
123
|
+
console.log(` ${status} ${filePath}`);
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
showTips();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Default: Show available categories and existing overrides
|
|
131
|
+
console.log(chalk_1.default.bold.cyan('Available Workflow Categories:\n'));
|
|
132
|
+
const workflowsDir = path_1.default.join(registryRoot, 'workflows');
|
|
133
|
+
const categories = fs_1.default.readdirSync(workflowsDir, { withFileTypes: true })
|
|
134
|
+
.filter(d => d.isDirectory())
|
|
135
|
+
.map(d => d.name)
|
|
136
|
+
.sort();
|
|
137
|
+
// Group categories for better display
|
|
138
|
+
const columns = 3;
|
|
139
|
+
for (let i = 0; i < categories.length; i += columns) {
|
|
140
|
+
const row = categories.slice(i, i + columns);
|
|
141
|
+
const formatted = row.map(cat => chalk_1.default.gray(` ⢠${cat.padEnd(30)}`)).join('');
|
|
142
|
+
console.log(formatted);
|
|
143
|
+
}
|
|
144
|
+
console.log('');
|
|
145
|
+
// Show existing overrides
|
|
146
|
+
if (existingOverrides.size > 0) {
|
|
147
|
+
console.log(chalk_1.default.bold.cyan('Your Active Overrides:\n'));
|
|
148
|
+
// Group by type
|
|
149
|
+
const overridesByType = {
|
|
150
|
+
workflows: [],
|
|
151
|
+
templates: [],
|
|
152
|
+
rules: [],
|
|
153
|
+
other: []
|
|
154
|
+
};
|
|
155
|
+
for (const override of Array.from(existingOverrides).sort()) {
|
|
156
|
+
if (override.startsWith('workflows/')) {
|
|
157
|
+
overridesByType.workflows.push(override);
|
|
158
|
+
}
|
|
159
|
+
else if (override.startsWith('templates/')) {
|
|
160
|
+
overridesByType.templates.push(override);
|
|
161
|
+
}
|
|
162
|
+
else if (override.startsWith('rules/')) {
|
|
163
|
+
overridesByType.rules.push(override);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
overridesByType.other.push(override);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (overridesByType.workflows.length > 0) {
|
|
170
|
+
console.log(chalk_1.default.gray(' Workflows:'));
|
|
171
|
+
overridesByType.workflows.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
if (overridesByType.templates.length > 0) {
|
|
175
|
+
console.log(chalk_1.default.gray(' Templates:'));
|
|
176
|
+
overridesByType.templates.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
|
|
177
|
+
console.log('');
|
|
178
|
+
}
|
|
179
|
+
if (overridesByType.rules.length > 0) {
|
|
180
|
+
console.log(chalk_1.default.gray(' Rules:'));
|
|
181
|
+
overridesByType.rules.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
if (overridesByType.other.length > 0) {
|
|
185
|
+
console.log(chalk_1.default.gray(' Other:'));
|
|
186
|
+
overridesByType.other.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.log(chalk_1.default.gray('No active overrides yet.\n'));
|
|
192
|
+
}
|
|
193
|
+
showTips();
|
|
194
|
+
function showTips() {
|
|
195
|
+
console.log(chalk_1.default.gray('Tips:'));
|
|
196
|
+
console.log(chalk_1.default.gray(' ⢠Use "fraim override <path> --inherit" to inherit from global'));
|
|
197
|
+
console.log(chalk_1.default.gray(' ⢠Use "fraim override <path> --copy" to copy current content'));
|
|
198
|
+
console.log(chalk_1.default.gray(' ⢠Use --job-category <category> to see category-specific items'));
|
|
199
|
+
console.log(chalk_1.default.gray(' ⢠Use --rules to see all overridable rules'));
|
|
200
|
+
console.log(chalk_1.default.gray(' ⢠Overrides are stored in .fraim/overrides/'));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
const git_utils_1 = require("../../core/utils/git-utils");
|
|
13
|
+
exports.overrideCommand = new commander_1.Command('override')
|
|
14
|
+
.description('Create a local override for a FRAIM registry file')
|
|
15
|
+
.argument('<path>', 'Registry path to override (e.g., workflows/product-building/spec.md)')
|
|
16
|
+
.option('--inherit', 'Create override with {{ import }} directive (inherits from global)')
|
|
17
|
+
.option('--copy', 'Copy current content from server to local')
|
|
18
|
+
.option('--local', 'Fetch from local development server (port derived from git branch)')
|
|
19
|
+
.action(async (registryPath, options) => {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const fraimDir = path_1.default.join(projectRoot, '.fraim');
|
|
22
|
+
const configPath = path_1.default.join(fraimDir, 'config.json');
|
|
23
|
+
// Validate .fraim directory exists
|
|
24
|
+
if (!fs_1.default.existsSync(fraimDir)) {
|
|
25
|
+
console.log(chalk_1.default.red('ā .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// Validate path format
|
|
29
|
+
if (registryPath.includes('..') || path_1.default.isAbsolute(registryPath)) {
|
|
30
|
+
console.log(chalk_1.default.red('ā Invalid path. Path must be relative and cannot contain ".."'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// Ensure exactly one option is specified
|
|
34
|
+
if (options.inherit && options.copy) {
|
|
35
|
+
console.log(chalk_1.default.red('ā Cannot use both --inherit and --copy. Choose one.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!options.inherit && !options.copy) {
|
|
39
|
+
console.log(chalk_1.default.red('ā Must specify either --inherit or --copy.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Create overrides directory structure
|
|
43
|
+
const overridePath = path_1.default.join(fraimDir, 'overrides', registryPath);
|
|
44
|
+
const overrideDir = path_1.default.dirname(overridePath);
|
|
45
|
+
if (!fs_1.default.existsSync(overrideDir)) {
|
|
46
|
+
fs_1.default.mkdirSync(overrideDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
// Check if override already exists
|
|
49
|
+
if (fs_1.default.existsSync(overridePath)) {
|
|
50
|
+
console.log(chalk_1.default.yellow(`ā ļø Override already exists: ${registryPath}`));
|
|
51
|
+
console.log(chalk_1.default.gray(` Location: ${overridePath}`));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
let content;
|
|
55
|
+
if (options.inherit) {
|
|
56
|
+
// Create file with import directive
|
|
57
|
+
content = `{{ import: ${registryPath} }}\n\n<!-- Add your custom content below -->\n`;
|
|
58
|
+
console.log(chalk_1.default.blue(`š Creating inherited override for: ${registryPath}`));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Fetch current content from server using MCP protocol
|
|
62
|
+
const isLocal = options.local || false;
|
|
63
|
+
const serverType = isLocal ? 'local server' : 'remote server';
|
|
64
|
+
console.log(chalk_1.default.blue(`š„ Fetching current content from ${serverType}: ${registryPath}`));
|
|
65
|
+
try {
|
|
66
|
+
// Read config to get remote URL
|
|
67
|
+
let config = {};
|
|
68
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
69
|
+
config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
70
|
+
}
|
|
71
|
+
// Determine server URL
|
|
72
|
+
let serverUrl;
|
|
73
|
+
if (isLocal) {
|
|
74
|
+
const localPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : (0, git_utils_1.getPort)();
|
|
75
|
+
serverUrl = `http://localhost:${localPort}`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
serverUrl = config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
79
|
+
}
|
|
80
|
+
const apiKey = isLocal ? 'local-dev' : (config.apiKey || process.env.FRAIM_API_KEY);
|
|
81
|
+
const headers = {
|
|
82
|
+
'Content-Type': 'application/json'
|
|
83
|
+
};
|
|
84
|
+
if (apiKey) {
|
|
85
|
+
headers['x-api-key'] = apiKey;
|
|
86
|
+
}
|
|
87
|
+
// First, establish session with fraim_connect
|
|
88
|
+
const connectRequest = {
|
|
89
|
+
jsonrpc: '2.0',
|
|
90
|
+
id: 0,
|
|
91
|
+
method: 'tools/call',
|
|
92
|
+
params: {
|
|
93
|
+
name: 'fraim_connect',
|
|
94
|
+
arguments: {
|
|
95
|
+
agent: { name: 'fraim-cli', model: 'override-command' },
|
|
96
|
+
machine: {
|
|
97
|
+
hostname: require('os').hostname(),
|
|
98
|
+
platform: process.platform,
|
|
99
|
+
memory: require('os').totalmem(),
|
|
100
|
+
cpus: require('os').cpus().length
|
|
101
|
+
},
|
|
102
|
+
repo: {
|
|
103
|
+
url: config.repository?.url || 'https://github.com/unknown/unknown',
|
|
104
|
+
owner: config.repository?.owner || 'unknown',
|
|
105
|
+
name: config.repository?.name || 'unknown',
|
|
106
|
+
branch: 'main'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
await axios_1.default.post(`${serverUrl}/mcp`, connectRequest, { headers, timeout: 30000 });
|
|
112
|
+
// Now determine MCP tool and arguments based on path
|
|
113
|
+
let toolName;
|
|
114
|
+
let toolArgs;
|
|
115
|
+
if (registryPath.startsWith('workflows/')) {
|
|
116
|
+
// Extract workflow name from path
|
|
117
|
+
// e.g., "workflows/product-building/spec.md" -> "spec"
|
|
118
|
+
const parts = registryPath.split('/');
|
|
119
|
+
const workflowName = parts[parts.length - 1].replace('.md', '');
|
|
120
|
+
toolName = 'get_fraim_workflow';
|
|
121
|
+
toolArgs = { workflow: workflowName };
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
toolName = 'get_fraim_file';
|
|
125
|
+
toolArgs = { path: registryPath };
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk_1.default.gray(` Using MCP tool: ${toolName}`));
|
|
128
|
+
// Make MCP JSON-RPC request
|
|
129
|
+
const mcpRequest = {
|
|
130
|
+
jsonrpc: '2.0',
|
|
131
|
+
id: 1,
|
|
132
|
+
method: 'tools/call',
|
|
133
|
+
params: {
|
|
134
|
+
name: toolName,
|
|
135
|
+
arguments: toolArgs
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const response = await axios_1.default.post(`${serverUrl}/mcp`, mcpRequest, {
|
|
139
|
+
headers,
|
|
140
|
+
timeout: 30000
|
|
141
|
+
});
|
|
142
|
+
// Extract content from MCP response
|
|
143
|
+
if (response.data.error) {
|
|
144
|
+
throw new Error(response.data.error.message || 'MCP request failed');
|
|
145
|
+
}
|
|
146
|
+
const result = response.data.result;
|
|
147
|
+
if (!result || !result.content || !Array.isArray(result.content)) {
|
|
148
|
+
throw new Error('Unexpected MCP response format');
|
|
149
|
+
}
|
|
150
|
+
// Extract text content from MCP response
|
|
151
|
+
const textContent = result.content.find((c) => c.type === 'text');
|
|
152
|
+
if (!textContent || !textContent.text) {
|
|
153
|
+
throw new Error('No text content in MCP response');
|
|
154
|
+
}
|
|
155
|
+
content = textContent.text;
|
|
156
|
+
console.log(chalk_1.default.green(`ā
Fetched ${content.length} bytes from ${serverType}`));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.log(chalk_1.default.red(`ā Failed to fetch content from ${serverType}: ${error.message}`));
|
|
160
|
+
if (isLocal) {
|
|
161
|
+
console.log(chalk_1.default.gray(` Tip: Make sure the FRAIM server is running locally (npm run start:fraim)`));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(chalk_1.default.gray(` Tip: Check your API key and network connection`));
|
|
165
|
+
console.log(chalk_1.default.gray(` Or use --local to fetch from a locally running server`));
|
|
166
|
+
}
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Write override file
|
|
171
|
+
try {
|
|
172
|
+
fs_1.default.writeFileSync(overridePath, content, 'utf-8');
|
|
173
|
+
console.log(chalk_1.default.green(`ā
Created override: ${registryPath}`));
|
|
174
|
+
console.log(chalk_1.default.gray(` Location: ${overridePath}`));
|
|
175
|
+
console.log(chalk_1.default.gray(` Edit this file to customize for your project`));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
console.log(chalk_1.default.red(`ā Failed to write override file: ${error.message}`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
});
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -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;
|