fraim 2.0.159 → 2.0.160

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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/src/ai-hub/cert-store.js +70 -0
  3. package/dist/src/ai-hub/desktop-main.js +225 -50
  4. package/dist/src/ai-hub/manager-turns.js +38 -0
  5. package/dist/src/ai-hub/office-sideload.js +138 -0
  6. package/dist/src/ai-hub/openclaw-bridge.js +239 -0
  7. package/dist/src/ai-hub/server.js +346 -115
  8. package/dist/src/ai-hub/word-sideload.js +95 -0
  9. package/dist/src/cli/commands/add-ide.js +9 -0
  10. package/dist/src/cli/commands/login.js +1 -2
  11. package/dist/src/cli/commands/setup.js +0 -2
  12. package/dist/src/cli/commands/sync.js +19 -10
  13. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +66 -2
  14. package/dist/src/cli/doctor/checks/workflow-checks.js +1 -65
  15. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
  16. package/dist/src/cli/mcp/mcp-server-registry.js +14 -10
  17. package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
  18. package/dist/src/cli/utils/fraim-gitignore.js +11 -0
  19. package/dist/src/cli/utils/remote-sync.js +1 -1
  20. package/dist/src/core/config-loader.js +1 -2
  21. package/dist/src/core/fraim-config-schema.generated.js +0 -5
  22. package/dist/src/core/types.js +0 -1
  23. package/dist/src/first-run/session-service.js +3 -3
  24. package/package.json +2 -1
  25. package/public/ai-hub/index.html +20 -2
  26. package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
  27. package/public/ai-hub/powerpoint-taskpane/index.html +235 -0
  28. package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
  29. package/public/ai-hub/script.js +337 -120
  30. package/public/ai-hub/styles.css +456 -135
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Sideloads the Word add-in manifest so it appears in Word's Developer Add-ins
4
+ * list without admin rights or AppSource publishing.
5
+ *
6
+ * Windows: writes a registry value under HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer
7
+ * macOS: writes an entry to ~/Library/Containers/com.microsoft.Word/Data/Documents/wef/
8
+ *
9
+ * Both paths are non-admin and survive app updates (keyed by manifest GUID).
10
+ * Safe to call multiple times — checks before writing.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.isSideloaded = isSideloaded;
17
+ exports.sideloadManifest = sideloadManifest;
18
+ exports.removeSideload = removeSideload;
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const os_1 = __importDefault(require("os"));
22
+ const child_process_1 = require("child_process");
23
+ const MANIFEST_GUID = 'd1090951-50cf-4cf2-9d12-b0f8541d265c';
24
+ function resolveManifestPath(projectPath) {
25
+ const candidates = [
26
+ path_1.default.resolve(projectPath, 'extensions/office-word/manifest.xml'),
27
+ path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/manifest.xml'),
28
+ path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/manifest.xml'),
29
+ ];
30
+ return candidates.find(c => fs_1.default.existsSync(c)) ?? null;
31
+ }
32
+ function isSideloaded() {
33
+ if (process.platform === 'win32') {
34
+ const result = (0, child_process_1.spawnSync)('reg', [
35
+ 'query',
36
+ `HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer`,
37
+ '/v', MANIFEST_GUID,
38
+ ], { encoding: 'utf8' });
39
+ return result.status === 0 && result.stdout.includes(MANIFEST_GUID);
40
+ }
41
+ if (process.platform === 'darwin') {
42
+ const wefDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', 'com.microsoft.Word', 'Data', 'Documents', 'wef');
43
+ return fs_1.default.existsSync(path_1.default.join(wefDir, `${MANIFEST_GUID}.xml`));
44
+ }
45
+ return false;
46
+ }
47
+ function sideloadManifest(projectPath) {
48
+ const manifestPath = resolveManifestPath(projectPath);
49
+ if (!manifestPath) {
50
+ return { ok: false, reason: 'Manifest file not found — is extensions/office-word/ present?' };
51
+ }
52
+ if (process.platform === 'win32') {
53
+ const result = (0, child_process_1.spawnSync)('reg', [
54
+ 'add',
55
+ `HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer`,
56
+ '/v', MANIFEST_GUID,
57
+ '/t', 'REG_SZ',
58
+ '/d', manifestPath,
59
+ '/f',
60
+ ], { encoding: 'utf8' });
61
+ if (result.status !== 0) {
62
+ return { ok: false, reason: result.stderr || 'reg add failed' };
63
+ }
64
+ return { ok: true };
65
+ }
66
+ if (process.platform === 'darwin') {
67
+ const wefDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', 'com.microsoft.Word', 'Data', 'Documents', 'wef');
68
+ try {
69
+ fs_1.default.mkdirSync(wefDir, { recursive: true });
70
+ fs_1.default.copyFileSync(manifestPath, path_1.default.join(wefDir, `${MANIFEST_GUID}.xml`));
71
+ return { ok: true };
72
+ }
73
+ catch (err) {
74
+ return { ok: false, reason: String(err) };
75
+ }
76
+ }
77
+ return { ok: false, reason: `Unsupported platform: ${process.platform}` };
78
+ }
79
+ function removeSideload() {
80
+ if (process.platform === 'win32') {
81
+ (0, child_process_1.spawnSync)('reg', [
82
+ 'delete',
83
+ `HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer`,
84
+ '/v', MANIFEST_GUID,
85
+ '/f',
86
+ ], { encoding: 'utf8' });
87
+ return;
88
+ }
89
+ if (process.platform === 'darwin') {
90
+ const wefDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', 'com.microsoft.Word', 'Data', 'Documents', 'wef');
91
+ const target = path_1.default.join(wefDir, `${MANIFEST_GUID}.xml`);
92
+ if (fs_1.default.existsSync(target))
93
+ fs_1.default.unlinkSync(target);
94
+ }
95
+ }
@@ -218,12 +218,18 @@ const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
218
218
  // Merge MCP servers intelligently
