fraim 2.0.176 → 2.0.179

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.
@@ -110,6 +110,16 @@ function loadPersonaCapabilityModule() {
110
110
  function getProtectedPersonaForHubJob(jobName) {
111
111
  return loadPersonaCapabilityModule()?.getProtectedPersonaForJob(jobName) ?? null;
112
112
  }
113
+ const FRAIM_INTERNAL_JOB_IDS = new Set([
114
+ 'contribute-to-fraim',
115
+ 'create-registry-asset',
116
+ 'extract-ashley-learnings',
117
+ 'file-fraim-issue',
118
+ 'praise-fraim',
119
+ 'run-on-remote-hub',
120
+ 'setup-remote-hub',
121
+ 'update-registry-override',
122
+ ]);
113
123
  function listHubPersonaBundles() {
114
124
  return loadPersonaCapabilityModule()?.listPersonaCapabilityBundles() ?? [];
115
125
  }
@@ -147,6 +157,9 @@ function createDefaultDbService() {
147
157
  return undefined;
148
158
  }
149
159
  }
160
+ function supportsManagerSeatAssignments(dbService) {
161
+ return typeof dbService.countHubManagerAssignments === 'function';
162
+ }
150
163
  class AiHubRunRegistry {
151
164
  constructor() {
152
165
  this.runs = new Map();
@@ -1246,6 +1259,23 @@ class AiHubServer {
1246
1259
  this.app.get('/api/health', (_req, res) => {
1247
1260
  res.json({ status: 'ok', service: 'fraim-ai-hub' });
1248
1261
  });
1262
+ // Issue #659: Inbound auth middleware for remote-exposed hubs.
1263
+ // Activated only when FRAIM_HUB_AUTH_TOKEN env var is set. Gates all
1264
+ // /api/ai-hub/* routes. /health and /api/health are registered above
1265
+ // this block and are never gated.
1266
+ if (process.env.FRAIM_HUB_AUTH_TOKEN) {
1267
+ const expectedToken = process.env.FRAIM_HUB_AUTH_TOKEN;
1268
+ this.app.use('/api/ai-hub', (req, res, next) => {
1269
+ const token = req.headers['x-hub-auth'];
1270
+ if (typeof token !== 'string' ||
1271
+ token.length !== expectedToken.length ||
1272
+ !(0, crypto_1.timingSafeEqual)(Buffer.from(token), Buffer.from(expectedToken))) {
1273
+ res.status(401).json({ error: 'Unauthorized' });
1274
+ return;
1275
+ }
1276
+ next();
1277
+ });
1278
+ }
1249
1279
  this.registerRoutes();
1250
1280
  }
1251
1281
  getApp() {
@@ -1363,7 +1393,9 @@ class AiHubServer {
1363
1393
  // Issue #566 (R7): jobs already carry `personalized` from catalog discovery
1364
1394
  // (true for the fraim/personalized-employee layer). The Hub renders a plain
1365
1395
  // "Personalized" marking from that flag — no author/attribution is tracked.
1366
- const jobs = rawJobs.map((job) => ({
1396
+ const jobs = rawJobs
1397
+ .filter((job) => !FRAIM_INTERNAL_JOB_IDS.has(job.id))
1398
+ .map((job) => ({
1367
1399
  ...job,
1368
1400
  requiredPersonaKey: getProtectedPersonaForHubJob(job.id),
1369
1401
  }));
@@ -1853,6 +1885,23 @@ class AiHubServer {
1853
1885
  if (!state) {
1854
1886
  return { personas: fallbackPersonas, subscriptionActive: false, workspaceId: null, userKey: null };
1855
1887
  }
1888
+ // Legacy bypass: persona gating is not active for this workspace, so all personas are accessible.
1889
+ // Mirrors the evaluatePersonaAccess legacy bypass in persona-entitlement-service.ts.
1890
+ if (!state.subscriptionActive) {
1891
+ const legacySeatCount = supportsManagerSeatAssignments(this.dbService) ? 0 : 1;
1892
+ const allPersonas = allBundles.map((bundle) => ({
1893
+ key: bundle.personaKey,
1894
+ displayName: bundle.catalogMetadata.displayName,
1895
+ role: bundle.catalogMetadata.role,
1896
+ avatarUrl: buildHubPersonaAvatarUrl(bundle.personaKey),
1897
+ pricingLabel: '',
1898
+ status: 'hired',
1899
+ hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
1900
+ seatCount: legacySeatCount,
1901
+ seatsInUse: 0,
1902
+ }));
1903
+ return { personas: allPersonas, subscriptionActive: false, workspaceId: state.workspaceId, userKey: state.userId ?? null };
1904
+ }
1856
1905
  const hiredKeys = new Set(state.entitlements
1857
1906
  .filter((e) => e.status === 'active')
1858
1907
  .map((e) => e.personaKey));
@@ -33,67 +33,79 @@ const saveGlobalConfig = (config) => {
33
33
  const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
34
34
  fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
35
35
  };
36
- const updateIDEConfigs = async (provider, config) => {
36
+ const forEachInstalledIDE = async (callback) => {
37
37
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
38
38
  if (detectedIDEs.length === 0) {
39
39
  console.log(chalk_1.default.yellow('⚠️ No IDEs detected. MCP configs not updated.'));
40
40
  return;
41
41
  }
42
42
  console.log(chalk_1.default.blue(`\n🔧 Updating ${detectedIDEs.length} IDE configuration(s)...\n`));
43
- const tokenConfig = {
44
- github: config.tokens?.github,
45
- gitlab: config.tokens?.gitlab,
46
- ado: config.tokens?.ado,
47
- jira: config.tokens?.jira
48
- };
49
- const providerConfigs = Object.entries(config.providerConfigs || {}).reduce((acc, [key, value]) => {
50
- const providerId = key.replace(/Config$/, '');
51
- acc[providerId] = value;
52
- return acc;
53
- }, {});
54
43
  for (const ide of detectedIDEs) {
55
44
  try {
56
- const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
57
- if (ide.configFormat === 'json') {
58
- // JSON-based config (Claude Desktop, Cursor, etc.)
59
- let ideConfig = {};
60
- if (fs_1.default.existsSync(configPath)) {
61
- ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
62
- }
63
- const mcpConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, providerConfigs);
64
- const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
65
- if (!ideConfig[serversKey]) {
66
- ideConfig[serversKey] = {};
67
- }
68
- // Add the new provider's MCP server
69
- const providerServerKey = provider === 'ado' ? 'ado' : provider;
70
- if (mcpConfig[serversKey] && mcpConfig[serversKey][providerServerKey]) {
71
- ideConfig[serversKey][providerServerKey] = mcpConfig[serversKey][providerServerKey];
72
- fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
73
- console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
74
- }
75
- }
76
- else if (ide.configFormat === 'toml') {
77
- // TOML-based config (Codex, Zed)
78
- const mcpConfigToml = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, providerConfigs);
79
- if (fs_1.default.existsSync(configPath)) {
80
- const existingContent = fs_1.default.readFileSync(configPath, 'utf8');
81
- // Merge only the new provider's server
82
- const serversToMerge = [provider === 'ado' ? 'ado' : provider];
83
- const mergeResult = (0, mcp_config_generator_2.mergeTomlMCPServers)(existingContent, mcpConfigToml, serversToMerge);
84
- fs_1.default.writeFileSync(configPath, mergeResult.content);
85
- console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
86
- }
87
- else {
88
- console.log(chalk_1.default.yellow(`⚠️ ${ide.name} config not found, skipping`));
89
- }
90
- }
45
+ await callback(ide, (0, ide_detector_1.expandPath)(ide.configPath));
91
46
  }
92
47
  catch (error) {
93
48
  console.log(chalk_1.default.red(`❌ Failed to update ${ide.name}: ${error instanceof Error ? error.message : 'Unknown error'}`));
94
49
  }
95
50
  }
96
51
  };
52
+ const writeOAuthProviderToIDEConfigs = async (provider, mcpUrl) => {
53
+ const urlOnlyEntry = { type: 'http', url: mcpUrl };
54
+ await forEachInstalledIDE(async (ide, configPath) => {
55
+ if (ide.configFormat === 'json') {
56
+ let ideConfig = {};
57
+ if (fs_1.default.existsSync(configPath)) {
58
+ ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
59
+ }
60
+ const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
61
+ if (!ideConfig[serversKey]) {
62
+ ideConfig[serversKey] = {};
63
+ }
64
+ ideConfig[serversKey][provider] = urlOnlyEntry;
65
+ fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
66
+ console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
67
+ }
68
+ else if (ide.configFormat === 'toml') {
69
+ console.log(chalk_1.default.yellow(`⚠️ ${ide.name} uses TOML format — update manually: add [tools.${provider}] url = "${mcpUrl}"`));
70
+ }
71
+ });
72
+ };
73
+ const updateIDEConfigs = async (provider, fraimKey, token, providerConfig) => {
74
+ const tokenConfig = { [provider]: token };
75
+ const providerConfigs = providerConfig ? { [provider]: providerConfig } : {};
76
+ await forEachInstalledIDE(async (ide, configPath) => {
77
+ if (ide.configFormat === 'json') {
78
+ let ideConfig = {};
79
+ if (fs_1.default.existsSync(configPath)) {
80
+ ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
81
+ }
82
+ const mcpConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokenConfig, providerConfigs);
83
+ const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
84
+ if (!ideConfig[serversKey]) {
85
+ ideConfig[serversKey] = {};
86
+ }
87
+ const providerServerKey = provider === 'ado' ? 'ado' : provider;
88
+ if (mcpConfig[serversKey] && mcpConfig[serversKey][providerServerKey]) {
89
+ ideConfig[serversKey][providerServerKey] = mcpConfig[serversKey][providerServerKey];
90
+ fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
91
+ console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
92
+ }
93
+ }
94
+ else if (ide.configFormat === 'toml') {
95
+ const mcpConfigToml = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokenConfig, providerConfigs);
96
+ if (fs_1.default.existsSync(configPath)) {
97
+ const existingContent = fs_1.default.readFileSync(configPath, 'utf8');
98
+ const serversToMerge = [provider === 'ado' ? 'ado' : provider];
99
+ const mergeResult = (0, mcp_config_generator_2.mergeTomlMCPServers)(existingContent, mcpConfigToml, serversToMerge);
100
+ fs_1.default.writeFileSync(configPath, mergeResult.content);
101
+ console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
102
+ }
103
+ else {
104
+ console.log(chalk_1.default.yellow(`⚠️ ${ide.name} config not found, skipping`));
105
+ }
106
+ }
107
+ });
108
+ };
97
109
  const offerModeSwitch = async (config) => {
98
110
  if (config.mode === 'split') {
99
111
  console.log(chalk_1.default.gray('\nAlready in split mode.'));
@@ -186,6 +198,19 @@ const runAddProvider = async (provider, options) => {
186
198
  }
187
199
  }
188
200
  }
201
+ // Detect OAuth-first providers: HTTP MCP with no authHeaderTemplate means the IDE
202
+ // handles OAuth natively. Skip credential collection and write a URL-only IDE entry.
203
+ const providerDefOAuth = await (0, provider_registry_1.getProvider)(provider);
204
+ if (providerDefOAuth?.mcpServer?.type === 'http' &&
205
+ !providerDefOAuth.mcpServer.authHeaderTemplate) {
206
+ const mcpUrl = providerDefOAuth.mcpServer.url;
207
+ await writeOAuthProviderToIDEConfigs(provider, mcpUrl);
208
+ console.log(chalk_1.default.green(`\n✅ ${providerName} configured for IDE-native OAuth.`));
209
+ console.log(chalk_1.default.cyan('\n💡 Next steps:'));
210
+ console.log(chalk_1.default.gray(' 1. Restart your IDE(s) to load the new MCP server'));
211
+ console.log(chalk_1.default.gray(' 2. On first use, your IDE will prompt you to sign in to ' + providerName));
212
+ return;
213
+ }
189
214
  // Get credentials using generic prompt system
190
215
  try {
191
216
  // Build provided tokens/configs from CLI options
@@ -205,20 +230,8 @@ const runAddProvider = async (provider, options) => {
205
230
  // Get credentials using generic prompt system
206
231
  const client = (0, get_provider_client_1.getProviderClient)();
207
232
  const creds = await (0, provider_prompts_1.promptForProviderCredentials)(client, provider, providedTokens[provider], providedConfigs[provider]);
208
- // Save token
209
- config.tokens[provider] = creds.token;
210
- // Save provider-specific config if present
211
- if (creds.config) {
212
- if (!config.providerConfigs) {
213
- config.providerConfigs = {};
214
- }
215
- config.providerConfigs[`${provider}Config`] = creds.config;
216
- }
217
- // Save updated config
218
- saveGlobalConfig(config);
219
- console.log(chalk_1.default.green(`\n✅ ${providerName} credentials saved to global config`));
220
- // Update IDE configs
221
- await updateIDEConfigs(provider, config);
233
+ // Write provider MCP entry directly to IDE configs (token not stored in global config)
234
+ await updateIDEConfigs(provider, config.apiKey, creds.token, creds.config);
222
235
  // Offer mode switch if adding a provider with issues capability
223
236
  const providerDef = await (0, provider_registry_1.getProvider)(provider);
224
237
  if (providerDef?.capabilities.includes('issues') && provider === 'jira') {
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * fraim add-surface — Register FRAIM as an MCP server on Track B Claude surfaces.
4
+ *
5
+ * Track B surfaces connect via remote HTTPS MCP (not local stdio):
6
+ * - claude.ai web, Claude Design, Claude Cowork (remote mode), Claude mobile
7
+ * - Claude in Word, Excel, PowerPoint, Outlook (via admin manifest)
8
+ *
9
+ * Track A surfaces (local stdio) are handled by `fraim add-ide` instead.
10
+ *
11
+ * Auth model: OAuth 2.1 PKCE. claude.ai discovers endpoints via RFC 8414
12
+ * (/.well-known/oauth-authorization-server) and drives the full authorize/token
13
+ * flow automatically — no manual API key entry required.
14
+ * Office add-ins still use a direct gateway token via the claude-in-office CLI.
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.addSurfaceCommand = void 0;
21
+ const commander_1 = require("commander");
22
+ const chalk_1 = __importDefault(require("chalk"));
23
+ const FRAIM_REMOTE_URL = 'https://fraim.wellnessatwork.me';
24
+ const MCP_ENDPOINT = `${FRAIM_REMOTE_URL}/mcp`;
25
+ // ─── surface handlers ────────────────────────────────────────────────────────
26
+ function printClaudeWebInstructions(surface) {
27
+ const label = {
28
+ 'claude.ai': 'claude.ai web',
29
+ 'design': 'Claude Design',
30
+ 'cowork': 'Claude Cowork (remote mode)',
31
+ 'mobile': 'Claude iOS / Android',
32
+ };
33
+ const name = label[surface] || surface;
34
+ console.log(chalk_1.default.bold(`\n ${name} — Remote MCP Connector`));
35
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────'));
36
+ console.log(chalk_1.default.white(' 1. Open claude.ai → profile icon → Settings → Connectors'));
37
+ console.log(chalk_1.default.white(' 2. Click "+ Add Custom Connector"'));
38
+ console.log(chalk_1.default.white(' 3. Enter this URL:'));
39
+ console.log(chalk_1.default.cyan(`\n ${MCP_ENDPOINT}\n`));
40
+ console.log(chalk_1.default.white(' 4. Click "Connect" — FRAIM uses OAuth sign-in (Google or GitHub).'));
41
+ console.log(chalk_1.default.white(' If you are already logged in to fraimworks.ai, the connector'));
42
+ console.log(chalk_1.default.white(' completes automatically with no extra steps.'));
43
+ console.log(chalk_1.default.white(' 5. The connector syncs to mobile and Claude Design automatically.'));
44
+ console.log(chalk_1.default.gray('\n Note: Settings > Connectors is the same for web, mobile, and Claude Design.'));
45
+ console.log(chalk_1.default.gray(` Discovery: ${FRAIM_REMOTE_URL}/.well-known/oauth-authorization-server`));
46
+ }
47
+ function printOfficeInstructions(surface) {
48
+ const label = {
49
+ 'word': 'Claude in Word',
50
+ 'excel': 'Claude in Excel',
51
+ 'powerpoint': 'Claude in PowerPoint',
52
+ 'outlook': 'Claude in Outlook',
53
+ };
54
+ const name = label[surface] || surface;
55
+ console.log(chalk_1.default.bold(`\n ${name} — Claude add-in (sign in with your Claude account)`));
56
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────'));
57
+ console.log(chalk_1.default.white(' The Claude Office add-in is a client-side panel you install from'));
58
+ console.log(chalk_1.default.white(' Microsoft AppSource and sign into with your claude.ai account.'));
59
+ console.log(chalk_1.default.white(' Connectors are per-account, so the FRAIM connector you configured'));
60
+ console.log(chalk_1.default.white(' on claude.ai is automatically available here — no extra setup needed.'));
61
+ console.log(chalk_1.default.white('\n Steps:'));
62
+ console.log(chalk_1.default.white(' 1. Open Word / Excel / PowerPoint'));
63
+ console.log(chalk_1.default.white(' 2. Insert → Add-ins → search for "Claude"'));
64
+ console.log(chalk_1.default.white(' 3. Sign in with the same account you used on claude.ai'));
65
+ console.log(chalk_1.default.white(' 4. FRAIM tools are available immediately via the connector'));
66
+ console.log(chalk_1.default.gray('\n Docs: https://support.claude.com/en/articles/13945233'));
67
+ }
68
+ function printDesktopBridgeNote() {
69
+ console.log(chalk_1.default.gray('\n Note: Claude Desktop and Claude Cowork (local mode) use Track A (local stdio).'));
70
+ console.log(chalk_1.default.gray(' Run `fraim add-ide` for those instead.'));
71
+ }
72
+ // ─── supported surfaces ───────────────────────────────────────────────────────
73
+ const WEB_SURFACES = ['claude.ai', 'design', 'cowork', 'mobile'];
74
+ const OFFICE_SURFACES = ['word', 'excel', 'powerpoint', 'outlook'];
75
+ const ALL_SURFACES = [...WEB_SURFACES, ...OFFICE_SURFACES];
76
+ // ─── command action ───────────────────────────────────────────────────────────
77
+ async function runAddSurface(options) {
78
+ if (options.list) {
79
+ console.log(chalk_1.default.bold('\nTrack B surfaces (remote HTTPS MCP):'));
80
+ console.log(chalk_1.default.cyan(' Web / Mobile'));
81
+ WEB_SURFACES.forEach(s => console.log(chalk_1.default.white(` • ${s}`)));
82
+ console.log(chalk_1.default.cyan('\n Microsoft 365 (admin manifest required)'));
83
+ OFFICE_SURFACES.forEach(s => console.log(chalk_1.default.white(` • ${s}`)));
84
+ console.log(chalk_1.default.gray('\nTrack A surfaces (local stdio — use `fraim add-ide` instead):'));
85
+ console.log(chalk_1.default.gray(' claude-desktop, claude-code, claude-cowork (local), cursor, vscode, codex, gemini, kiro, windsurf'));
86
+ return;
87
+ }
88
+ const targets = options.all
89
+ ? ALL_SURFACES
90
+ : options.surface
91
+ ? [options.surface.toLowerCase()]
92
+ : [];
93
+ if (targets.length === 0) {
94
+ console.log(chalk_1.default.bold('\nUsage: fraim add-surface --surface <name> or --all or --list\n'));
95
+ console.log(chalk_1.default.white('Available surfaces (Track B — remote HTTPS MCP):'));
96
+ console.log(chalk_1.default.cyan(' Web/mobile: claude.ai | design | cowork | mobile'));
97
+ console.log(chalk_1.default.cyan(' Office: word | excel | powerpoint | outlook'));
98
+ console.log(chalk_1.default.gray('\nExample: fraim add-surface --surface claude.ai'));
99
+ console.log(chalk_1.default.gray(' fraim add-surface --all'));
100
+ return;
101
+ }
102
+ const unknown = targets.filter(t => !ALL_SURFACES.includes(t));
103
+ if (unknown.length > 0) {
104
+ console.log(chalk_1.default.red(`✗ Unknown surface(s): ${unknown.join(', ')}`));
105
+ console.log(chalk_1.default.yellow(` Valid values: ${ALL_SURFACES.join(', ')}`));
106
+ process.exit(1);
107
+ }
108
+ console.log(chalk_1.default.bold('\nFRAIM MCP — Surface Registration Instructions'));
109
+ console.log(chalk_1.default.gray(`Remote endpoint: ${MCP_ENDPOINT}`));
110
+ const webTargets = targets.filter(t => WEB_SURFACES.includes(t));
111
+ const officeTargets = targets.filter(t => OFFICE_SURFACES.includes(t));
112
+ webTargets.forEach(s => printClaudeWebInstructions(s));
113
+ officeTargets.forEach(s => printOfficeInstructions(s));
114
+ if (targets.some(t => ['cowork', 'claude-desktop'].includes(t))) {
115
+ printDesktopBridgeNote();
116
+ }
117
+ console.log(chalk_1.default.bold('\n MCP endpoint details:'));
118
+ console.log(chalk_1.default.white(` URL: ${MCP_ENDPOINT}`));
119
+ console.log(chalk_1.default.white(` Auth: OAuth 2.1 PKCE (Google / GitHub sign-in via fraimworks.ai)`));
120
+ console.log(chalk_1.default.white(` Discovery: ${FRAIM_REMOTE_URL}/.well-known/oauth-authorization-server`));
121
+ console.log(chalk_1.default.gray('\n Run `fraim doctor` to verify local (Track A) configuration.'));
122
+ }
123
+ exports.addSurfaceCommand = new commander_1.Command('add-surface')
124
+ .description('Register FRAIM as a remote MCP server on Track B Claude surfaces (claude.ai, Design, Cowork, Office add-ins)')
125
+ .option('--surface <name>', 'Target surface: claude.ai | design | cowork | mobile | word | excel | powerpoint | outlook')
126
+ .option('--all', 'Print instructions for all Track B surfaces')
127
+ .option('--list', 'List supported surfaces')
128
+ .action(runAddSurface);
@@ -65,11 +65,9 @@ function openBrowser(url) {
65
65
  }
66
66
  }
67
67
  const runFirstRun = async (options) => {
68
- const key = options.key || process.env.FRAIM_API_KEY || process.env.FRAIM_SETUP_KEY || process.env.FRAIM_INSTALL_KEY;
69
- if (!key) {
70
- console.log(chalk_1.default.red('FRAIM key is required for first-run.'));
71
- process.exit(1);
72
- }
68
+ // Issue #646: the key is optional. When launched by the no-terminal macOS
69
+ // installer (.pkg), no key is passed — the wizard prompts the user to paste it.
70
+ const key = options.key || process.env.FRAIM_API_KEY || process.env.FRAIM_SETUP_KEY || process.env.FRAIM_INSTALL_KEY || '';
73
71
  const sessionService = new session_service_1.FirstRunSessionService({
74
72
  key,
75
73
  headless: options.headless,
@@ -6,78 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loginCommand = void 0;
7
7
  const commander_1 = require("commander");
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
- const device_flow_service_1 = require("../internal/device-flow-service");
10
- const provider_client_1 = require("../api/provider-client");
11
- const script_sync_utils_1 = require("../utils/script-sync-utils");
12
- const fs_1 = __importDefault(require("fs"));
13
- const path_1 = __importDefault(require("path"));
14
- const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
15
9
  exports.loginCommand = new commander_1.Command('login')
16
10
  .description('Login to platforms (GitHub, etc.)');
17
11
  exports.loginCommand
18
12
  .command('github')
19
- .description('Login to GitHub using Device Flow')
13
+ .description('Configure GitHub MCP access (redirects to add-provider)')
20
14
  .action(async () => {
21
- // Load the token to global config
22
- const globalConfigDir = (0, script_sync_utils_1.getUserFraimDir)();
23
- const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
24
- let config = {};
25
- if (fs_1.default.existsSync(globalConfigPath)) {
26
- config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
27
- }
28
- if (!config.apiKey) {
29
- console.error(chalk_1.default.red('\n❌ No FRAIM API key found. Please run "fraim setup" first.'));
30
- process.exit(1);
31
- }
32
- const client = new provider_client_1.ProviderClient(config.apiKey);
33
- let githubConfig;
34
- try {
35
- const providers = await client.getAllProviders();
36
- githubConfig = providers.find(p => p.id === 'github')?.deviceFlowConfig;
37
- if (!githubConfig) {
38
- console.error(chalk_1.default.red('\n❌ Device flow configuration for GitHub not found on server.'));
39
- process.exit(1);
40
- }
41
- }
42
- catch (error) {
43
- console.error(chalk_1.default.red(`\n❌ Failed to fetch provider configuration: ${error.message}`));
44
- process.exit(1);
45
- }
46
- const deviceFlow = new device_flow_service_1.DeviceFlowService(githubConfig);
47
- try {
48
- const token = await deviceFlow.login();
49
- config.tokens = {
50
- ...(config.tokens || {}),
51
- github: token
52
- };
53
- delete config.version;
54
- config.updatedAt = new Date().toISOString();
55
- fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
56
- console.log(chalk_1.default.green('\n✅ GitHub token saved to global configuration.'));
57
- // Trigger MCP reconfiguration
58
- if (config.apiKey) {
59
- console.log(chalk_1.default.blue('🔌 Updating MCP server configurations...'));
60
- const mcpTokens = {};
61
- Object.entries(config.tokens).forEach(([id, t]) => {
62
- mcpTokens[id] = t;
63
- });
64
- // Re-use existing provider configs if any
65
- const providerConfigsMap = {};
66
- if (config.providerConfigs) {
67
- Object.entries(config.providerConfigs).forEach(([key, val]) => {
68
- const providerId = key.replace('Config', '');
69
- providerConfigsMap[providerId] = val;
70
- });
71
- }
72
- await (0, auto_mcp_setup_1.autoConfigureMCP)(config.apiKey, mcpTokens, undefined, providerConfigsMap);
73
- console.log(chalk_1.default.green('✅ MCP servers updated with new GitHub token.'));
74
- }
75
- else {
76
- console.log(chalk_1.default.yellow('⚠️ No FRAIM API key found. Run "fraim setup" to complete configuration.'));
77
- }
78
- }
79
- catch (error) {
80
- console.error(chalk_1.default.red(`\n❌ Login failed: ${error.message}`));
81
- process.exit(1);
82
- }
15
+ console.log(chalk_1.default.blue('\n💡 GitHub authentication is now handled natively by your IDE.'));
16
+ console.log(chalk_1.default.gray('To configure GitHub MCP access, run:'));
17
+ console.log(chalk_1.default.cyan('\n fraim add-provider github\n'));
18
+ console.log(chalk_1.default.gray('Your IDE will prompt for GitHub sign-in automatically on first use.'));
83
19
  });