fraim 2.0.105 ā 2.0.108
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/dist/src/cli/commands/setup.js +57 -12
- package/dist/src/cli/commands/sync.js +15 -0
- package/dist/src/cli/setup/ide-global-integration.js +127 -0
- package/dist/src/cli/setup/user-level-sync.js +59 -0
- package/dist/src/core/utils/local-registry-resolver.js +26 -14
- package/dist/src/core/utils/project-fraim-paths.js +37 -0
- package/dist/src/local-mcp-server/stdio-server.js +18 -3
- package/package.json +1 -1
|
@@ -602,6 +602,26 @@ const runSetup = async (options) => {
|
|
|
602
602
|
console.log(chalk_1.default.gray(' You can update them manually with: fraim add-ide <ide-name>\n'));
|
|
603
603
|
}
|
|
604
604
|
}
|
|
605
|
+
// Sync user-level FRAIM artifacts (always, on both initial and update)
|
|
606
|
+
try {
|
|
607
|
+
const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
|
|
608
|
+
console.log(chalk_1.default.blue('\nš¦ Syncing user-level FRAIM content...'));
|
|
609
|
+
await syncUserLevelArtifacts();
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
console.log(chalk_1.default.yellow(`ā ļø User-level content sync encountered issues: ${e.message}`));
|
|
613
|
+
console.log(chalk_1.default.gray(' You can sync later with: fraim sync --global'));
|
|
614
|
+
}
|
|
615
|
+
// Install IDE slash commands and global rules
|
|
616
|
+
try {
|
|
617
|
+
const { installSlashCommands, installGlobalRules } = await Promise.resolve().then(() => __importStar(require('../setup/ide-global-integration')));
|
|
618
|
+
console.log(chalk_1.default.blue('\nš Installing IDE integrations...'));
|
|
619
|
+
await installSlashCommands();
|
|
620
|
+
await installGlobalRules();
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
console.log(chalk_1.default.yellow(`ā ļø IDE integration encountered issues: ${e.message}`));
|
|
624
|
+
}
|
|
605
625
|
// Auto-run project init if we're in a git repo (only on initial setup)
|
|
606
626
|
if (!isUpdate) {
|
|
607
627
|
if ((0, platform_detection_1.isGitRepository)()) {
|
|
@@ -613,24 +633,49 @@ const runSetup = async (options) => {
|
|
|
613
633
|
console.log(chalk_1.default.cyan(' To initialize a project later, cd into a repo and run: fraim init-project'));
|
|
614
634
|
}
|
|
615
635
|
}
|
|
616
|
-
// Show summary
|
|
617
|
-
console.log(chalk_1.default.green('\n
|
|
618
|
-
console.log(chalk_1.default.
|
|
636
|
+
// Show mode-aware summary with clear value prop and next steps
|
|
637
|
+
console.log(chalk_1.default.green('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
638
|
+
console.log(chalk_1.default.green(' FRAIM is ready!'));
|
|
639
|
+
console.log(chalk_1.default.green('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
640
|
+
console.log(chalk_1.default.white('\n FRAIM is an AI management layer that turns you into'));
|
|
641
|
+
console.log(chalk_1.default.white(' a manager of AI agents. Run multiple agents through'));
|
|
642
|
+
console.log(chalk_1.default.white(' structured jobs, get manager-style coaching, and build'));
|
|
643
|
+
console.log(chalk_1.default.white(' a learning loop where agents improve over time.'));
|
|
644
|
+
console.log(chalk_1.default.gray('\n 60+ jobs across engineering, marketing, fundraising,'));
|
|
645
|
+
console.log(chalk_1.default.gray(' legal, product, hiring, customer development, and more.'));
|
|
646
|
+
// Show which IDEs were configured and how to use FRAIM in each
|
|
647
|
+
const { detectInstalledIDEs: detectIDEs } = await Promise.resolve().then(() => __importStar(require('../setup/ide-detector')));
|
|
648
|
+
const configuredIDEs = detectIDEs();
|
|
649
|
+
if (configuredIDEs.length > 0) {
|
|
650
|
+
const ideNames = configuredIDEs.map(ide => ide.name).join(', ');
|
|
651
|
+
console.log(chalk_1.default.cyan(`\n FRAIM is configured for: `) + chalk_1.default.white(ideNames));
|
|
652
|
+
console.log(chalk_1.default.cyan('\n Get started ā open any of those tools and:'));
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
console.log(chalk_1.default.cyan('\n Get started ā open your AI tool and:'));
|
|
656
|
+
}
|
|
657
|
+
// Check if Claude is among configured IDEs
|
|
658
|
+
const hasClaude = configuredIDEs.some(ide => ide.name.toLowerCase().includes('claude'));
|
|
659
|
+
if (hasClaude) {
|
|
660
|
+
console.log(chalk_1.default.white(' /fraim ') + chalk_1.default.gray('(Claude Code) Browse all jobs'));
|
|
661
|
+
}
|
|
662
|
+
console.log(chalk_1.default.white(' "What can FRAIM help me with?" ') + chalk_1.default.gray('Works in any AI tool'));
|
|
663
|
+
console.log(chalk_1.default.gray('\n Just tell your AI what you need ā FRAIM will find the right job.'));
|
|
619
664
|
if (mode !== 'conversational') {
|
|
665
|
+
console.log(chalk_1.default.cyan('\n To set up FRAIM in a specific project:'));
|
|
666
|
+
console.log(chalk_1.default.white(' cd your-project && fraim init-project'));
|
|
667
|
+
console.log(chalk_1.default.gray(' This enables project-specific customizations,'));
|
|
668
|
+
console.log(chalk_1.default.gray(' GitHub workflows, and team learning.'));
|
|
620
669
|
const configuredProviders = await Promise.all(Object.keys(tokens).map(async (id) => await (0, provider_registry_1.getProviderDisplayName)(id)));
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
console.log(chalk_1.default.cyan(' 1. cd into any project directory'));
|
|
625
|
-
console.log(chalk_1.default.cyan(' 2. Run: fraim init-project'));
|
|
626
|
-
console.log(chalk_1.default.cyan(' 3. Ask your AI agent: "FRAIM was just installed. Read the FRAIM docs, explain what it can do for me, then run project-onboarding."'));
|
|
627
|
-
if (mode === 'integrated') {
|
|
670
|
+
if (configuredProviders.length > 0) {
|
|
671
|
+
console.log(chalk_1.default.gray(`\n Platforms: ${configuredProviders.join(', ')}`));
|
|
672
|
+
}
|
|
628
673
|
const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
|
|
629
674
|
const unconfiguredProviders = allProviderIds.filter(id => !tokens[id]);
|
|
630
675
|
if (unconfiguredProviders.length > 0) {
|
|
631
|
-
console.log(chalk_1.default.gray('\n
|
|
676
|
+
console.log(chalk_1.default.gray('\n To add more platforms later:'));
|
|
632
677
|
unconfiguredProviders.forEach(id => {
|
|
633
|
-
console.log(chalk_1.default.gray(`
|
|
678
|
+
console.log(chalk_1.default.gray(` fraim setup --${id}`));
|
|
634
679
|
});
|
|
635
680
|
}
|
|
636
681
|
}
|
|
@@ -84,6 +84,20 @@ function updateVersionInConfig(fraimDir) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
const runSync = async (options) => {
|
|
87
|
+
// Handle --global flag: sync to user-level ~/.fraim/ instead of project
|
|
88
|
+
if (options.global) {
|
|
89
|
+
console.log(chalk_1.default.blue('Syncing FRAIM content to user-level directory (~/.fraim/)...'));
|
|
90
|
+
try {
|
|
91
|
+
const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
|
|
92
|
+
await syncUserLevelArtifacts();
|
|
93
|
+
console.log(chalk_1.default.green('\nā
User-level FRAIM content sync complete.'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(chalk_1.default.red(`User-level sync failed: ${error.message}`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
87
101
|
const projectRoot = process.cwd();
|
|
88
102
|
const config = (0, config_loader_1.loadFraimConfig)();
|
|
89
103
|
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
|
|
@@ -171,4 +185,5 @@ exports.syncCommand = new commander_1.Command('sync')
|
|
|
171
185
|
.option('-f, --force', 'Force sync even if digest matches')
|
|
172
186
|
.option('--skip-updates', 'Skip checking for CLI updates (legacy)')
|
|
173
187
|
.option('--local', 'Sync from local development server (port derived from git branch)')
|
|
188
|
+
.option('--global', 'Sync user-level FRAIM content (~/.fraim/) instead of project')
|
|
174
189
|
.action(exports.runSync);
|
|
@@ -0,0 +1,127 @@
|
|
|
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.installSlashCommands = installSlashCommands;
|
|
7
|
+
exports.installGlobalRules = installGlobalRules;
|
|
8
|
+
/**
|
|
9
|
+
* IDE Global Integration
|
|
10
|
+
*
|
|
11
|
+
* Installs FRAIM slash commands and global rules into user-level IDE
|
|
12
|
+
* configuration directories. These enable FRAIM to be discoverable
|
|
13
|
+
* in any project without requiring fraim init-project.
|
|
14
|
+
*
|
|
15
|
+
* Part of: User-Level FRAIM Artifacts with Local Shadow Semantics
|
|
16
|
+
*/
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
21
|
+
const FRAIM_SLASH_COMMAND_CONTENT = `The user wants to run FRAIM. The requested job or topic is: $ARGUMENTS
|
|
22
|
+
|
|
23
|
+
Follow this process:
|
|
24
|
+
|
|
25
|
+
1. **If no argument was given** (the line above ends with ": "):
|
|
26
|
+
Call \`list_fraim_jobs()\` to discover available jobs. Present the results to the user grouped by business function (the server returns jobs organized by category ā use those categories as group headings). For each group, list 3-5 of the most impactful jobs with a one-line description.
|
|
27
|
+
|
|
28
|
+
After listing, suggest 2-3 starting points based on what seems most relevant:
|
|
29
|
+
- If in a code repo: suggest jobs from engineering/product-building categories
|
|
30
|
+
- If no repo context: suggest jobs from marketing, fundraising, or business categories
|
|
31
|
+
|
|
32
|
+
Ask the user which job they want to run, then proceed to step 2.
|
|
33
|
+
|
|
34
|
+
2. **Find the match**: from the list returned by \`list_fraim_jobs()\`, find the job whose name matches or closely resembles the argument. If no job matches, search for a matching skill by calling \`get_fraim_file({ path: "skills/<likely-category>/<argument>.md" })\` ā try common categories like \`engineering/\`, \`marketing/\`, \`business/\`, \`product-management/\`, \`ai-tools/\`. Confirm the match with the user.
|
|
35
|
+
|
|
36
|
+
3. **Load the full content**:
|
|
37
|
+
- For jobs: call \`get_fraim_job({ job: "<matched-job-name>" })\` ā never execute from stub content.
|
|
38
|
+
- For skills: the content from \`get_fraim_file\` is the full skill. Use it directly.
|
|
39
|
+
|
|
40
|
+
4. **Execute**: for jobs, follow the phased instructions returned by \`get_fraim_job\`, using \`seekMentoring\` at phase transitions where indicated. For skills, apply the skill steps to the user's current context.
|
|
41
|
+
`;
|
|
42
|
+
const FRAIM_CURSOR_RULES_CONTENT = `# FRAIM Rules
|
|
43
|
+
|
|
44
|
+
This workspace uses FRAIM (Framework for Rigor-based AI Management).
|
|
45
|
+
|
|
46
|
+
## Discovery
|
|
47
|
+
- Before acting on any user request, check if a FRAIM job matches the intent.
|
|
48
|
+
- Call \`list_fraim_jobs()\` to discover available jobs.
|
|
49
|
+
- Call \`get_fraim_job({ job: "<job-name>" })\` to get full phased instructions.
|
|
50
|
+
|
|
51
|
+
## Execution
|
|
52
|
+
- Jobs are FRAIM's primary execution units ā structured, multi-phase workflows.
|
|
53
|
+
- Skills are reusable capabilities that jobs compose.
|
|
54
|
+
- Rules are always-on constraints and conventions.
|
|
55
|
+
- Follow phased instructions and use \`seekMentoring\` at phase transitions.
|
|
56
|
+
|
|
57
|
+
## Principles
|
|
58
|
+
- Never execute from job stubs ā always load full instructions via MCP.
|
|
59
|
+
- Follow the constitution: integrity, correctness, contribution, completeness.
|
|
60
|
+
`;
|
|
61
|
+
/**
|
|
62
|
+
* Install the FRAIM slash command for Claude Code at the user level.
|
|
63
|
+
* Writes to ~/.claude/commands/fraim.md.
|
|
64
|
+
* Does NOT overwrite if the file already exists.
|
|
65
|
+
*
|
|
66
|
+
* @param homeDir - Override for home directory (for testing)
|
|
67
|
+
*/
|
|
68
|
+
async function installSlashCommands(homeDir) {
|
|
69
|
+
const home = homeDir || os_1.default.homedir();
|
|
70
|
+
const claudeDir = path_1.default.join(home, '.claude');
|
|
71
|
+
// Only install if Claude Code is installed (indicated by .claude/ existing)
|
|
72
|
+
if (!fs_1.default.existsSync(claudeDir)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const commandsDir = path_1.default.join(claudeDir, 'commands');
|
|
76
|
+
const slashCommandPath = path_1.default.join(commandsDir, 'fraim.md');
|
|
77
|
+
// Do not overwrite existing file
|
|
78
|
+
if (fs_1.default.existsSync(slashCommandPath)) {
|
|
79
|
+
console.log(chalk_1.default.gray(' Claude slash command already exists ā skipping'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
fs_1.default.mkdirSync(commandsDir, { recursive: true });
|
|
83
|
+
fs_1.default.writeFileSync(slashCommandPath, FRAIM_SLASH_COMMAND_CONTENT, 'utf8');
|
|
84
|
+
console.log(chalk_1.default.green(' ā
Installed Claude slash command (~/.claude/commands/fraim.md)'));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Install FRAIM global rules/instructions for supported IDEs.
|
|
88
|
+
* Supports: Cursor, Codex, Windsurf, Kiro
|
|
89
|
+
* Does NOT overwrite if files already exist.
|
|
90
|
+
*
|
|
91
|
+
* @param homeDir - Override for home directory (for testing)
|
|
92
|
+
*/
|
|
93
|
+
async function installGlobalRules(homeDir) {
|
|
94
|
+
const home = homeDir || os_1.default.homedir();
|
|
95
|
+
// Cursor: ~/.cursor/rules/fraim-rules.md
|
|
96
|
+
const cursorDir = path_1.default.join(home, '.cursor');
|
|
97
|
+
if (fs_1.default.existsSync(cursorDir)) {
|
|
98
|
+
installRuleFile(path_1.default.join(cursorDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Cursor global rules (~/.cursor/rules/fraim-rules.md)');
|
|
99
|
+
}
|
|
100
|
+
// Codex: ~/.codex/instructions.md
|
|
101
|
+
const codexDir = path_1.default.join(home, '.codex');
|
|
102
|
+
if (fs_1.default.existsSync(codexDir)) {
|
|
103
|
+
installRuleFile(path_1.default.join(codexDir, 'instructions.md'), FRAIM_CURSOR_RULES_CONTENT, 'Codex global instructions (~/.codex/instructions.md)');
|
|
104
|
+
}
|
|
105
|
+
// Windsurf: ~/.codeium/windsurf/rules/fraim-rules.md
|
|
106
|
+
const windsurfDir = path_1.default.join(home, '.codeium', 'windsurf');
|
|
107
|
+
if (fs_1.default.existsSync(windsurfDir)) {
|
|
108
|
+
installRuleFile(path_1.default.join(windsurfDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Windsurf global rules (~/.codeium/windsurf/rules/fraim-rules.md)');
|
|
109
|
+
}
|
|
110
|
+
// Kiro: ~/.kiro/rules/fraim-rules.md
|
|
111
|
+
const kiroDir = path_1.default.join(home, '.kiro');
|
|
112
|
+
if (fs_1.default.existsSync(kiroDir)) {
|
|
113
|
+
installRuleFile(path_1.default.join(kiroDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Kiro global rules (~/.kiro/rules/fraim-rules.md)');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Install a rule file if it doesn't already exist.
|
|
118
|
+
*/
|
|
119
|
+
function installRuleFile(filePath, content, displayName) {
|
|
120
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
121
|
+
console.log(chalk_1.default.gray(` ${displayName.split('(')[0].trim()} already exist ā skipping`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
125
|
+
fs_1.default.writeFileSync(filePath, content, 'utf8');
|
|
126
|
+
console.log(chalk_1.default.green(` ā
Installed ${displayName}`));
|
|
127
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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.ensureUserLevelDirectories = ensureUserLevelDirectories;
|
|
7
|
+
exports.syncUserLevelArtifacts = syncUserLevelArtifacts;
|
|
8
|
+
/**
|
|
9
|
+
* User-Level FRAIM Setup
|
|
10
|
+
*
|
|
11
|
+
* Ensures the user-level ~/.fraim/ directory has the minimal structure needed
|
|
12
|
+
* for FRAIM to work outside of any project:
|
|
13
|
+
*
|
|
14
|
+
* - config.json ā auth, mode, tokens (managed by setup.ts)
|
|
15
|
+
* - scripts/ ā locally-executed scripts (synced during any fraim sync)
|
|
16
|
+
* - personalized-employee/ ā user-level overrides and learnings
|
|
17
|
+
*
|
|
18
|
+
* Job/skill/rule stubs are NOT synced to user-level. The MCP proxy serves
|
|
19
|
+
* those on demand via list_fraim_jobs() and get_fraim_job(). Stubs only exist
|
|
20
|
+
* at project-level (fraim/) where CLAUDE.md tells agents to scan them on disk.
|
|
21
|
+
*
|
|
22
|
+
* Part of: User-Level FRAIM Artifacts with Local Shadow Semantics
|
|
23
|
+
*/
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
27
|
+
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
28
|
+
/**
|
|
29
|
+
* Ensure the user-level FRAIM directory structure exists.
|
|
30
|
+
* Creates personalized-employee dirs for user-level overrides.
|
|
31
|
+
* Scripts dir is handled by existing script-sync-utils during any fraim sync.
|
|
32
|
+
*/
|
|
33
|
+
function ensureUserLevelDirectories(userFraimDir) {
|
|
34
|
+
const baseDir = userFraimDir || (0, script_sync_utils_1.getUserFraimDir)();
|
|
35
|
+
const dirs = [
|
|
36
|
+
path_1.default.join(baseDir, 'personalized-employee', 'jobs'),
|
|
37
|
+
path_1.default.join(baseDir, 'personalized-employee', 'skills'),
|
|
38
|
+
path_1.default.join(baseDir, 'personalized-employee', 'rules'),
|
|
39
|
+
path_1.default.join(baseDir, 'personalized-employee', 'learnings'),
|
|
40
|
+
];
|
|
41
|
+
for (const dir of dirs) {
|
|
42
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
43
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set up the user-level FRAIM directory.
|
|
49
|
+
* Creates the personalized-employee structure so FRAIM works outside any project.
|
|
50
|
+
*
|
|
51
|
+
* @param userFraimDir - Override for the target directory (for testing)
|
|
52
|
+
*/
|
|
53
|
+
async function syncUserLevelArtifacts(userFraimDir) {
|
|
54
|
+
const baseDir = userFraimDir || (0, script_sync_utils_1.getUserFraimDir)();
|
|
55
|
+
console.log(chalk_1.default.blue('š¦ Setting up user-level FRAIM directory...'));
|
|
56
|
+
console.log(chalk_1.default.gray(` Target: ${baseDir}`));
|
|
57
|
+
ensureUserLevelDirectories(baseDir);
|
|
58
|
+
console.log(chalk_1.default.green('ā
User-level FRAIM directory ready'));
|
|
59
|
+
}
|
|
@@ -49,10 +49,22 @@ const project_fraim_paths_1 = require("./project-fraim-paths");
|
|
|
49
49
|
class LocalRegistryResolver {
|
|
50
50
|
constructor(options) {
|
|
51
51
|
this.workspaceRoot = options.workspaceRoot;
|
|
52
|
+
this.effectiveFraimDir = options.effectiveFraimDir;
|
|
52
53
|
this.remoteContentResolver = options.remoteContentResolver;
|
|
53
54
|
this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
|
|
54
55
|
this.shouldFilter = options.shouldFilter;
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Get a path within the effective FRAIM directory.
|
|
59
|
+
* When effectiveFraimDir is set (user-level mode), joins directly with that dir.
|
|
60
|
+
* Otherwise, uses the standard getWorkspaceFraimPath which prepends 'fraim/'.
|
|
61
|
+
*/
|
|
62
|
+
getFraimPath(...parts) {
|
|
63
|
+
if (this.effectiveFraimDir) {
|
|
64
|
+
return (0, path_1.join)(this.effectiveFraimDir, ...parts);
|
|
65
|
+
}
|
|
66
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, ...parts);
|
|
67
|
+
}
|
|
56
68
|
/**
|
|
57
69
|
* Check if a local override exists for the given path
|
|
58
70
|
*/
|
|
@@ -72,7 +84,7 @@ class LocalRegistryResolver {
|
|
|
72
84
|
if (this.hasLocalOverride(literal))
|
|
73
85
|
return literal;
|
|
74
86
|
// Deep search
|
|
75
|
-
const fullBaseDir =
|
|
87
|
+
const fullBaseDir = this.getFraimPath('personalized-employee', dir);
|
|
76
88
|
const found = this.searchFileRecursively(fullBaseDir, baseName);
|
|
77
89
|
if (found) {
|
|
78
90
|
// Convert absolute back to relative
|
|
@@ -116,7 +128,7 @@ class LocalRegistryResolver {
|
|
|
116
128
|
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
117
129
|
// Personal overrides are in fraim/personalized-employee/
|
|
118
130
|
// We don't need a redundant 'registry/' subfolder here as the path already includes type (e.g. jobs/)
|
|
119
|
-
return
|
|
131
|
+
return this.getFraimPath('personalized-employee', normalized);
|
|
120
132
|
}
|
|
121
133
|
/**
|
|
122
134
|
* Get the full path to a locally synced FRAIM file when available.
|
|
@@ -132,18 +144,18 @@ class LocalRegistryResolver {
|
|
|
132
144
|
if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
|
|
133
145
|
const role = parts[1];
|
|
134
146
|
const subPath = parts.slice(2).join('/');
|
|
135
|
-
return
|
|
147
|
+
return this.getFraimPath(role, 'workflows', subPath);
|
|
136
148
|
}
|
|
137
149
|
// Fallback: Try ai-employee and ai-manager if no role prefix
|
|
138
150
|
const subPath = normalizedPath.substring('workflows/'.length);
|
|
139
|
-
const employeePath =
|
|
151
|
+
const employeePath = this.getFraimPath('ai-employee', 'workflows', subPath);
|
|
140
152
|
if (fs.existsSync(employeePath))
|
|
141
153
|
return employeePath;
|
|
142
|
-
const managerPath =
|
|
154
|
+
const managerPath = this.getFraimPath('ai-manager', 'workflows', subPath);
|
|
143
155
|
if (fs.existsSync(managerPath))
|
|
144
156
|
return managerPath;
|
|
145
157
|
// Fallback for non-role-prefixed direct workspace paths
|
|
146
|
-
return
|
|
158
|
+
return this.getFraimPath(normalizedPath);
|
|
147
159
|
}
|
|
148
160
|
// 2. Jobs: jobs/[role]/path -> fraim/[role]/jobs/path
|
|
149
161
|
if (normalizedPath.startsWith('jobs/')) {
|
|
@@ -151,18 +163,18 @@ class LocalRegistryResolver {
|
|
|
151
163
|
if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
|
|
152
164
|
const role = parts[1];
|
|
153
165
|
const subPath = parts.slice(2).join('/');
|
|
154
|
-
return
|
|
166
|
+
return this.getFraimPath(role, 'jobs', subPath);
|
|
155
167
|
}
|
|
156
168
|
// Fallback: Try ai-employee and ai-manager if no role prefix
|
|
157
169
|
const subPath = normalizedPath.substring('jobs/'.length);
|
|
158
|
-
const employeePath =
|
|
170
|
+
const employeePath = this.getFraimPath('ai-employee', 'jobs', subPath);
|
|
159
171
|
if (fs.existsSync(employeePath))
|
|
160
172
|
return employeePath;
|
|
161
|
-
const managerPath =
|
|
173
|
+
const managerPath = this.getFraimPath('ai-manager', 'jobs', subPath);
|
|
162
174
|
if (fs.existsSync(managerPath))
|
|
163
175
|
return managerPath;
|
|
164
176
|
// Fallback
|
|
165
|
-
return
|
|
177
|
+
return this.getFraimPath(normalizedPath);
|
|
166
178
|
}
|
|
167
179
|
// 3. Rules: [role]/rules/path -> fraim/[role]/rules/path
|
|
168
180
|
if (normalizedPath.includes('/rules/')) {
|
|
@@ -170,16 +182,16 @@ class LocalRegistryResolver {
|
|
|
170
182
|
// Extract the part after "rules/"
|
|
171
183
|
const rulesIdx = normalizedPath.indexOf('rules/');
|
|
172
184
|
const subPath = normalizedPath.substring(rulesIdx + 'rules/'.length);
|
|
173
|
-
return
|
|
185
|
+
return this.getFraimPath(role, 'rules', subPath);
|
|
174
186
|
}
|
|
175
187
|
// 4. Skills: skills/path -> fraim/ai-employee/skills/path (default to ai-employee)
|
|
176
188
|
if (normalizedPath.startsWith('skills/')) {
|
|
177
189
|
const subPath = normalizedPath.substring('skills/'.length);
|
|
178
|
-
return
|
|
190
|
+
return this.getFraimPath('ai-employee', 'skills', subPath);
|
|
179
191
|
}
|
|
180
192
|
// 5. Rules: rules/path -> fraim/ai-employee/rules/path (default to ai-employee)
|
|
181
193
|
if (normalizedPath.startsWith('rules/')) {
|
|
182
|
-
return
|
|
194
|
+
return this.getFraimPath('ai-employee', normalizedPath);
|
|
183
195
|
}
|
|
184
196
|
return null;
|
|
185
197
|
}
|
|
@@ -580,7 +592,7 @@ class LocalRegistryResolver {
|
|
|
580
592
|
const items = [];
|
|
581
593
|
const dirs = ['jobs'];
|
|
582
594
|
for (const dir of dirs) {
|
|
583
|
-
const localDir =
|
|
595
|
+
const localDir = this.getFraimPath('personalized-employee', dir);
|
|
584
596
|
if (fs.existsSync(localDir)) {
|
|
585
597
|
const relPaths = this.collectLocalMarkdownPaths(localDir);
|
|
586
598
|
for (const rel of relPaths) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.WORKSPACE_SYNCED_CONTENT_DIRS = exports.WORKSPACE_FRAIM_DIRNAME = void 0;
|
|
4
7
|
exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
|
|
@@ -6,8 +9,11 @@ exports.workspaceFraimExists = workspaceFraimExists;
|
|
|
6
9
|
exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
|
|
7
10
|
exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
|
|
8
11
|
exports.getWorkspaceFraimDisplayPath = getWorkspaceFraimDisplayPath;
|
|
12
|
+
exports.getUserFraimDirPath = getUserFraimDirPath;
|
|
13
|
+
exports.getEffectiveFraimDir = getEffectiveFraimDir;
|
|
9
14
|
const fs_1 = require("fs");
|
|
10
15
|
const path_1 = require("path");
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
11
17
|
exports.WORKSPACE_FRAIM_DIRNAME = 'fraim';
|
|
12
18
|
exports.WORKSPACE_SYNCED_CONTENT_DIRS = [
|
|
13
19
|
'workflows',
|
|
@@ -36,3 +42,34 @@ function getWorkspaceFraimDisplayPath(relativePath = '') {
|
|
|
36
42
|
? `${exports.WORKSPACE_FRAIM_DIRNAME}/${normalized}`
|
|
37
43
|
: `${exports.WORKSPACE_FRAIM_DIRNAME}/`;
|
|
38
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the user-level FRAIM directory (~/.fraim/).
|
|
47
|
+
* Can be overridden with FRAIM_USER_DIR env var for testing.
|
|
48
|
+
*/
|
|
49
|
+
function getUserFraimDirPath() {
|
|
50
|
+
return process.env.FRAIM_USER_DIR || (0, path_1.join)(os_1.default.homedir(), '.fraim');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Determine the effective FRAIM content root directory.
|
|
54
|
+
*
|
|
55
|
+
* Shadow semantics: if the project has a local fraim/ directory, it completely
|
|
56
|
+
* shadows the user-level ~/.fraim/ ā no mixing, no layering.
|
|
57
|
+
*
|
|
58
|
+
* @param projectRoot - The project/workspace root directory
|
|
59
|
+
* @param userFraimDir - Optional override for the user-level dir (for testing)
|
|
60
|
+
* @returns The effective fraim content root directory path, or '' if neither exists
|
|
61
|
+
*/
|
|
62
|
+
function getEffectiveFraimDir(projectRoot = process.cwd(), userFraimDir) {
|
|
63
|
+
// 1. Check for project-level fraim/ directory
|
|
64
|
+
const projectFraimDir = getWorkspaceFraimDir(projectRoot);
|
|
65
|
+
if ((0, fs_1.existsSync)(projectFraimDir)) {
|
|
66
|
+
return projectFraimDir;
|
|
67
|
+
}
|
|
68
|
+
// 2. Fall back to user-level ~/.fraim/
|
|
69
|
+
const userDir = userFraimDir || getUserFraimDirPath();
|
|
70
|
+
if ((0, fs_1.existsSync)(userDir)) {
|
|
71
|
+
return userDir;
|
|
72
|
+
}
|
|
73
|
+
// 3. Neither exists
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
@@ -1216,8 +1216,16 @@ class FraimLocalMCPServer {
|
|
|
1216
1216
|
getRegistryResolver(requestSessionId) {
|
|
1217
1217
|
const projectRoot = this.findProjectRoot();
|
|
1218
1218
|
this.log(`š getRegistryResolver: projectRoot = ${projectRoot}`);
|
|
1219
|
-
|
|
1220
|
-
|
|
1219
|
+
// Determine effective FRAIM dir using shadow semantics
|
|
1220
|
+
const { getEffectiveFraimDir } = require('../core/utils/project-fraim-paths');
|
|
1221
|
+
const effectiveFraimDir = projectRoot
|
|
1222
|
+
? getEffectiveFraimDir(projectRoot)
|
|
1223
|
+
: getEffectiveFraimDir(process.cwd());
|
|
1224
|
+
if (effectiveFraimDir) {
|
|
1225
|
+
this.log(`š Effective FRAIM dir: ${effectiveFraimDir}`);
|
|
1226
|
+
}
|
|
1227
|
+
if (!projectRoot && !effectiveFraimDir) {
|
|
1228
|
+
this.log('ā ļø No project root or user-level FRAIM found, override resolution disabled');
|
|
1221
1229
|
// Return a resolver that always falls back to remote
|
|
1222
1230
|
return new local_registry_resolver_1.LocalRegistryResolver({
|
|
1223
1231
|
workspaceRoot: process.cwd(),
|
|
@@ -1228,8 +1236,15 @@ class FraimLocalMCPServer {
|
|
|
1228
1236
|
});
|
|
1229
1237
|
}
|
|
1230
1238
|
else {
|
|
1239
|
+
// Determine if we need effectiveFraimDir override.
|
|
1240
|
+
// If the effective dir is user-level (~/.fraim/), it's NOT under projectRoot/fraim/,
|
|
1241
|
+
// so we pass it as effectiveFraimDir to bypass the getWorkspaceFraimPath logic.
|
|
1242
|
+
const workspaceRoot = projectRoot || process.cwd();
|
|
1243
|
+
const projectFraimDir = projectRoot ? (0, path_1.join)(projectRoot, 'fraim') : '';
|
|
1244
|
+
const needsEffectiveDirOverride = effectiveFraimDir && effectiveFraimDir !== projectFraimDir;
|
|
1231
1245
|
return new local_registry_resolver_1.LocalRegistryResolver({
|
|
1232
|
-
workspaceRoot
|
|
1246
|
+
workspaceRoot,
|
|
1247
|
+
...(needsEffectiveDirOverride ? { effectiveFraimDir } : {}),
|
|
1233
1248
|
shouldFilter: (content) => this.isStub(content),
|
|
1234
1249
|
remoteContentResolver: async (path) => {
|
|
1235
1250
|
// Fetch parent content from remote for inheritance
|