219
219
  const mergedMCPServers = { ...existingMCPServers };
220
220
  const addedServers = [];
221
+ const updatedServers = [];
221
222
  const skippedServers = [];
223
+ const alwaysUpdateServers = new Set(['fraim', 'github', 'gitlab', 'jira', 'ado', 'linear']);
222
224
  for (const [serverName, serverConfig] of Object.entries(newMCPServers)) {
223
225
  if (!existingMCPServers[serverName]) {
224
226
  mergedMCPServers[serverName] = serverConfig;
225
227
  addedServers.push(serverName);
226
228
  }
229
+ else if (alwaysUpdateServers.has(serverName)) {
230
+ mergedMCPServers[serverName] = serverConfig;
231
+ updatedServers.push(serverName);
232
+ }
227
233
  else {
228
234
  skippedServers.push(serverName);
229
235
  }
@@ -239,6 +245,9 @@ const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
239
245
  addedServers.forEach(server => {
240
246
  console.log(chalk_1.default.green(` ✅ Added ${server} MCP server`));
241
247
  });
248
+ updatedServers.forEach(server => {
249
+ console.log(chalk_1.default.blue(` Updated ${server} MCP server`));
250
+ });
242
251
  skippedServers.forEach(server => {
243
252
  console.log(chalk_1.default.gray(` ⏭️ Skipped ${server} (already exists)`));
244
253
  });
@@ -11,7 +11,6 @@ const provider_client_1 = require("../api/provider-client");
11
11
  const script_sync_utils_1 = require("../utils/script-sync-utils");
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
- const version_utils_1 = require("../utils/version-utils");
15
14
  const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
16
15
  exports.loginCommand = new commander_1.Command('login')
17
16
  .description('Login to platforms (GitHub, etc.)');
@@ -51,7 +50,7 @@ exports.loginCommand
51
50
  ...(config.tokens || {}),
52
51
  github: token
53
52
  };
54
- config.version = config.version || (0, version_utils_1.getFraimVersion)();
53
+ delete config.version;
55
54
  config.updatedAt = new Date().toISOString();
56
55
  fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
57
56
  console.log(chalk_1.default.green('\n✅ GitHub token saved to global configuration.'));
@@ -44,7 +44,6 @@ const prompts_1 = __importDefault(require("prompts"));
44
44
  const fs_1 = __importDefault(require("fs"));
45
45
  const path_1 = __importDefault(require("path"));
46
46
  const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
47
- const version_utils_1 = require("../utils/version-utils");
48
47
  const provider_client_1 = require("../api/provider-client");
49
48
  const provider_prompts_1 = require("../setup/provider-prompts");
50
49
  const provider_registry_1 = require("../providers/provider-registry");
@@ -225,7 +224,6 @@ const saveGlobalConfig = (fraimKey, mode, tokens, configs) => {
225
224
  };
226
225
  });
227
226
  const config = {
228
- version: (0, version_utils_1.getFraimVersion)(),
229
227
  apiKey: fraimKey,
230
228
  mode: mode,
231
229
  tokens: {
@@ -84,22 +84,30 @@ function loadUserApiKey() {
84
84
  return undefined;
85
85
  }
86
86
  }
87
- function updateVersionInConfig(fraimDir) {
87
+ function writeSyncMetadata(fraimDir, mode, remoteUrl) {
88
+ fs_1.default.mkdirSync(fraimDir, { recursive: true });
89
+ fs_1.default.writeFileSync(path_1.default.join(fraimDir, '.sync-metadata.json'), JSON.stringify({
90
+ localVersion: (0, version_utils_1.getFraimVersion)(),
91
+ mode,
92
+ remoteUrl,
93
+ syncedAt: new Date().toISOString()
94
+ }, null, 2), 'utf8');
95
+ }
96
+ function removeLegacyVersionFromConfig(fraimDir) {
88
97
  const configPath = path_1.default.join(fraimDir, 'config.json');
89
98
  if (!fs_1.default.existsSync(configPath)) {
90
99
  return;
91
100
  }
92
101
  try {
93
102
  const currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
94
- const newVersion = (0, version_utils_1.getFraimVersion)();
95
- if (currentConfig.version !== newVersion) {
96
- currentConfig.version = newVersion;
103
+ if (Object.prototype.hasOwnProperty.call(currentConfig, 'version')) {
104
+ delete currentConfig.version;
97
105
  fs_1.default.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
98
- console.log(chalk_1.default.green(`Updated FRAIM version to ${newVersion} in config.`));
106
+ console.log(chalk_1.default.green('Removed legacy config.version from config.json.'));
99
107
  }
100
108
  }
101
109
  catch {
102
- console.warn(chalk_1.default.yellow('Could not update version in config.json.'));
110
+ console.warn(chalk_1.default.yellow('Could not remove legacy version from config.json.'));
103
111
  }
104
112
  }
105
113
  function failSync(mode, message) {
@@ -141,7 +149,7 @@ const runSync = async (options) => {
141
149
  if (isGlobal && !options.skipUpdates) {
142
150
  console.log(chalk_1.default.yellow('You are running a global installation of FRAIM.'));
143
151
  console.log(chalk_1.default.gray('Updates are not automatic in this mode.'));
144
- console.log(chalk_1.default.cyan('Recommended: Use "npx fraim-framework@latest sync" instead.\n'));
152
+ console.log(chalk_1.default.cyan('Recommended: Use "npx fraim@latest sync" instead.\n'));
145
153
  }
146
154
  const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
147
155
  // Allow `FRAIM_LOCAL_SYNC=1` to flip into local-mode without needing
@@ -163,7 +171,8 @@ const runSync = async (options) => {
163
171
  });
164
172
  if (result.success) {
165
173
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
166
- updateVersionInConfig(fraimDir);
174
+ removeLegacyVersionFromConfig(fraimDir);
175
+ writeSyncMetadata(fraimDir, 'local', localUrl);
167
176
  refreshLocalIgnoreConfig();
168
177
  const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
169
178
  if (adapterUpdates.length > 0) {
@@ -210,13 +219,13 @@ const runSync = async (options) => {
210
219
  console.error(chalk_1.default.yellow('Check your API key and network connection.'));
211
220
  if (process.env.TEST_MODE === 'true') {
212
221
  console.log(chalk_1.default.yellow('TEST_MODE: Continuing without remote sync (server may be unavailable).'));
213
- updateVersionInConfig(fraimDir);
214
222
  return;
215
223
  }
216
224
  failSync(failHard, `Remote sync failed: ${result.error}`);
217
225
  }
218
226
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
219
- updateVersionInConfig(fraimDir);
227
+ removeLegacyVersionFromConfig(fraimDir);
228
+ writeSyncMetadata(fraimDir, 'remote', config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
220
229
  refreshLocalIgnoreConfig();
221
230
  if (config.repository?.provider === 'github' && (0, github_workflow_sync_1.isGitHubWorkflowAutomationEnabled)(config)) {
222
231
  const workflowBundle = await (0, github_workflow_sync_1.fetchGitHubWorkflowBundle)({
@@ -41,13 +41,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  return (mod && mod.__esModule) ? mod : { "default": mod };
42
42
  };
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.getNpmMajorVersion = getNpmMajorVersion;
45
+ exports.diagnoseFraimMcpLaunchPlan = diagnoseFraimMcpLaunchPlan;
44
46
  exports.getMCPConnectivityChecks = getMCPConnectivityChecks;
45
47
  const fs_1 = __importDefault(require("fs"));
46
48
  const path_1 = __importDefault(require("path"));
47
49
  const os_1 = __importDefault(require("os"));
50
+ const child_process_1 = require("child_process");
48
51
  const axios_1 = __importDefault(require("axios"));
49
52
  const toml = __importStar(require("toml"));
50
53
  const ide_detector_1 = require("../../setup/ide-detector");
54
+ const fraim_mcp_latest_launcher_1 = require("../../mcp/fraim-mcp-latest-launcher");
55
+ function getNpmMajorVersion() {
56
+ try {
57
+ const command = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'npm';
58
+ const args = process.platform === 'win32' ? ['/d', '/s', '/c', 'npm --version'] : ['--version'];
59
+ const output = (0, child_process_1.execFileSync)(command, args, {
60
+ encoding: 'utf8',
61
+ stdio: ['ignore', 'pipe', 'ignore']
62
+ });
63
+ const major = Number.parseInt(output.trim().split('.')[0], 10);
64
+ return Number.isFinite(major) ? major : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function diagnoseFraimMcpLaunchPlan(fraimServer, platform = process.platform, npmMajorVersion = getNpmMajorVersion()) {
71
+ const command = String(fraimServer?.command || '');
72
+ const args = Array.isArray(fraimServer?.args) ? fraimServer.args.map(String) : [];
73
+ const usesDirectLatest = command.toLowerCase().includes('npx')
74
+ && args.some((arg) => arg === 'fraim-framework@latest' || arg === 'fraim@latest');
75
+ if (usesDirectLatest) {
76
+ const isKnownBrokenWindowsNpm = platform === 'win32' && npmMajorVersion !== null && npmMajorVersion >= 11;
77
+ return {
78
+ status: isKnownBrokenWindowsNpm ? 'error' : 'warning',
79
+ message: isKnownBrokenWindowsNpm
80
+ ? 'FRAIM MCP config uses direct npx @latest, which is fragile on Windows npm 11'
81
+ : 'FRAIM MCP config uses direct npx @latest',
82
+ suggestion: 'Run: fraim add-ide to regenerate MCP config with the FRAIM latest launcher',
83
+ details: {
84
+ launchPlanSource: 'direct-npx-latest',
85
+ platform,
86
+ npmMajorVersion,
87
+ args
88
+ }
89
+ };
90
+ }
91
+ const launcherPath = (0, fraim_mcp_latest_launcher_1.getFraimMcpLatestLauncherPath)();
92
+ const usesLatestLauncher = command === process.execPath && args.length > 0 && path_1.default.resolve(args[0]) === path_1.default.resolve(launcherPath);
93
+ if (usesLatestLauncher) {
94
+ return {
95
+ status: 'passed',
96
+ message: 'FRAIM MCP config uses the latest launcher',
97
+ details: {
98
+ launchPlanSource: 'fraim-latest-launcher'
99
+ }
100
+ };
101
+ }
102
+ return null;
103
+ }
51
104
  /**
52
105
  * Test FRAIM connectivity by calling fraim_connect
53
106
  */
@@ -325,6 +378,16 @@ async function validateIDEMCPConfig(ide) {
325
378
  details: { configPath }
326
379
  };
327
380
  }
381
+ const launchPlanDiagnostic = diagnoseFraimMcpLaunchPlan(fraimServer);
382
+ if (launchPlanDiagnostic && launchPlanDiagnostic.status !== 'passed') {
383
+ return {
384
+ ...launchPlanDiagnostic,
385
+ details: {
386
+ ...launchPlanDiagnostic.details,
387
+ configPath
388
+ }
389
+ };
390
+ }
328
391
  // Check for auth header (for HTTP servers)
329
392
  if (fraimServer.url) {
330
393
  const hasAuth = fraimServer.headers?.Authorization || fraimServer.http_headers?.Authorization;
@@ -339,10 +402,11 @@ async function validateIDEMCPConfig(ide) {
339
402
  }
340
403
  return {
341
404
  status: 'passed',
342
- message: `${ide.name} MCP config valid`,
405
+ message: launchPlanDiagnostic?.message || `${ide.name} MCP config valid`,
343
406
  details: {
344
407
  configPath,
345
- serverCount: Object.keys(servers).length
408
+ serverCount: Object.keys(servers).length,
409
+ ...(launchPlanDiagnostic?.details || {})
346
410
  }
347
411
  };
348
412
  }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Job checks for FRAIM doctor command
4
- * Validates job installation and versions
4
+ * Validates job installation
5
5
  * Issue #144: Enhanced doctor command
6
6
  */
7
7
  var __importDefault = (this && this.__importDefault) || function (mod) {
@@ -100,69 +100,6 @@ function checkJobStubsPresent() {
100
100
  }
101
101
  };
102
102
  }
103
- /**
104
- * Check if jobs are up to date
105
- */
106
- function checkJobsUpToDate() {
107
- return {
108
- name: 'Jobs up to date',
109
- category: 'jobs',
110
- critical: false,
111
- run: async () => {
112
- const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(process.cwd());
113
- if (!fs_1.default.existsSync(configPath)) {
114
- return {
115
- status: 'warning',
116
- message: 'Cannot check job version - config missing'
117
- };
118
- }
119
- try {
120
- const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
121
- const possiblePaths = [
122
- path_1.default.join(__dirname, '../../../../package.json'),
123
- path_1.default.join(__dirname, '../../../package.json'),
124
- path_1.default.join(process.cwd(), 'package.json')
125
- ];
126
- let packageJson = null;
127
- for (const pkgPath of possiblePaths) {
128
- if (fs_1.default.existsSync(pkgPath)) {
129
- packageJson = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
130
- break;
131
- }
132
- }
133
- if (!packageJson) {
134
- return {
135
- status: 'warning',
136
- message: 'Cannot determine package version'
137
- };
138
- }
139
- const currentVersion = packageJson.version;
140
- const configVersion = config.version;
141
- if (configVersion === currentVersion) {
142
- return {
143
- status: 'passed',
144
- message: `Jobs up to date (v${currentVersion})`,
145
- details: { version: currentVersion }
146
- };
147
- }
148
- return {
149
- status: 'warning',
150
- message: `Jobs outdated (v${configVersion} -> v${currentVersion})`,
151
- suggestion: 'Update local job stubs with fraim sync.',
152
- command: 'fraim sync',
153
- details: { configVersion, currentVersion }
154
- };
155
- }
156
- catch (error) {
157
- return {
158
- status: 'error',
159
- message: 'Failed to check job version',
160
- details: { error: error.message }
161
- };
162
- }
163
- }
164
- };
165
- }
166
103
  /**
167
104
  * Check if override syntax is valid
168
105
  */
@@ -245,7 +182,6 @@ function getJobChecks() {
245
182
  return [
246
183
  checkJobsDirectoryExists(),
247
184
  checkJobStubsPresent(),
248
- checkJobsUpToDate(),
249
185
  checkOverrideSyntaxValid()
250
186
  ];
251
187
  }
@@ -0,0 +1,136 @@
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.getFraimMcpLatestLauncherPath = getFraimMcpLatestLauncherPath;
7
+ exports.ensureFraimMcpLatestLauncher = ensureFraimMcpLatestLauncher;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const LAUNCHER_VERSION = 1;
12
+ const launcherSource = `#!/usr/bin/env node
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const LAUNCHER_VERSION = ${LAUNCHER_VERSION};
19
+ const USER_DIR = process.env.FRAIM_USER_DIR || path.join(os.homedir(), '.fraim');
20
+ const STATE_DIR = path.join(USER_DIR, 'mcp-launcher');
21
+ const STATE_PATH = path.join(STATE_DIR, 'latest.json');
22
+ const CACHE_ROOT = path.join(USER_DIR, 'npm-cache', 'mcp');
23
+
24
+ const commandName = (name) => process.platform === 'win32' ? name + '.cmd' : name;
25
+ const npmCommand = commandName('npm');
26
+ const npxCommand = commandName('npx');
27
+ const cliArgs = process.argv.slice(2).length > 0 ? process.argv.slice(2) : ['mcp'];
28
+
29
+ function quoteCmdArg(arg) {
30
+ const value = String(arg);
31
+ return /[\\s"&|<>^]/.test(value) ? '"' + value.replace(/"/g, '""') + '"' : value;
32
+ }
33
+
34
+ function runCommand(command, args, options) {
35
+ if (process.platform !== 'win32') {
36
+ return spawnSync(command, args, options);
37
+ }
38
+
39
+ return spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', [command, ...args.map(quoteCmdArg)].join(' ')], options);
40
+ }
41
+
42
+ function ensureDir(dir) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+
46
+ function readState() {
47
+ try {
48
+ return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function writeState(state) {
55
+ ensureDir(STATE_DIR);
56
+ fs.writeFileSync(STATE_PATH, JSON.stringify({ ...state, launcherVersion: LAUNCHER_VERSION, updatedAt: new Date().toISOString() }, null, 2));
57
+ }
58
+
59
+ function resolveLatest(packageName) {
60
+ const result = runCommand(npmCommand, ['view', packageName, 'version', '--silent'], {
61
+ encoding: 'utf8',
62
+ env: process.env
63
+ });
64
+
65
+ if (result.status !== 0) {
66
+ return null;
67
+ }
68
+
69
+ const version = String(result.stdout || '').trim();
70
+ return /^\\d+\\.\\d+\\.\\d+/.test(version) ? version : null;
71
+ }
72
+
73
+ function resolvePackage() {
74
+ const fraimVersion = resolveLatest('fraim');
75
+ if (fraimVersion) {
76
+ const state = { packageName: 'fraim', binName: 'fraim', version: fraimVersion, fromCache: false };
77
+ writeState(state);
78
+ return state;
79
+ }
80
+
81
+ const cached = readState();
82
+ if (cached && cached.packageName === 'fraim' && cached.binName === 'fraim' && cached.version) {
83
+ console.error('[fraim-mcp-launcher] Could not resolve npm latest; using cached fraim@' + cached.version + '.');
84
+ return { packageName: 'fraim', binName: 'fraim', version: cached.version, fromCache: true };
85
+ }
86
+
87
+ console.error('[fraim-mcp-launcher] Could not resolve fraim latest from npm and no cached fraim version exists.');
88
+ process.exit(1);
89
+ }
90
+
91
+ function runPackage(plan, cacheDir) {
92
+ ensureDir(cacheDir);
93
+ const env = { ...process.env, npm_config_cache: cacheDir };
94
+ const result = runCommand(npxCommand, ['--yes', '--package', plan.packageName + '@' + plan.version, plan.binName, ...cliArgs], {
95
+ stdio: 'inherit',
96
+ env
97
+ });
98
+ return typeof result.status === 'number' ? result.status : 1;
99
+ }
100
+
101
+ const plan = resolvePackage();
102
+ const versionCacheDir = path.join(CACHE_ROOT, plan.packageName + '-' + plan.version.replace(/[^a-zA-Z0-9._-]/g, '_'));
103
+ let status = runPackage(plan, versionCacheDir);
104
+
105
+ if (status !== 0 && process.platform === 'win32') {
106
+ const retryCache = path.join(CACHE_ROOT, 'retry-' + Date.now() + '-' + process.pid);
107
+ console.error('[fraim-mcp-launcher] Package execution failed; retrying once with a fresh npm cache.');
108
+ status = runPackage(plan, retryCache);
109
+ }
110
+
111
+ process.exit(status);
112
+ `;
113
+ function getFraimMcpLatestLauncherPath() {
114
+ return path_1.default.join(process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim'), 'bin', 'fraim-mcp-latest.js');
115
+ }
116
+ function ensureFraimMcpLatestLauncher() {
117
+ const launcherPath = getFraimMcpLatestLauncherPath();
118
+ const launcherDir = path_1.default.dirname(launcherPath);
119
+ fs_1.default.mkdirSync(launcherDir, { recursive: true });
120
+ if (!fs_1.default.existsSync(launcherPath) || fs_1.default.readFileSync(launcherPath, 'utf8') !== launcherSource) {
121
+ fs_1.default.writeFileSync(launcherPath, launcherSource, 'utf8');
122
+ if (process.platform !== 'win32') {
123
+ try {
124
+ fs_1.default.chmodSync(launcherPath, 0o755);
125
+ }
126
+ catch {
127
+ // Best effort only. IDE configs invoke this through node, so chmod is not required.
128
+ }
129
+ }
130
+ }
131
+ return {
132
+ command: process.execPath,
133
+ args: [launcherPath],
134
+ path: launcherPath
135
+ };
136
+ }
@@ -10,6 +10,7 @@ exports.isHTTPServer = isHTTPServer;
10
10
  exports.buildAllBaseServers = buildAllBaseServers;
11
11
  const provider_registry_1 = require("../providers/provider-registry");
12
12
  const command_resolution_1 = require("./command-resolution");
13
+ const fraim_mcp_latest_launcher_1 = require("./fraim-mcp-latest-launcher");
13
14
  exports.BASE_MCP_SERVERS = [
14
15
  {
15
16
  id: 'git',
@@ -33,16 +34,19 @@ exports.BASE_MCP_SERVERS = [
33
34
  id: 'fraim',
34
35
  name: 'FRAIM',
35
36
  description: 'FRAIM job orchestration and mentoring',
36
- buildServer: (fraimKey) => ({
37
- command: (0, command_resolution_1.resolveManagedCommand)('npx'),
38
- args: ['-y', 'fraim-framework@latest', 'mcp'],
39
- env: {
40
- // Include API key for IDE configs (Codex, VSCode, etc.)
41
- // The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
42
- FRAIM_API_KEY: fraimKey,
43
- FRAIM_REMOTE_URL: 'https://fraim.wellnessatwork.me'
44
- }
45
- })
37
+ buildServer: (fraimKey) => {
38
+ const launcher = (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
39
+ return {
40
+ command: launcher.command,
41
+ args: launcher.args,
42
+ env: {
43
+ // Include API key for IDE configs (Codex, VSCode, etc.)
44
+ // The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
45
+ FRAIM_API_KEY: fraimKey,
46
+ FRAIM_REMOTE_URL: 'https://fraim.wellnessatwork.me'
47
+ }
48
+ };
49
+ }
46
50
  }
47
51
  ];
48
52
  // ============================================================================
@@ -338,7 +338,7 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
338
338
  console.log(chalk_1.default.blue('\n🎯 Next steps:'));
339
339
  console.log(chalk_1.default.cyan(' 1. Restart your configured IDEs'));
340
340
  console.log(chalk_1.default.cyan(' 2. Go to any project directory'));
341
- console.log(chalk_1.default.cyan(' 3. Run: npx fraim-framework@latest init-project'));
341
+ console.log(chalk_1.default.cyan(' 3. Run: npx fraim@latest init-project'));
342
342
  console.log(chalk_1.default.cyan(' 4. Tell your AI agent: "Onboard this project"'));
343
343
  }
344
344
  };
@@ -12,6 +12,7 @@ exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
12
12
  'fraim/ai-employee/',
13
13
  'fraim/ai-manager/',
14
14
  'fraim/docs/',
15
+ 'fraim/.sync-metadata.json',
15
16
  ];
16
17
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
18
  const managedBlockPattern = new RegExp(`\\n?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
@@ -43,6 +44,16 @@ function resolveGitInfoExcludePath(projectRoot) {
43
44
  const gitDir = path_1.default.isAbsolute(match[1])
44
45
  ? match[1]
45
46
  : path_1.default.resolve(projectRoot, match[1]);
47
+ const commonDirPath = path_1.default.join(gitDir, 'commondir');
48
+ if (fs_1.default.existsSync(commonDirPath)) {
49
+ const commonDirRaw = fs_1.default.readFileSync(commonDirPath, 'utf8').trim();
50
+ if (commonDirRaw.length > 0) {
51
+ const commonDir = path_1.default.isAbsolute(commonDirRaw)
52
+ ? commonDirRaw
53
+ : path_1.default.resolve(gitDir, commonDirRaw);
54
+ return path_1.default.join(commonDir, 'info', 'exclude');
55
+ }
56
+ }
46
57
  return path_1.default.join(gitDir, 'info', 'exclude');
47
58
  }
48
59
  const removeFraimSyncedContentGitignoreBlock = (projectRoot) => {
@@ -31,8 +31,8 @@ function shouldLockSyncedContent() {
31
31
  }
32
32
  function getSyncedContentLockTargets(projectRoot) {
33
33
  return fraim_gitignore_1.FRAIM_SYNC_GITIGNORE_ENTRIES
34
+ .filter((entry) => /[\\/]$/.test(entry))
34
35
  .map((entry) => entry.replace(/[\\/]+$/, ''))
35
- .filter((entry) => entry.length > 0)
36
36
  .map((entry) => (0, path_1.join)(projectRoot, entry));
37
37
  }
38
38
  function setFileWriteLockRecursively(dirPath, readOnly) {
@@ -88,7 +88,6 @@ function normalizeAutomation(config) {
88
88
  function normalizeFraimConfig(config) {
89
89
  // Handle backward compatibility and migration
90
90
  const mergedConfig = {
91
- version: config.version || types_1.DEFAULT_FRAIM_CONFIG.version,
92
91
  project: {
93
92
  ...types_1.DEFAULT_FRAIM_CONFIG.project,
94
93
  ...(config.project || {})
@@ -154,7 +153,7 @@ function loadFraimConfig(configPath = (0, project_fraim_paths_1.getWorkspaceConf
154
153
  const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
155
154
  const config = JSON.parse(configContent);
156
155
  const mergedConfig = normalizeFraimConfig(config);
157
- console.log(`Loaded FRAIM config from ${displayPath} (version ${mergedConfig.version})`);
156
+ console.log(`Loaded FRAIM config from ${displayPath}`);
158
157
  if (config.git && !config.repository) {
159
158
  console.warn('Deprecated: "git" config detected. Consider migrating to "repository" config.');
160
159
  }