fraim-framework 2.0.74 → 2.0.76
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/README.md +12 -105
- package/bin/fraim-mcp.js +1 -1
- package/dist/src/cli/commands/add-ide.js +15 -17
- package/dist/src/cli/commands/init-project.js +11 -9
- package/dist/src/cli/commands/install.js +86 -0
- package/dist/src/cli/commands/override.js +11 -1
- package/dist/src/cli/commands/sync.js +2 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +15 -19
- package/dist/src/cli/setup/codex-local-config.js +37 -0
- package/dist/src/cli/setup/mcp-config-generator.js +94 -11
- package/dist/src/cli/utils/remote-sync.js +22 -2
- package/dist/src/core/config-loader.js +4 -10
- package/dist/src/core/types.js +2 -19
- package/dist/src/core/utils/include-resolver.js +47 -0
- package/dist/src/local-mcp-server/stdio-server.js +624 -203
- package/package.json +116 -106
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateMCPConfig = exports.generateWindsurfMCPServers = exports.generateVSCodeMCPServers = exports.generateCodexMCPServers = exports.generateKiroMCPServers = exports.generateClaudeMCPServers = exports.generateStandardMCPServers = void 0;
|
|
3
|
+
exports.generateMCPConfig = exports.generateWindsurfMCPServers = exports.generateVSCodeMCPServers = exports.generateCodexMCPServers = exports.generateKiroMCPServers = exports.generateClaudeMCPServers = exports.generateStandardMCPServers = exports.mergeTomlMCPServers = exports.extractTomlMcpServerBlock = void 0;
|
|
4
|
+
const escapeTomlString = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
5
|
+
const findTomlServerBlockRange = (content, server) => {
|
|
6
|
+
const lines = content.split(/\r?\n/);
|
|
7
|
+
const serverSection = `mcp_servers.${server}`;
|
|
8
|
+
const serverHeader = `[${serverSection}]`;
|
|
9
|
+
const start = lines.findIndex(line => line.trim() === serverHeader);
|
|
10
|
+
if (start === -1) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
let end = lines.length;
|
|
14
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
15
|
+
const sectionMatch = lines[i].trim().match(/^\[([^\]]+)\]$/);
|
|
16
|
+
if (!sectionMatch)
|
|
17
|
+
continue;
|
|
18
|
+
const sectionName = sectionMatch[1];
|
|
19
|
+
if (sectionName === serverSection || sectionName.startsWith(`${serverSection}.`)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
end = i;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
return { start, end };
|
|
26
|
+
};
|
|
27
|
+
const extractTomlMcpServerBlock = (content, server) => {
|
|
28
|
+
const range = findTomlServerBlockRange(content, server);
|
|
29
|
+
if (!range)
|
|
30
|
+
return null;
|
|
31
|
+
const lines = content.split(/\r?\n/);
|
|
32
|
+
return lines.slice(range.start, range.end).join('\n').trim();
|
|
33
|
+
};
|
|
34
|
+
exports.extractTomlMcpServerBlock = extractTomlMcpServerBlock;
|
|
35
|
+
const removeTomlMcpServerBlock = (content, server) => {
|
|
36
|
+
const range = findTomlServerBlockRange(content, server);
|
|
37
|
+
if (!range)
|
|
38
|
+
return content;
|
|
39
|
+
const lines = content.split(/\r?\n/);
|
|
40
|
+
const updated = [...lines.slice(0, range.start), ...lines.slice(range.end)].join('\n');
|
|
41
|
+
return updated.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
42
|
+
};
|
|
43
|
+
const appendTomlBlock = (content, block) => {
|
|
44
|
+
const trimmedContent = content.trimEnd();
|
|
45
|
+
const trimmedBlock = block.trim();
|
|
46
|
+
if (!trimmedBlock)
|
|
47
|
+
return trimmedContent;
|
|
48
|
+
if (!trimmedContent)
|
|
49
|
+
return `${trimmedBlock}\n`;
|
|
50
|
+
return `${trimmedContent}\n\n${trimmedBlock}\n`;
|
|
51
|
+
};
|
|
52
|
+
const mergeTomlMCPServers = (existingContent, generatedContent, servers) => {
|
|
53
|
+
let merged = existingContent || '';
|
|
54
|
+
const addedServers = [];
|
|
55
|
+
const replacedServers = [];
|
|
56
|
+
const skippedServers = [];
|
|
57
|
+
for (const server of servers) {
|
|
58
|
+
const newBlock = (0, exports.extractTomlMcpServerBlock)(generatedContent, server);
|
|
59
|
+
if (!newBlock) {
|
|
60
|
+
skippedServers.push(server);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const existingBlock = (0, exports.extractTomlMcpServerBlock)(merged, server);
|
|
64
|
+
if (existingBlock && existingBlock.trim() === newBlock.trim()) {
|
|
65
|
+
skippedServers.push(server);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (existingBlock) {
|
|
69
|
+
merged = removeTomlMcpServerBlock(merged, server);
|
|
70
|
+
merged = appendTomlBlock(merged, newBlock);
|
|
71
|
+
replacedServers.push(server);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
merged = appendTomlBlock(merged, newBlock);
|
|
75
|
+
addedServers.push(server);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
content: merged.trimEnd() + '\n',
|
|
79
|
+
addedServers,
|
|
80
|
+
replacedServers,
|
|
81
|
+
skippedServers
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
exports.mergeTomlMCPServers = mergeTomlMCPServers;
|
|
4
85
|
const generateStandardMCPServers = (fraimKey, githubToken) => {
|
|
5
86
|
const servers = {
|
|
6
87
|
git: {
|
|
@@ -93,17 +174,19 @@ const generateKiroMCPServers = (fraimKey, githubToken) => {
|
|
|
93
174
|
};
|
|
94
175
|
exports.generateKiroMCPServers = generateKiroMCPServers;
|
|
95
176
|
const generateCodexMCPServers = (fraimKey, githubToken) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
177
|
+
const escapedFraimKey = escapeTomlString(fraimKey);
|
|
178
|
+
const escapedGithubToken = escapeTomlString(githubToken);
|
|
179
|
+
let config = `
|
|
180
|
+
[mcp_servers.git]
|
|
181
|
+
command = "npx"
|
|
99
182
|
args = ["-y", "@cyanheads/git-mcp-server"]
|
|
100
183
|
`;
|
|
101
184
|
// Only add GitHub server if token is provided
|
|
102
185
|
if (githubToken) {
|
|
103
|
-
config += `
|
|
104
|
-
[mcp_servers.github]
|
|
105
|
-
url = "https://api.githubcopilot.com/mcp/"
|
|
106
|
-
|
|
186
|
+
config += `
|
|
187
|
+
[mcp_servers.github]
|
|
188
|
+
url = "https://api.githubcopilot.com/mcp/"
|
|
189
|
+
http_headers = { Authorization = "Bearer ${escapedGithubToken}" }
|
|
107
190
|
`;
|
|
108
191
|
}
|
|
109
192
|
config += `
|
|
@@ -114,9 +197,9 @@ args = ["-y", "@playwright/mcp"]
|
|
|
114
197
|
[mcp_servers.fraim]
|
|
115
198
|
command = "fraim-mcp"
|
|
116
199
|
|
|
117
|
-
[mcp_servers.fraim.env]
|
|
118
|
-
FRAIM_API_KEY = "${
|
|
119
|
-
FRAIM_REMOTE_URL = "https://fraim.wellnessatwork.me"
|
|
200
|
+
[mcp_servers.fraim.env]
|
|
201
|
+
FRAIM_API_KEY = "${escapedFraimKey}"
|
|
202
|
+
FRAIM_REMOTE_URL = "https://fraim.wellnessatwork.me"
|
|
120
203
|
`;
|
|
121
204
|
return config;
|
|
122
205
|
};
|
|
@@ -28,6 +28,7 @@ async function syncFromRemote(options) {
|
|
|
28
28
|
success: false,
|
|
29
29
|
workflowsSynced: 0,
|
|
30
30
|
scriptsSynced: 0,
|
|
31
|
+
coachingSynced: 0,
|
|
31
32
|
error: 'FRAIM_API_KEY not set'
|
|
32
33
|
};
|
|
33
34
|
}
|
|
@@ -48,6 +49,7 @@ async function syncFromRemote(options) {
|
|
|
48
49
|
success: false,
|
|
49
50
|
workflowsSynced: 0,
|
|
50
51
|
scriptsSynced: 0,
|
|
52
|
+
coachingSynced: 0,
|
|
51
53
|
error: 'No files received'
|
|
52
54
|
};
|
|
53
55
|
}
|
|
@@ -88,11 +90,28 @@ async function syncFromRemote(options) {
|
|
|
88
90
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
89
91
|
console.log(chalk_1.default.gray(` + ${file.path}`));
|
|
90
92
|
}
|
|
91
|
-
|
|
93
|
+
// Sync coaching files to .fraim/coaching-moments/
|
|
94
|
+
const coachingFiles = files.filter(f => f.type === 'coaching');
|
|
95
|
+
const coachingDir = (0, path_1.join)(options.projectRoot, '.fraim', 'coaching-moments');
|
|
96
|
+
if (!(0, fs_1.existsSync)(coachingDir)) {
|
|
97
|
+
(0, fs_1.mkdirSync)(coachingDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
cleanDirectory(coachingDir);
|
|
100
|
+
for (const file of coachingFiles) {
|
|
101
|
+
const filePath = (0, path_1.join)(coachingDir, file.path);
|
|
102
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
103
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
104
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
107
|
+
console.log(chalk_1.default.gray(` + coaching-moments/${file.path}`));
|
|
108
|
+
}
|
|
109
|
+
console.log(chalk_1.default.green(`\n✅ Synced ${workflowFiles.length} workflows, ${scriptFiles.length} scripts, and ${coachingFiles.length} coaching files from remote`));
|
|
92
110
|
return {
|
|
93
111
|
success: true,
|
|
94
112
|
workflowsSynced: workflowFiles.length,
|
|
95
|
-
scriptsSynced: scriptFiles.length
|
|
113
|
+
scriptsSynced: scriptFiles.length,
|
|
114
|
+
coachingSynced: coachingFiles.length
|
|
96
115
|
};
|
|
97
116
|
}
|
|
98
117
|
catch (error) {
|
|
@@ -101,6 +120,7 @@ async function syncFromRemote(options) {
|
|
|
101
120
|
success: false,
|
|
102
121
|
workflowsSynced: 0,
|
|
103
122
|
scriptsSynced: 0,
|
|
123
|
+
coachingSynced: 0,
|
|
104
124
|
error: error.message
|
|
105
125
|
};
|
|
106
126
|
}
|
|
@@ -46,22 +46,16 @@ function loadFraimConfig() {
|
|
|
46
46
|
...(config.customizations || {})
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
-
// Add optional fields only if they exist in the config
|
|
50
|
-
if (config.persona) {
|
|
51
|
-
mergedConfig.persona = config.persona;
|
|
52
|
-
}
|
|
53
|
-
if (config.marketing) {
|
|
54
|
-
mergedConfig.marketing = config.marketing;
|
|
55
|
-
}
|
|
56
|
-
if (config.database) {
|
|
57
|
-
mergedConfig.database = config.database;
|
|
58
|
-
}
|
|
49
|
+
// Add optional workflow-driven fields only if they exist in the config
|
|
59
50
|
if (config.compliance) {
|
|
60
51
|
mergedConfig.compliance = config.compliance;
|
|
61
52
|
}
|
|
62
53
|
if (config.learning) {
|
|
63
54
|
mergedConfig.learning = config.learning;
|
|
64
55
|
}
|
|
56
|
+
if (config.competitors && typeof config.competitors === 'object') {
|
|
57
|
+
mergedConfig.competitors = config.competitors;
|
|
58
|
+
}
|
|
65
59
|
console.log(`📋 Loaded FRAIM config from .fraim/config.json (version ${mergedConfig.version})`);
|
|
66
60
|
// Warn about deprecated git config
|
|
67
61
|
if (config.git && !config.repository) {
|
package/dist/src/core/types.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* TypeScript types for .fraim/config.json
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.
|
|
7
|
+
exports.DEFAULT_FRAIM_CONFIG = void 0;
|
|
8
8
|
/**
|
|
9
9
|
* Default configuration values
|
|
10
10
|
*/
|
|
@@ -19,22 +19,5 @@ exports.DEFAULT_FRAIM_CONFIG = {
|
|
|
19
19
|
name: '',
|
|
20
20
|
defaultBranch: 'main'
|
|
21
21
|
},
|
|
22
|
-
customizations: {
|
|
23
|
-
workflowsPath: '.fraim/workflows'
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Minimal config template for new projects
|
|
28
|
-
*/
|
|
29
|
-
exports.MINIMAL_FRAIM_CONFIG = {
|
|
30
|
-
version: '2.0.47',
|
|
31
|
-
project: {
|
|
32
|
-
name: 'My Project'
|
|
33
|
-
},
|
|
34
|
-
repository: {
|
|
35
|
-
provider: 'github',
|
|
36
|
-
owner: 'your-username',
|
|
37
|
-
name: 'your-repo',
|
|
38
|
-
defaultBranch: 'main'
|
|
39
|
-
}
|
|
22
|
+
customizations: {}
|
|
40
23
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve {{include:path}} directives in registry content.
|
|
4
|
+
*
|
|
5
|
+
* Used by: MCP service (get_fraim_file, get_fraim_workflow), AI Mentor (phase instructions),
|
|
6
|
+
* and any other server-side content that needs includes resolved.
|
|
7
|
+
*
|
|
8
|
+
* - Resolves recursively (e.g. skills can include other skills)
|
|
9
|
+
* - MAX_PASSES prevents infinite loops from circular includes
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.MAX_INCLUDE_PASSES = void 0;
|
|
13
|
+
exports.resolveIncludesWithIndex = resolveIncludesWithIndex;
|
|
14
|
+
const fs_1 = require("fs");
|
|
15
|
+
/** Maximum resolution passes to prevent infinite loops from circular includes */
|
|
16
|
+
exports.MAX_INCLUDE_PASSES = 10;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve {{include:path}} directives in content.
|
|
19
|
+
* Looks up referenced files in the fileIndex and inlines their content.
|
|
20
|
+
*
|
|
21
|
+
* @param content - Raw content that may contain {{include:path}} directives
|
|
22
|
+
* @param fileIndex - Map of path -> { fullPath, ... } for registry files
|
|
23
|
+
* @returns Content with all resolvable includes inlined (recursive, up to MAX_PASSES)
|
|
24
|
+
*/
|
|
25
|
+
function resolveIncludesWithIndex(content, fileIndex) {
|
|
26
|
+
let result = content;
|
|
27
|
+
let pass = 0;
|
|
28
|
+
while (result.includes('{{include:') && pass < exports.MAX_INCLUDE_PASSES) {
|
|
29
|
+
result = result.replace(/\{\{include:([^}]+)\}\}/g, (match, filePath) => {
|
|
30
|
+
const trimmedPath = filePath.trim();
|
|
31
|
+
const fileEntry = fileIndex.get(trimmedPath);
|
|
32
|
+
if (fileEntry?.fullPath) {
|
|
33
|
+
try {
|
|
34
|
+
return (0, fs_1.readFileSync)(fileEntry.fullPath, 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error(`❌ Failed to read included file: ${trimmedPath}`, error);
|
|
38
|
+
return match;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.warn(`⚠️ Include file not found in fileIndex: ${trimmedPath}`);
|
|
42
|
+
return match;
|
|
43
|
+
});
|
|
44
|
+
pass++;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|