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.
@@ -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šŸŽÆ Setup complete!'));
618
- console.log(chalk_1.default.gray(` Mode: ${mode}`));
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
- console.log(chalk_1.default.gray(` Platforms: ${configuredProviders.join(', ') || 'none'}`));
622
- }
623
- console.log(chalk_1.default.cyan('\nšŸ“ For future projects:'));
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šŸ’” To add more platforms later:'));
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(` fraim setup --${id}`));
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 = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'personalized-employee', dir);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'personalized-employee', normalized);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, role, 'workflows', subPath);
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 = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', 'workflows', subPath);
151
+ const employeePath = this.getFraimPath('ai-employee', 'workflows', subPath);
140
152
  if (fs.existsSync(employeePath))
141
153
  return employeePath;
142
- const managerPath = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-manager', 'workflows', subPath);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, normalizedPath);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, role, 'jobs', subPath);
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 = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', 'jobs', subPath);
170
+ const employeePath = this.getFraimPath('ai-employee', 'jobs', subPath);
159
171
  if (fs.existsSync(employeePath))
160
172
  return employeePath;
161
- const managerPath = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-manager', 'jobs', subPath);
173
+ const managerPath = this.getFraimPath('ai-manager', 'jobs', subPath);
162
174
  if (fs.existsSync(managerPath))
163
175
  return managerPath;
164
176
  // Fallback
165
- return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, normalizedPath);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, role, 'rules', subPath);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', 'skills', subPath);
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 (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', normalizedPath);
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 = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'personalized-employee', dir);
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
- if (!projectRoot) {
1220
- this.log('āš ļø No project root found, override resolution disabled');
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: projectRoot,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.105",
3
+ "version": "2.0.108",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {