fraim-framework 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.
@@ -44,17 +44,7 @@ const prompts_1 = __importDefault(require("prompts"));
44
44
  const ide_detector_1 = require("./ide-detector");
45
45
  const mcp_config_generator_1 = require("./mcp-config-generator");
46
46
  const codex_local_config_1 = require("./codex-local-config");
47
- const provider_registry_1 = require("../providers/provider-registry");
48
- const normalizePlatformTokens = (tokenInput) => {
49
- if (!tokenInput)
50
- return {};
51
- if (typeof tokenInput === 'string') {
52
- return tokenInput ? { github: tokenInput } : {};
53
- }
54
- return tokenInput;
55
- };
56
- const promptForIDESelection = async (detectedIDEs, tokenInput) => {
57
- const tokens = normalizePlatformTokens(tokenInput);
47
+ const promptForIDESelection = async (detectedIDEs) => {
58
48
  if (process.env.FRAIM_NON_INTERACTIVE) {
59
49
  console.log(chalk_1.default.yellow(`\nℹ️ Non-interactive mode: configuring all detected IDEs (${detectedIDEs.length})`));
60
50
  return detectedIDEs;
@@ -70,17 +60,8 @@ const promptForIDESelection = async (detectedIDEs, tokenInput) => {
70
60
  console.log(chalk_1.default.blue('FRAIM will add these MCP servers to selected IDEs:'));
71
61
  console.log(chalk_1.default.gray(' • fraim (required for FRAIM jobs)'));
72
62
  console.log(chalk_1.default.gray(' • git (version control integration)'));
73
- // Show configured provider MCP servers dynamically
74
- const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
75
- for (const providerId of allProviderIds) {
76
- if (tokens[providerId]) {
77
- const provider = await (0, provider_registry_1.getProvider)(providerId);
78
- if (provider) {
79
- console.log(chalk_1.default.gray(` - ${providerId} (${provider.displayName} API access)`));
80
- }
81
- }
82
- }
83
63
  console.log(chalk_1.default.gray(' • playwright (browser automation)'));
64
+ console.log(chalk_1.default.blue('\n💡 Run "fraim add-provider <provider>" after setup to connect GitHub, GitLab, Jira, or other tools.'));
84
65
  console.log(chalk_1.default.yellow('\n💡 Existing MCP servers will be preserved - only missing servers will be added.'));
85
66
  const response = await (0, prompts_1.default)({
86
67
  type: 'text',
@@ -153,8 +134,7 @@ const validateSetupResults = async (configuredIDEs) => {
153
134
  }
154
135
  };
155
136
  exports.validateSetupResults = validateSetupResults;
156
- const configureIDEMCP = async (ide, fraimKey, tokenInput, providerConfigs) => {
157
- const tokens = normalizePlatformTokens(tokenInput);
137
+ const configureIDEMCP = async (ide, fraimKey) => {
158
138
  const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
159
139
  console.log(chalk_1.default.blue(`🔧 Configuring ${ide.name}...`));
160
140
  // Create backup
@@ -185,11 +165,9 @@ const configureIDEMCP = async (ide, fraimKey, tokenInput, providerConfigs) => {
185
165
  existingTomlContent = fs_1.default.readFileSync(configPath, 'utf8');
186
166
  console.log(chalk_1.default.gray(` 📋 Found existing TOML config`));
187
167
  }
188
- const newTomlContent = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokens, providerConfigs);
168
+ const newTomlContent = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, {});
189
169
  const { getAllMCPServerIds } = await Promise.resolve().then(() => __importStar(require('../mcp/mcp-server-registry')));
190
- const baseServerIds = getAllMCPServerIds();
191
- const providerServerIds = Object.keys(tokens).filter(id => tokens[id]);
192
- const serversToAdd = [...baseServerIds, ...providerServerIds];
170
+ const serversToAdd = getAllMCPServerIds();
193
171
  const mergeResult = (0, mcp_config_generator_1.mergeTomlMCPServers)(existingTomlContent, newTomlContent, serversToAdd);
194
172
  fs_1.default.writeFileSync(configPath, mergeResult.content);
195
173
  mergeResult.addedServers.forEach(server => {
@@ -204,7 +182,7 @@ const configureIDEMCP = async (ide, fraimKey, tokenInput, providerConfigs) => {
204
182
  }
205
183
  else {
206
184
  // For JSON configs - intelligent merging
207
- const newConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokens, providerConfigs);
185
+ const newConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, {});
208
186
  const newMCPServers = newConfig[serversKey] || newConfig.mcpServers || {};
209
187
  // Merge MCP servers intelligently
210
188
  const mergedMCPServers = { ...existingMCPServers };
@@ -251,8 +229,7 @@ const configureIDEMCP = async (ide, fraimKey, tokenInput, providerConfigs) => {
251
229
  console.log(chalk_1.default.green(` ✅ ${status} local Codex config: ${localResult.path}`));
252
230
  }
253
231
  };
254
- const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConfigs) => {
255
- const tokens = normalizePlatformTokens(tokenInput);
232
+ const autoConfigureMCP = async (fraimKey, selectedIDEs) => {
256
233
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
257
234
  if (detectedIDEs.length === 0 && (!selectedIDEs || selectedIDEs.length === 0)) {
258
235
  console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected.'));
@@ -294,7 +271,7 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
294
271
  }
295
272
  else {
296
273
  // Interactive selection
297
- idesToConfigure = await (0, exports.promptForIDESelection)(detectedIDEs, tokens);
274
+ idesToConfigure = await (0, exports.promptForIDESelection)(detectedIDEs);
298
275
  }
299
276
  if (idesToConfigure.length === 0) {
300
277
  console.log(chalk_1.default.yellow('⚠️ No IDEs selected for configuration.'));
@@ -308,7 +285,7 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
308
285
  };
309
286
  for (const ide of idesToConfigure) {
310
287
  try {
311
- await configureIDEMCP(ide, fraimKey, tokens, providerConfigs);
288
+ await configureIDEMCP(ide, fraimKey);
312
289
  results.successful.push(ide.name);
313
290
  }
314
291
  catch (error) {
@@ -23,6 +23,58 @@ exports.expandPath = expandPath;
23
23
  const checkMultiplePaths = (paths) => {
24
24
  return paths.some(p => fs_1.default.existsSync(expandPath(p)));
25
25
  };
26
+ // Issue #646 (Bug 2): a config directory left behind after uninstall (e.g. ~/.cursor,
27
+ // ~/Library/Application Support/Cursor) made config-surface detection report Cursor
28
+ // as "Installed" when it was not. App evidence is the reliable signal for Cursor;
29
+ // other GUI surfaces keep their existing config-surface checks for compatibility
30
+ // with the #640 first-run detection contract.
31
+ const appBundlePaths = (appName) => [
32
+ `/Applications/${appName}.app`,
33
+ `~/Applications/${appName}.app`,
34
+ ];
35
+ const windowsUserAppPaths = (appName) => {
36
+ switch (appName) {
37
+ case 'Cursor':
38
+ return [
39
+ '~/AppData/Local/Programs/Cursor/Cursor.exe',
40
+ '~/AppData/Local/Cursor/Cursor.exe',
41
+ ];
42
+ default:
43
+ return [];
44
+ }
45
+ };
46
+ // Issue #646 follow-up: a user may run a GUI app from a non-standard location
47
+ // (e.g. straight from a mounted DMG: /Volumes/.../Cursor.app) that the
48
+ // /Applications checks miss. A running process of the app is definitive proof it
49
+ // is installed and in use, and a stale config dir alone has no such process — so
50
+ // this catches real installs without re-introducing the stale-dir false positive.
51
+ const isMacAppRunning = (appName) => {
52
+ if (process.platform !== 'darwin')
53
+ return false;
54
+ // Test seam: `pgrep` is system-global (not HOME-scoped), so tests that simulate
55
+ // "app not installed" via a temp HOME set this to keep results deterministic.
56
+ if (process.env.FRAIM_DETECT_DISABLE_PROCESS_CHECK === '1')
57
+ return false;
58
+ const result = (0, child_process_1.spawnSync)('pgrep', ['-f', `${appName}.app/Contents`], { encoding: 'utf8', timeout: 1500 });
59
+ return result.status === 0 && Boolean((result.stdout || '').trim());
60
+ };
61
+ const hasAppEvidence = (appName) => {
62
+ const evidencePaths = process.platform === 'win32'
63
+ ? [...appBundlePaths(appName), ...windowsUserAppPaths(appName)]
64
+ : appBundlePaths(appName);
65
+ return checkMultiplePaths(evidencePaths) || isMacAppRunning(appName);
66
+ };
67
+ const guiAppDetect = (configSurfaceCheck, appName, options = {}) => {
68
+ return () => {
69
+ if (options.requireAppEvidence) {
70
+ return hasAppEvidence(appName);
71
+ }
72
+ if (process.platform === 'darwin') {
73
+ return hasAppEvidence(appName);
74
+ }
75
+ return configSurfaceCheck();
76
+ };
77
+ };
26
78
  const availableByVersionProbe = (command) => {
27
79
  const result = process.platform === 'win32'
28
80
  ? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500 })
@@ -48,12 +100,12 @@ const detectVSCode = () => {
48
100
  ];
49
101
  return checkMultiplePaths(paths);
50
102
  };
51
- const detectCursor = () => {
103
+ const detectCursorMcpConfig = () => {
52
104
  const paths = [
53
- '~/.cursor',
54
- '~/Library/Application Support/Cursor',
55
- '~/AppData/Roaming/Cursor',
56
- '~/.config/Cursor'
105
+ '~/.cursor/mcp.json',
106
+ '~/Library/Application Support/Cursor/mcp.json',
107
+ '~/AppData/Roaming/Cursor/mcp.json',
108
+ '~/.config/Cursor/mcp.json'
57
109
  ];
58
110
  return checkMultiplePaths(paths);
59
111
  };
@@ -71,6 +123,7 @@ const detectGeminiCli = () => {
71
123
  };
72
124
  const detectGeminiSurface = () => {
73
125
  const paths = [
126
+ '~/.gemini',
74
127
  '~/.gemini/settings.json',
75
128
  '~/AppData/Roaming/gemini/settings.json',
76
129
  '~/.config/gemini/settings.json'
@@ -112,7 +165,7 @@ exports.IDE_CONFIGS = [
112
165
  configFormat: 'json',
113
166
  configType: 'claude',
114
167
  invocationProfile: 'launch-phrase',
115
- detectMethod: detectClaude,
168
+ detectMethod: guiAppDetect(detectClaude, 'Claude'),
116
169
  aliases: ['claude', 'claude-desktop', 'claude desktop', 'claude-cowork', 'cowork', 'claude cowork'],
117
170
  alternativePaths: [
118
171
  '~/Library/Application Support/Claude/claude_desktop_config.json'
@@ -126,9 +179,9 @@ exports.IDE_CONFIGS = [
126
179
  configFormat: 'json',
127
180
  configType: 'standard',
128
181
  invocationProfile: 'cursor-mention',
129
- detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
182
+ detectMethod: guiAppDetect(() => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')), 'Antigravity'),
130
183
  description: 'Google Gemini Antigravity IDE',
131
- downloadUrl: 'https://deepmind.google/technologies/gemini/',
184
+ downloadUrl: 'https://antigravity.google/',
132
185
  },
133
186
  {
134
187
  name: 'Gemini CLI',
@@ -152,7 +205,7 @@ exports.IDE_CONFIGS = [
152
205
  configFormat: 'json',
153
206
  configType: 'kiro',
154
207
  invocationProfile: 'kiro-hashtag',
155
- detectMethod: () => fs_1.default.existsSync(expandPath('~/.kiro')),
208
+ detectMethod: guiAppDetect(() => fs_1.default.existsSync(expandPath('~/.kiro')), 'Kiro'),
156
209
  description: 'Kiro AI-powered IDE',
157
210
  downloadUrl: 'https://kiro.dev/',
158
211
  },
@@ -163,7 +216,7 @@ exports.IDE_CONFIGS = [
163
216
  configType: 'kiro',
164
217
  adapterConfigType: 'cursor',
165
218
  invocationProfile: 'cursor-mention',
166
- detectMethod: detectCursor,
219
+ detectMethod: () => detectCursorMcpConfig() || hasAppEvidence('Cursor'),
167
220
  alternativePaths: [
168
221
  '~/Library/Application Support/Cursor/mcp.json',
169
222
  '~/AppData/Roaming/Cursor/mcp.json',
@@ -182,7 +235,7 @@ exports.IDE_CONFIGS = [
182
235
  configFormat: 'json',
183
236
  configType: 'vscode',
184
237
  invocationProfile: 'vscode-prompt',
185
- detectMethod: detectVSCode,
238
+ detectMethod: guiAppDetect(detectVSCode, 'Visual Studio Code'),
186
239
  alternativePaths: [
187
240
  '~/Library/Application Support/Code/User/mcp.json',
188
241
  '~/AppData/Roaming/Code/User/mcp.json',
@@ -219,7 +272,7 @@ exports.IDE_CONFIGS = [
219
272
  configFormat: 'json',
220
273
  configType: 'windsurf',
221
274
  invocationProfile: 'windsurf-command',
222
- detectMethod: detectWindsurf,
275
+ detectMethod: guiAppDetect(detectWindsurf, 'Windsurf'),
223
276
  alternativePaths: [
224
277
  '~/Library/Application Support/Windsurf/mcp_config.json',
225
278
  '~/AppData/Roaming/Windsurf/mcp_config.json',
@@ -40,7 +40,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
40
40
  personaKey: 'beza',
41
41
  bundleId: 'persona-beza-core',
42
42
  catalogMetadata: buildCatalogMetadata('beza', ['competitive-analysis', 'review-business-strategy', 'business-plan-creation']),
43
- protectedJobs: ['competitive-analysis', 'review-business-strategy', 'business-plan-creation', 'problem-statement-crystallization', 'business-idea-validation-and-scoping', 'founder-market-fit-analysis', 'rfp-response-preparation', 'aws-activate-credits-application', 'community-funding-preparation', 'fundraising-prospect-discovery', 'google-cloud-credits-application', 'investor-pitch-preparation', 'microsoft-azure-credits-application', 'review-funding-preparation', 'advisory-board-development', 'advisor-interview', 'advisory-board-selection'],
43
+ protectedJobs: ['competitive-analysis', 'review-business-strategy', 'business-plan-creation', 'problem-statement-crystallization', 'business-idea-validation-and-scoping', 'founder-market-fit-analysis', 'community-funding-preparation', 'fundraising-prospect-discovery', 'investor-pitch-preparation', 'review-funding-preparation', 'advisory-board-development', 'advisor-interview', 'advisory-board-selection', 'blue-sky-brainstorming', 'domain-registration-research'],
44
44
  protectedAliases: ['business-strategy', 'company-strategy'],
45
45
  defaultHireMode: 'job',
46
46
  lockCopy: 'Hire BeZa to unlock business strategy work for this request.'
@@ -49,7 +49,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
49
49
  personaKey: 'pam',
50
50
  bundleId: 'persona-pam-core',
51
51
  catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'technical-design', 'issue-preparation']),
52
- protectedJobs: ['feature-specification', 'technical-design', 'issue-preparation', 'implementation-design-review', 'issue-retrospective', 'work-completion', 'scrum-sprint-planning', 'mvp-validation-plan'],
52
+ protectedJobs: ['feature-specification', 'technical-design', 'issue-preparation', 'implementation-design-review', 'issue-retrospective', 'work-completion', 'scrum-sprint-planning', 'mvp-validation-plan', 'sprint-planning', 'customer-prospect-discovery', 'interview-preparation', 'participant-recruitment', 'process-interview-notes', 'review-customer-development', 'triage-customer-needs'],
53
53
  protectedAliases: ['product-management', 'product-spec'],
54
54
  defaultHireMode: 'job',
55
55
  lockCopy: 'Hire PaM to unlock product-management work for this request.'
@@ -58,7 +58,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
58
58
  personaKey: 'swen',
59
59
  bundleId: 'persona-swen-core',
60
60
  catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'code-refactoring', 'pr-iteration']),
61
- protectedJobs: ['feature-implementation', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design'],
61
+ protectedJobs: ['feature-implementation', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
62
62
  protectedAliases: ['software-engineering', 'implementation'],
63
63
  defaultHireMode: 'job',
64
64
  lockCopy: 'Hire SWEn to unlock software-engineering delivery for this request.'
@@ -76,7 +76,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
76
76
  personaKey: 'huxley',
77
77
  bundleId: 'persona-huxley-core',
78
78
  catalogMetadata: buildCatalogMetadata('huxley', ['design-system-creation', 'user-facing-prototyping', 'ux-research-synthesis']),
79
- protectedJobs: ['design-system-creation', 'user-facing-prototyping', 'website-creation', 'linkedin-carousel-from-deck', 'brand-creation', 'branding-quality-audit', 'ux-research-synthesis', 'user-journey-mapping'],
79
+ protectedJobs: ['design-system-creation', 'user-facing-prototyping', 'website-creation', 'brand-creation', 'branding-quality-audit', 'ux-research-synthesis', 'user-journey-mapping'],
80
80
  protectedAliases: ['ux-design', 'brand-design'],
81
81
  defaultHireMode: 'job',
82
82
  lockCopy: 'Hire hUXley to unlock design-system, UX research, and brand design work for this request.'
@@ -85,7 +85,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
85
85
  personaKey: 'gautam',
86
86
  bundleId: 'persona-gautam-core',
87
87
  catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', 'ppc-campaign-management']),
88
- protectedJobs: ['pricing-strategy-definition', 'marketing-strategy-definition', 'product-launch-management', 'customer-prospect-discovery', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'marketing-analytics-review'],
88
+ protectedJobs: ['pricing-strategy-definition', 'marketing-strategy-definition', 'product-launch-management', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'marketing-analytics-review', 'linkedin-company-page-setup', 'x-account-setup', 'social-engagement-campaign', 'thought-leadership-engagement', 'promo-video-creation', 'linkedin-carousel-from-deck'],
89
89
  protectedAliases: ['gtm', 'marketing'],
90
90
  defaultHireMode: 'job',
91
91
  lockCopy: 'Hire GauTaM to unlock go-to-market and paid media work for this request.'
@@ -94,7 +94,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
94
94
  personaKey: 'cela',
95
95
  bundleId: 'persona-cela-core',
96
96
  catalogMetadata: buildCatalogMetadata('cela', ['contract-review-analysis', 'nda-creation', 'saas-contract-package-creation']),
97
- protectedJobs: ['contract-review-analysis', 'nda-creation', 'saas-contract-package-creation', 'trademark-registration-management', 'provisional-patent-application-creation', 'opensign-cloud-esign-dispatch', 'w9-creation', 'invoice-generation', 'entity-type-selection', 'state-incorporation-filing', 'business-tax-registration', 'ein-application', 'cap-table-construction', 'founder-and-equity-agreements', 'ip-assignment-agreement-creation', 'employment-structure-decision', 'business-banking-setup'],
97
+ protectedJobs: ['contract-review-analysis', 'nda-creation', 'saas-contract-package-creation', 'trademark-registration-management', 'provisional-patent-application-creation', 'opensign-cloud-esign-dispatch', 'w9-creation', 'entity-type-selection', 'state-incorporation-filing', 'business-tax-registration', 'ein-application', 'cap-table-construction', 'founder-and-equity-agreements', 'ip-assignment-agreement-creation', 'employment-structure-decision'],
98
98
  protectedAliases: ['legal', 'counsel'],
99
99
  defaultHireMode: 'job',
100
100
  lockCopy: 'Hire CELiA to unlock legal workflow support for this request.'
@@ -103,7 +103,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
103
103
  personaKey: 'sekhar',
104
104
  bundleId: 'persona-sekhar-core',
105
105
  catalogMetadata: buildCatalogMetadata('sekhar', ['security-review', 'ai-native-security-setup', 'security-findings-command-center']),
106
- protectedJobs: ['security-review', 'ai-native-security-setup', 'security-findings-command-center', 'vulnerability-triage-and-remediation', 'production-readiness-review', 'compliance-review', 'compliance-continuous-monitoring', 'compliance-policy-authoring', 'compliance-questionnaire-response', 'compliance-requirements-detection', 'generate-audit-evidence', 'regulation-evidence-management', 'soc2-evidence-management'],
106
+ protectedJobs: ['security-review', 'ai-native-security-setup', 'security-findings-command-center', 'vulnerability-triage-and-remediation', 'compliance-review', 'compliance-continuous-monitoring', 'compliance-policy-authoring', 'compliance-questionnaire-response', 'compliance-requirements-detection', 'generate-audit-evidence', 'regulation-evidence-management', 'soc2-evidence-management'],
107
107
  protectedAliases: ['security', 'appsec'],
108
108
  defaultHireMode: 'job',
109
109
  lockCopy: 'Hire SEChar to unlock security setup, review, and findings-command work for this request.'
@@ -112,7 +112,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
112
112
  personaKey: 'ashley',
113
113
  bundleId: 'persona-ashley-core',
114
114
  catalogMetadata: buildCatalogMetadata('ashley', ['executive-assistant', 'weekly-operating-review', 'send-thank-you-notes']),
115
- protectedJobs: ['executive-assistant', 'weekly-operating-review', 'send-thank-you-notes', 'send-newsletter', 'analyze-transcript', 'send-stakeholder-update', 'portfolio-impact-report'],
115
+ protectedJobs: ['executive-assistant', 'weekly-operating-review', 'send-thank-you-notes', 'send-newsletter', 'analyze-transcript', 'send-stakeholder-update'],
116
116
  protectedAliases: ['executive-assistant', 'operations-assistant'],
117
117
  defaultHireMode: 'job',
118
118
  lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
@@ -121,7 +121,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
121
121
  personaKey: 'mandy',
122
122
  bundleId: 'persona-mandy-core',
123
123
  catalogMetadata: buildCatalogMetadata('mandy', ['fully-delegate', 'project-plan-creation', 'stakeholder-status-reporting']),
124
- protectedJobs: ['fully-delegate', 'delivery-governance-review', 'project-plan-creation', 'stakeholder-status-reporting', 'experiment-tracking', 'cross-functional-dependency-management'],
124
+ protectedJobs: ['fully-delegate', 'delivery-governance-review', 'project-plan-creation', 'stakeholder-status-reporting', 'experiment-tracking', 'cross-functional-dependency-management', 'portfolio-impact-report'],
125
125
  protectedAliases: ['manager', 'team-lead', 'orchestrator'],
126
126
  defaultHireMode: 'job',
127
127
  lockCopy: 'Hire MANdy to unlock autonomous multi-role orchestration — MANdy plans the job sequence, runs sub-agents in parallel, coaches them through verification loops, and hands back a synthesized DRAFT for your approval.'
@@ -175,7 +175,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
175
175
  personaKey: 'casey',
176
176
  bundleId: 'persona-casey-core',
177
177
  catalogMetadata: buildCatalogMetadata('casey', ['support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation']),
178
- protectedJobs: ['crm-account-health-review', 'crm-case-resolution', 'customer-health-review', 'loyalty-program-management', 'survey-campaign-management', 'support-queue-management', 'support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation'],
178
+ protectedJobs: ['crm-account-health-review', 'crm-case-resolution', 'customer-health-review', 'loyalty-program-management', 'survey-campaign-management', 'support-queue-management', 'support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation', 'user-survey-management'],
179
179
  protectedAliases: ['customer-success', 'customer-support', 'csm', 'support'],
180
180
  defaultHireMode: 'job',
181
181
  lockCopy: 'Hire CaSey to unlock customer success and support work for this request.'
@@ -205,7 +205,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
205
205
  personaKey: 'mona',
206
206
  bundleId: 'persona-mona-core',
207
207
  catalogMetadata: buildCatalogMetadata('mona', ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review']),
208
- protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning'],
208
+ protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning', 'aws-activate-credits-application', 'google-cloud-credits-application', 'microsoft-azure-credits-application', 'invoice-generation', 'business-banking-setup'],
209
209
  protectedAliases: ['finance', 'financial-modeling', 'fpa', 'bookkeeping'],
210
210
  defaultHireMode: 'job',
211
211
  lockCopy: 'Hire MONa to unlock financial modeling, FP&A, and tax strategy work for this request.'
@@ -214,7 +214,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
214
214
  personaKey: 'sreya',
215
215
  bundleId: 'persona-sreya-core',
216
216
  catalogMetadata: buildCatalogMetadata('sreya', ['slo-design-and-implementation', 'incident-response', 'reliability-review']),
217
- protectedJobs: ['slo-design-and-implementation', 'incident-response', 'reliability-review'],
217
+ protectedJobs: ['slo-design-and-implementation', 'incident-response', 'reliability-review', 'production-readiness-review'],
218
218
  protectedAliases: ['sre', 'reliability', 'incident-response', 'on-call'],
219
219
  defaultHireMode: 'job',
220
220
  lockCopy: 'Hire SREya to unlock SLO governance, incident response, and production reliability work for this request.'
@@ -231,7 +231,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
231
231
  'procurement-negotiation-prep',
232
232
  'purchase-approval-and-po-package',
233
233
  'equipment-acceptance-and-supplier-retrospective',
234
- 'supplier-performance-review'
234
+ 'supplier-performance-review',
235
+ 'rfp-response-preparation'
235
236
  ],
236
237
  protectedAliases: ['procurement', 'sourcing', 'purchasing', 'supplier-management'],
237
238
  defaultHireMode: 'job',
@@ -266,6 +267,9 @@ function getPersonaCapabilityBundle(personaKey) {
266
267
  return exports.PERSONA_CAPABILITY_BUNDLES[personaKey];
267
268
  }
268
269
  function getProtectedPersonaForJob(jobName) {
270
+ if (isFreeJob(jobName)) {
271
+ return null;
272
+ }
269
273
  return PROTECTED_JOB_TO_PERSONA.get(jobName) || null;
270
274
  }
271
275
  function listPersonaCapabilityBundles() {
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.maskInstallKey = maskInstallKey;
6
7
  exports.createInitialFirstRunState = createInitialFirstRunState;
7
8
  exports.loadFirstRunState = loadFirstRunState;
8
9
  exports.saveFirstRunState = saveFirstRunState;
@@ -183,6 +183,19 @@ class FirstRunServer {
183
183
  return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not run row.' });
184
184
  }
185
185
  });
186
+ // Issue #646: accept a user-pasted key when first-run was launched without one
187
+ // (the no-terminal macOS installer path).
188
+ this.app.post('/api/first-run/set-key', (req, res) => {
189
+ const { key } = req.body || {};
190
+ if (!key || typeof key !== 'string') {
191
+ return res.status(400).json({ ok: false, error: 'key is required.' });
192
+ }
193
+ const result = this.sessionService.setKey(key);
194
+ if (!result.ok) {
195
+ return res.status(400).json({ ok: false, error: result.message });
196
+ }
197
+ return res.json({ ok: true, session: this.sessionService.getSession() });
198
+ });
186
199
  this.app.post('/api/first-run/agent/change', (req, res) => {
187
200
  try {
188
201
  return res.json(this.sessionService.changeAgent(req.body || {}));
@@ -200,7 +200,10 @@ function surfaceForAgent(option) {
200
200
  }
201
201
  class FirstRunSessionService {
202
202
  constructor(options) {
203
- this.key = options.key;
203
+ // Issue #646: the key may be absent when first-run is launched by the macOS
204
+ // installer (.pkg) instead of `fraim first-run --key=…`. In that case the
205
+ // wizard prompts the user to paste their key (see setKey / needsKey).
206
+ this.key = options.key || '';
204
207
  this.headless = options.headless === true;
205
208
  this.fakeMode = getFakeStateMode();
206
209
  this.fakeStderr =
@@ -449,8 +452,24 @@ class FirstRunSessionService {
449
452
  agentOptions: types_1.FIRST_RUN_AGENT_OPTIONS,
450
453
  currentAgentId: this.state.agentId,
451
454
  supportedAgents,
455
+ needsKey: !this.key,
452
456
  };
453
457
  }
458
+ /**
459
+ * Issue #646: accept a user-pasted FRAIM key when first-run was launched
460
+ * without one (the no-terminal macOS installer path). Returns ok=false with a
461
+ * message on an invalid key so the wizard can show inline guidance.
462
+ */
463
+ setKey(rawKey) {
464
+ const key = (rawKey || '').trim();
465
+ if (!/^fraim_[A-Za-z0-9]+$/.test(key)) {
466
+ return { ok: false, message: 'That doesn\'t look like a FRAIM key. It should start with "fraim_" — copy it from your account page.' };
467
+ }
468
+ this.key = key;
469
+ this.state.installKeyRef = (0, install_state_1.maskInstallKey)(key);
470
+ this.persist();
471
+ return { ok: true };
472
+ }
454
473
  respond(message, ok) {
455
474
  return {
456
475
  ok,
@@ -588,12 +607,12 @@ class FirstRunSessionService {
588
607
  persistShellPath();
589
608
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
590
609
  if (detectedIDEs.length > 0) {
591
- await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, detectedIDEs.map((ide) => ide.name), {});
610
+ await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, detectedIDEs.map((ide) => ide.name));
592
611
  }
593
612
  // Write config.json after autoConfigureMCP so detectRowsOnLoad can use its
594
613
  // existence as a signal that IDE setup completed. Writing it before would cause
595
614
  // the wizard to mark this row 'ok' on retry before IDEs are actually configured.
596
- (0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
615
+ (0, setup_1.saveGlobalConfig)(this.key, 'conversational');
597
616
  const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../cli/setup/user-level-sync')));
598
617
  await syncUserLevelArtifacts();
599
618
  const { installSlashCommands, installGlobalRules } = await Promise.resolve().then(() => __importStar(require('../cli/setup/ide-global-integration')));
@@ -413,6 +413,7 @@ class FraimLocalMCPServer {
413
413
  this.otlpServer = null;
414
414
  this.isShutdown = false;
415
415
  this.mentoringResponseCache = null;
416
+ this.jobStartCache = new Map();
416
417
  this.connectSyncInFlight = null;
417
418
  this.latestConnectSyncWarning = null;
418
419
  this.orgCacheRefreshInFlight = false;
@@ -1996,7 +1997,7 @@ class FraimLocalMCPServer {
1996
1997
  if (!this.mentoringResponseCache) {
1997
1998
  this.mentoringResponseCache = new Map();
1998
1999
  }
1999
- this.mentoringResponseCache.set(requestId, {
2000
+ this.mentoringResponseCache.set(request.id, {
2000
2001
  nextPhase: tutoringResponse.nextPhase,
2001
2002
  jobId: args.jobId || requestSessionId // Use jobId from args, fallback to sessionId
2002
2003
  });
@@ -2063,10 +2064,13 @@ class FraimLocalMCPServer {
2063
2064
  const overview = await mentor.getJobOverview(name);
2064
2065
  if (overview) {
2065
2066
  this.log(`✅ Local override found for get_fraim_job: ${name}`);
2067
+ const jobId = (0, crypto_1.randomUUID)();
2068
+ this.jobStartCache.set(request.id, { jobId, jobName: name });
2069
+ this.log(`📊 Generated jobId=${jobId} for job=${name}`);
2066
2070
  let responseText = overview.overview;
2067
2071
  if (!overview.isSimple) {
2068
2072
  responseText = `${mentor.getCompactPhaseAuthority()}\n\n${responseText}`;
2069
- responseText += `\n\n---\n\n**This job has phases.** Use \`seekMentoring\` to get phase-specific instructions.`;
2073
+ responseText += `\n\n---\n\n**Job ID:** \`${jobId}\`\n\n**This job has phases.** Use \`seekMentoring\` with the jobId above to get phase-specific instructions.`;
2070
2074
  }
2071
2075
  // Inject local learning context for job requests (RFC 177).
2072
2076
  const userEmail = this.ensureEngine().getUserEmail();
@@ -2356,7 +2360,7 @@ class FraimLocalMCPServer {
2356
2360
  if (!this.repoInfo) {
2357
2361
  this.detectRepoInfo();
2358
2362
  }
2359
- // Bug fix: Enrich seekMentoring args with response data
2363
+ // Enrich seekMentoring args with response data (nextPhase, jobId from cache)
2360
2364
  if (toolName === 'seekMentoring' && this.mentoringResponseCache) {
2361
2365
  const requestId = request.id;
2362
2366
  const cachedResponse = this.mentoringResponseCache.get(requestId);
@@ -2366,10 +2370,20 @@ class FraimLocalMCPServer {
2366
2370
  nextPhase: cachedResponse.nextPhase,
2367
2371
  jobId: cachedResponse.jobId
2368
2372
  };
2369
- this.mentoringResponseCache.delete(requestId); // Clean up
2373
+ this.mentoringResponseCache.delete(requestId);
2370
2374
  this.log(`📊 Enriched seekMentoring args with nextPhase=${cachedResponse.nextPhase}, jobId=${cachedResponse.jobId}`);
2371
2375
  }
2372
2376
  }
2377
+ // Enrich get_fraim_job args with the UUID jobId and action:start from jobStartCache
2378
+ if (toolName === 'get_fraim_job') {
2379
+ const requestId = request.id;
2380
+ const cached = this.jobStartCache.get(requestId);
2381
+ if (cached) {
2382
+ args = { ...args, jobId: cached.jobId, action: 'start' };
2383
+ this.jobStartCache.delete(requestId);
2384
+ this.log(`📊 Enriched get_fraim_job args with jobId=${cached.jobId}, action=start`);
2385
+ }
2386
+ }
2373
2387
  // Capture the current queue size before collection
2374
2388
  const beforeCount = this.usageCollector.getEventCount();
2375
2389
  try {
@@ -2410,6 +2424,16 @@ class FraimLocalMCPServer {
2410
2424
  this.log(`📊 🔍 Debug: get_fraim_file path="${args.path}"`);
2411
2425
  }
2412
2426
  }
2427
+ // Emit job-complete event when seekMentoring reaches the final phase
2428
+ if (toolName === 'seekMentoring' && args.nextPhase === null && args.jobName && args.jobId) {
2429
+ try {
2430
+ this.usageCollector.collectJobComplete(args.jobName, args.jobId, requestSessionId);
2431
+ this.log(`📊 ✅ Job complete event emitted: ${args.jobName} (jobId: ${args.jobId})`);
2432
+ }
2433
+ catch (err) {
2434
+ this.log(`📊 ⚠️ Job complete event failed (non-blocking): ${err.message}`);
2435
+ }
2436
+ }
2413
2437
  }
2414
2438
  catch (error) {
2415
2439
  this.log(`📊 ❌ Usage collection error: ${error.message}`);
@@ -142,6 +142,8 @@ class UsageCollector {
142
142
  case 'get_fraim_job':
143
143
  if (args.job)
144
144
  analyticsArgs.job = args.job;
145
+ if (args.action)
146
+ analyticsArgs.action = args.action;
145
147
  break;
146
148
  case 'fraim_connect':
147
149
  if (args.agent?.name)
@@ -179,6 +181,28 @@ class UsageCollector {
179
181
  return null;
180
182
  }
181
183
  }
184
+ /**
185
+ * Emit a job-completion event when seekMentoring returns nextPhase === null.
186
+ * Called by the local proxy after the final mentoring event is queued.
187
+ */
188
+ collectJobComplete(jobName, jobId, sessionId) {
189
+ const event = {
190
+ type: 'job',
191
+ name: jobName,
192
+ userId: this.userId || 'unknown',
193
+ sessionId,
194
+ success: true,
195
+ jobId,
196
+ repoIdentifier: this.repoIdentifier || undefined,
197
+ agentName: this.agentName || undefined,
198
+ agentModel: this.agentModel || undefined,
199
+ args: { action: 'complete', jobId },
200
+ };
201
+ this.events.push(event);
202
+ const msg = `[UsageCollector] ✅ Job complete event: ${jobName} (jobId: ${jobId}, queue: ${this.events.length})`;
203
+ console.error(msg);
204
+ process.stderr.write(msg + '\n');
205
+ }
182
206
  /**
183
207
  * Get collected events for upload and clear the queue
184
208
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.176",
3
+ "version": "2.0.179",
4
4
  "description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
11
11
  "dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
12
- "build": "tsx scripts/build-fraim-config-schema-template.ts && npm run typecheck:scripts && tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && npm run validate:employee-catalog && npm run validate:learning-format-contract && tsx scripts/validate-purity.ts",
12
+ "build": "tsx scripts/build-fraim-config-schema-template.ts && npm run typecheck:scripts && tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:marketplaces && npm run validate:fraim-pro-assets && npm run validate:employee-catalog && npm run validate:learning-format-contract && tsx scripts/validate-purity.ts",
13
13
  "validate:learning-format-contract": "tsx scripts/validate-learning-format-contract.ts",
14
14
  "build:stubs": "tsx scripts/build-stub-registry.ts",
15
15
  "build:fraim-brain": "node scripts/generate-fraim-brain.js",
@@ -28,6 +28,8 @@
28
28
  "hub:desktop": "npm run build && electron dist/src/ai-hub/desktop-main.js",
29
29
  "hub:dev": "tsx scripts/start-hub-dev.ts",
30
30
  "firstrun:dev": "tsx scripts/start-firstrun-dev.ts",
31
+ "sign:mac": "bash scripts/sign-macos-installer.sh",
32
+ "build:mac-installer": "bash scripts/build-macos-installer.sh",
31
33
  "start:fraim": "tsx src/fraim-mcp-server.ts",
32
34
  "dev:fraim": "tsx --watch src/fraim-mcp-server.ts",
33
35
  "serve:website": "node fraim-pro/serve.js",
@@ -48,6 +50,7 @@
48
50
  "publish-fraim-only": "node scripts/publish-fraim.js",
49
51
  "publish-both-manual": "node scripts/publish-both.js",
50
52
  "validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:jobs && npm run validate:skills && npm run validate:registry-references && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens && npm run validate:brain-mapping && npm run validate:template-syntax",
53
+ "validate:marketplaces": "tsx scripts/validate-marketplace-bundles.ts",
51
54
  "typecheck:scripts": "tsc -p tsconfig.scripts.json --pretty false",
52
55
  "validate:registry-references": "tsx scripts/validate-registry-references.ts",
53
56
  "validate:brain-mapping": "tsx scripts/validate-brain-mapping.ts",
@@ -144,8 +144,20 @@
144
144
  <!-- Overview sub-view: dashboard project cards -->
145
145
  <div id="proj-overview">
146
146
  <div class="hub-area-page">
147
- <div class="sec-label">All projects</div>
148
- <div class="proj-grid" id="proj-grid"></div>
147
+ <!-- Issue #671: Cards / Kanban view toggle -->
148
+ <div class="kb-view-toggle" id="kb-view-toggle">
149
+ <button class="kb-vtab on" data-view="cards" type="button">Cards</button>
150
+ <button class="kb-vtab" data-view="kanban" type="button">Kanban</button>
151
+ </div>
152
+ <div id="proj-cards-view">
153
+ <div class="sec-label" id="proj-cards-label">All projects</div>
154
+ <div class="proj-grid" id="proj-grid"></div>
155
+ <div id="kb-runs-grid" hidden></div>
156
+ </div>
157
+ </div>
158
+ <!-- Issue #671: Kanban board — rendered by tfRenderKanban() -->
159
+ <div id="proj-kanban" hidden>
160
+ <div id="kb-stats" class="kb-stats"></div>
149
161
  </div>
150
162
  </div>
151
163