fraim 2.0.105 ā 2.0.106
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 +20 -0
- package/dist/src/cli/commands/sync.js +15 -0
- package/dist/src/cli/setup/ide-global-integration.js +120 -0
- package/dist/src/cli/setup/user-level-sync.js +139 -0
- package/dist/src/cli/utils/remote-sync.js +17 -8
- 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)()) {
|
|
@@ -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,120 @@
|
|
|
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 ": "): call \`list_fraim_jobs()\` to discover available jobs. List each by name and its Intent line. Ask the user which job they want to run, then proceed to step 2.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
3. **Load the full content**:
|
|
30
|
+
- For jobs: call \`get_fraim_job({ job: "<matched-job-name>" })\` ā never execute from stub content.
|
|
31
|
+
- For skills: the content from \`get_fraim_file\` is the full skill. Use it directly.
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
`;
|
|
35
|
+
const FRAIM_CURSOR_RULES_CONTENT = `# FRAIM Rules
|
|
36
|
+
|
|
37
|
+
This workspace uses FRAIM (Framework for Rigor-based AI Management).
|
|
38
|
+
|
|
39
|
+
## Discovery
|
|
40
|
+
- Before acting on any user request, check if a FRAIM job matches the intent.
|
|
41
|
+
- Call \`list_fraim_jobs()\` to discover available jobs.
|
|
42
|
+
- Call \`get_fraim_job({ job: "<job-name>" })\` to get full phased instructions.
|
|
43
|
+
|
|
44
|
+
## Execution
|
|
45
|
+
- Jobs are FRAIM's primary execution units ā structured, multi-phase workflows.
|
|
46
|
+
- Skills are reusable capabilities that jobs compose.
|
|
47
|
+
- Rules are always-on constraints and conventions.
|
|
48
|
+
- Follow phased instructions and use \`seekMentoring\` at phase transitions.
|
|
49
|
+
|
|
50
|
+
## Principles
|
|
51
|
+
- Never execute from job stubs ā always load full instructions via MCP.
|
|
52
|
+
- Follow the constitution: integrity, correctness, contribution, completeness.
|
|
53
|
+
`;
|
|
54
|
+
/**
|
|
55
|
+
* Install the FRAIM slash command for Claude Code at the user level.
|
|
56
|
+
* Writes to ~/.claude/commands/fraim.md.
|
|
57
|
+
* Does NOT overwrite if the file already exists.
|
|
58
|
+
*
|
|
59
|
+
* @param homeDir - Override for home directory (for testing)
|
|
60
|
+
*/
|
|
61
|
+
async function installSlashCommands(homeDir) {
|
|
62
|
+
const home = homeDir || os_1.default.homedir();
|
|
63
|
+
const claudeDir = path_1.default.join(home, '.claude');
|
|
64
|
+
// Only install if Claude Code is installed (indicated by .claude/ existing)
|
|
65
|
+
if (!fs_1.default.existsSync(claudeDir)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const commandsDir = path_1.default.join(claudeDir, 'commands');
|
|
69
|
+
const slashCommandPath = path_1.default.join(commandsDir, 'fraim.md');
|
|
70
|
+
// Do not overwrite existing file
|
|
71
|
+
if (fs_1.default.existsSync(slashCommandPath)) {
|
|
72
|
+
console.log(chalk_1.default.gray(' Claude slash command already exists ā skipping'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
fs_1.default.mkdirSync(commandsDir, { recursive: true });
|
|
76
|
+
fs_1.default.writeFileSync(slashCommandPath, FRAIM_SLASH_COMMAND_CONTENT, 'utf8');
|
|
77
|
+
console.log(chalk_1.default.green(' ā
Installed Claude slash command (~/.claude/commands/fraim.md)'));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Install FRAIM global rules/instructions for supported IDEs.
|
|
81
|
+
* Supports: Cursor, Codex, Windsurf, Kiro
|
|
82
|
+
* Does NOT overwrite if files already exist.
|
|
83
|
+
*
|
|
84
|
+
* @param homeDir - Override for home directory (for testing)
|
|
85
|
+
*/
|
|
86
|
+
async function installGlobalRules(homeDir) {
|
|
87
|
+
const home = homeDir || os_1.default.homedir();
|
|
88
|
+
// Cursor: ~/.cursor/rules/fraim-rules.md
|
|
89
|
+
const cursorDir = path_1.default.join(home, '.cursor');
|
|
90
|
+
if (fs_1.default.existsSync(cursorDir)) {
|
|
91
|
+
installRuleFile(path_1.default.join(cursorDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Cursor global rules (~/.cursor/rules/fraim-rules.md)');
|
|
92
|
+
}
|
|
93
|
+
// Codex: ~/.codex/instructions.md
|
|
94
|
+
const codexDir = path_1.default.join(home, '.codex');
|
|
95
|
+
if (fs_1.default.existsSync(codexDir)) {
|
|
96
|
+
installRuleFile(path_1.default.join(codexDir, 'instructions.md'), FRAIM_CURSOR_RULES_CONTENT, 'Codex global instructions (~/.codex/instructions.md)');
|
|
97
|
+
}
|
|
98
|
+
// Windsurf: ~/.codeium/windsurf/rules/fraim-rules.md
|
|
99
|
+
const windsurfDir = path_1.default.join(home, '.codeium', 'windsurf');
|
|
100
|
+
if (fs_1.default.existsSync(windsurfDir)) {
|
|
101
|
+
installRuleFile(path_1.default.join(windsurfDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Windsurf global rules (~/.codeium/windsurf/rules/fraim-rules.md)');
|
|
102
|
+
}
|
|
103
|
+
// Kiro: ~/.kiro/rules/fraim-rules.md
|
|
104
|
+
const kiroDir = path_1.default.join(home, '.kiro');
|
|
105
|
+
if (fs_1.default.existsSync(kiroDir)) {
|
|
106
|
+
installRuleFile(path_1.default.join(kiroDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Kiro global rules (~/.kiro/rules/fraim-rules.md)');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Install a rule file if it doesn't already exist.
|
|
111
|
+
*/
|
|
112
|
+
function installRuleFile(filePath, content, displayName) {
|
|
113
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
114
|
+
console.log(chalk_1.default.gray(` ${displayName.split('(')[0].trim()} already exist ā skipping`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
118
|
+
fs_1.default.writeFileSync(filePath, content, 'utf8');
|
|
119
|
+
console.log(chalk_1.default.green(` ā
Installed ${displayName}`));
|
|
120
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ensureUserLevelDirectories = ensureUserLevelDirectories;
|
|
40
|
+
exports.syncUserLevelArtifacts = syncUserLevelArtifacts;
|
|
41
|
+
/**
|
|
42
|
+
* User-Level FRAIM Artifact Sync
|
|
43
|
+
*
|
|
44
|
+
* Syncs jobs, skills, rules, and docs stubs from the remote FRAIM server
|
|
45
|
+
* to the user-level ~/.fraim/ directory. This makes FRAIM discoverable
|
|
46
|
+
* everywhere without requiring fraim init-project.
|
|
47
|
+
*
|
|
48
|
+
* Part of: User-Level FRAIM Artifacts with Local Shadow Semantics
|
|
49
|
+
*/
|
|
50
|
+
const fs_1 = __importDefault(require("fs"));
|
|
51
|
+
const path_1 = __importDefault(require("path"));
|
|
52
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
53
|
+
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
54
|
+
/**
|
|
55
|
+
* Ensure the user-level FRAIM directory structure exists for content sync.
|
|
56
|
+
* Creates directories for synced content and personalized overrides.
|
|
57
|
+
*/
|
|
58
|
+
function ensureUserLevelDirectories(userFraimDir) {
|
|
59
|
+
const baseDir = userFraimDir || (0, script_sync_utils_1.getUserFraimDir)();
|
|
60
|
+
const dirs = [
|
|
61
|
+
path_1.default.join(baseDir, 'ai-employee', 'jobs'),
|
|
62
|
+
path_1.default.join(baseDir, 'ai-employee', 'skills'),
|
|
63
|
+
path_1.default.join(baseDir, 'ai-employee', 'rules'),
|
|
64
|
+
path_1.default.join(baseDir, 'ai-manager', 'jobs'),
|
|
65
|
+
path_1.default.join(baseDir, 'personalized-employee', 'jobs'),
|
|
66
|
+
path_1.default.join(baseDir, 'personalized-employee', 'skills'),
|
|
67
|
+
path_1.default.join(baseDir, 'personalized-employee', 'rules'),
|
|
68
|
+
path_1.default.join(baseDir, 'personalized-employee', 'learnings'),
|
|
69
|
+
path_1.default.join(baseDir, 'docs'),
|
|
70
|
+
];
|
|
71
|
+
for (const dir of dirs) {
|
|
72
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
73
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Sync FRAIM artifacts (jobs, skills, rules, docs) from the remote server
|
|
79
|
+
* to the user-level ~/.fraim/ directory.
|
|
80
|
+
*
|
|
81
|
+
* Uses the same remote sync endpoint as project-level sync, but writes
|
|
82
|
+
* content directly to the user-level directory structure instead of under
|
|
83
|
+
* a project's fraim/ subdirectory.
|
|
84
|
+
*
|
|
85
|
+
* @param userFraimDir - Override for the target directory (for testing)
|
|
86
|
+
*/
|
|
87
|
+
async function syncUserLevelArtifacts(userFraimDir) {
|
|
88
|
+
const baseDir = userFraimDir || (0, script_sync_utils_1.getUserFraimDir)();
|
|
89
|
+
console.log(chalk_1.default.blue('š¦ Syncing FRAIM content to user-level directory...'));
|
|
90
|
+
console.log(chalk_1.default.gray(` Target: ${baseDir}`));
|
|
91
|
+
// Ensure directory structure exists
|
|
92
|
+
ensureUserLevelDirectories(baseDir);
|
|
93
|
+
// Try to sync from remote. If this fails (no network, no key),
|
|
94
|
+
// we still have the directory structure in place.
|
|
95
|
+
try {
|
|
96
|
+
const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
|
|
97
|
+
const apiKey = loadApiKeyFromConfig(baseDir);
|
|
98
|
+
if (!apiKey) {
|
|
99
|
+
console.log(chalk_1.default.yellow('ā ļø No API key found. User-level content sync skipped.'));
|
|
100
|
+
console.log(chalk_1.default.gray(' Directory structure created. Content will sync on next fraim sync --global.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const result = await syncFromRemote({
|
|
104
|
+
apiKey,
|
|
105
|
+
projectRoot: baseDir,
|
|
106
|
+
targetIsUserLevel: true,
|
|
107
|
+
skipUpdates: true
|
|
108
|
+
});
|
|
109
|
+
if (result.success) {
|
|
110
|
+
console.log(chalk_1.default.green(`ā
Synced ${result.employeeJobsSynced} ai-employee jobs, ` +
|
|
111
|
+
`${result.managerJobsSynced} ai-manager jobs, ` +
|
|
112
|
+
`${result.skillsSynced} skills, ${result.rulesSynced} rules, ` +
|
|
113
|
+
`${result.scriptsSynced} scripts, and ${result.docsSynced} docs to ~/.fraim/`));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(chalk_1.default.yellow(`ā ļø User-level content sync failed: ${result.error}`));
|
|
117
|
+
console.log(chalk_1.default.gray(' Directory structure created. Content will sync on next fraim sync --global.'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.log(chalk_1.default.yellow(`ā ļø User-level content sync failed: ${error.message}`));
|
|
122
|
+
console.log(chalk_1.default.gray(' Directory structure created. Content will sync on next fraim sync --global.'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load API key from the user-level config file.
|
|
127
|
+
*/
|
|
128
|
+
function loadApiKeyFromConfig(userFraimDir) {
|
|
129
|
+
const configPath = path_1.default.join(userFraimDir, 'config.json');
|
|
130
|
+
if (!fs_1.default.existsSync(configPath))
|
|
131
|
+
return undefined;
|
|
132
|
+
try {
|
|
133
|
+
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
134
|
+
return config.apiKey;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -135,8 +135,17 @@ async function syncFromRemote(options) {
|
|
|
135
135
|
error: 'No files received'
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// Helper: resolve path within the FRAIM content directory.
|
|
139
|
+
// For user-level sync, write directly to projectRoot (which IS ~/.fraim/).
|
|
140
|
+
// For project-level sync, use getWorkspaceFraimPath which prepends 'fraim/'.
|
|
141
|
+
const resolveFraimPath = (...parts) => {
|
|
142
|
+
if (options.targetIsUserLevel) {
|
|
143
|
+
return (0, path_1.join)(options.projectRoot, ...parts);
|
|
144
|
+
}
|
|
145
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, ...parts);
|
|
146
|
+
};
|
|
147
|
+
const lockTargets = options.targetIsUserLevel ? [] : getSyncedContentLockTargets(options.projectRoot);
|
|
148
|
+
if (!options.targetIsUserLevel && shouldLockSyncedContent()) {
|
|
140
149
|
// If previous sync locked these paths read-only, temporarily unlock before cleanup/write.
|
|
141
150
|
for (const target of lockTargets) {
|
|
142
151
|
setFileWriteLockRecursively(target, false);
|
|
@@ -146,7 +155,7 @@ async function syncFromRemote(options) {
|
|
|
146
155
|
const allJobFiles = files.filter(f => f.type === 'job');
|
|
147
156
|
const managerJobFiles = allJobFiles.filter(f => f.path.startsWith('ai-manager/'));
|
|
148
157
|
const jobFiles = allJobFiles.filter(f => !f.path.startsWith('ai-manager/'));
|
|
149
|
-
const employeeJobsDir = (
|
|
158
|
+
const employeeJobsDir = resolveFraimPath('ai-employee', 'jobs');
|
|
150
159
|
if (!(0, fs_1.existsSync)(employeeJobsDir)) {
|
|
151
160
|
(0, fs_1.mkdirSync)(employeeJobsDir, { recursive: true });
|
|
152
161
|
}
|
|
@@ -166,7 +175,7 @@ async function syncFromRemote(options) {
|
|
|
166
175
|
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/jobs/${relPath}`)}`));
|
|
167
176
|
}
|
|
168
177
|
// Sync ai-manager job stubs to fraim/ai-manager/jobs/
|
|
169
|
-
const managerJobsDir = (
|
|
178
|
+
const managerJobsDir = resolveFraimPath('ai-manager', 'jobs');
|
|
170
179
|
if (!(0, fs_1.existsSync)(managerJobsDir)) {
|
|
171
180
|
(0, fs_1.mkdirSync)(managerJobsDir, { recursive: true });
|
|
172
181
|
}
|
|
@@ -187,7 +196,7 @@ async function syncFromRemote(options) {
|
|
|
187
196
|
}
|
|
188
197
|
// Sync skill STUBS to fraim/ai-employee/skills/
|
|
189
198
|
const skillFiles = files.filter(f => f.type === 'skill');
|
|
190
|
-
const skillsDir = (
|
|
199
|
+
const skillsDir = resolveFraimPath('ai-employee', 'skills');
|
|
191
200
|
if (!(0, fs_1.existsSync)(skillsDir)) {
|
|
192
201
|
(0, fs_1.mkdirSync)(skillsDir, { recursive: true });
|
|
193
202
|
}
|
|
@@ -207,7 +216,7 @@ async function syncFromRemote(options) {
|
|
|
207
216
|
}
|
|
208
217
|
// Sync rule STUBS to fraim/ai-employee/rules/
|
|
209
218
|
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
210
|
-
const rulesDir = (
|
|
219
|
+
const rulesDir = resolveFraimPath('ai-employee', 'rules');
|
|
211
220
|
if (!(0, fs_1.existsSync)(rulesDir)) {
|
|
212
221
|
(0, fs_1.mkdirSync)(rulesDir, { recursive: true });
|
|
213
222
|
}
|
|
@@ -246,7 +255,7 @@ async function syncFromRemote(options) {
|
|
|
246
255
|
}
|
|
247
256
|
// Sync docs to fraim/docs/
|
|
248
257
|
const docsFiles = files.filter(f => f.type === 'docs');
|
|
249
|
-
const docsDir = (
|
|
258
|
+
const docsDir = resolveFraimPath('docs');
|
|
250
259
|
if (!(0, fs_1.existsSync)(docsDir)) {
|
|
251
260
|
(0, fs_1.mkdirSync)(docsDir, { recursive: true });
|
|
252
261
|
}
|
|
@@ -260,7 +269,7 @@ async function syncFromRemote(options) {
|
|
|
260
269
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
261
270
|
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`docs/${file.path}`)}`));
|
|
262
271
|
}
|
|
263
|
-
if (shouldLockSyncedContent()) {
|
|
272
|
+
if (!options.targetIsUserLevel && shouldLockSyncedContent()) {
|
|
264
273
|
for (const target of lockTargets) {
|
|
265
274
|
setFileWriteLockRecursively(target, true);
|
|
266
275
|
}
|
|
@@ -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
|