fraim-framework 2.0.87 → 2.0.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -304,6 +304,36 @@ fraim sync # Sync latest workflows and rules
304
304
 
305
305
  **šŸ’” Pro Tip**: Use `fraim add-ide` when you install a new IDE after initial setup. It reuses your existing FRAIM and platform tokens, making it much faster than running full setup again.
306
306
 
307
+ ### **🧩 Personalized Jobs, Skills, and Rules**
308
+
309
+ Project-specific customization now lives under `.fraim/personalized-employee/`.
310
+
311
+ Recommended layout:
312
+
313
+ ```text
314
+ .fraim/
315
+ personalized-employee/
316
+ jobs/
317
+ skills/
318
+ rules/
319
+ templates/
320
+ ```
321
+
322
+ Use `fraim override` to create a local starting point:
323
+
324
+ ```bash
325
+ fraim override --inherit jobs/product-building/feature-implementation.md
326
+ fraim override --copy rules/engineering/architecture-standards.md
327
+ ```
328
+
329
+ Guidance:
330
+ - Put phased job customizations in `.fraim/personalized-employee/jobs/...`
331
+ - Put reusable local capability snippets in `.fraim/personalized-employee/skills/...`
332
+ - Put broad team conventions in `.fraim/personalized-employee/rules/...`
333
+ - Put local deliverable tweaks in `.fraim/personalized-employee/templates/...`
334
+ - Do not edit synced content under `.fraim/ai-employee/` or `.fraim/ai-manager/`; `fraim sync` will overwrite it
335
+ - Legacy `.fraim/overrides/` is still read for compatibility, but new work should go in `.fraim/personalized-employee/`
336
+
307
337
  ### **šŸ”§ Jira Integration Setup**
308
338
 
309
339
  FRAIM uses the official Model Context Protocol (MCP) server for Jira integration. The setup command automatically configures the correct format.
@@ -42,9 +42,14 @@ const updateIDEConfigs = async (provider, config) => {
42
42
  const tokenConfig = {
43
43
  github: config.tokens?.github,
44
44
  gitlab: config.tokens?.gitlab,
45
+ ado: config.tokens?.ado,
45
46
  jira: config.tokens?.jira
46
47
  };
47
- const providerConfigs = config.providerConfigs || {};
48
+ const providerConfigs = Object.entries(config.providerConfigs || {}).reduce((acc, [key, value]) => {
49
+ const providerId = key.replace(/Config$/, '');
50
+ acc[providerId] = value;
51
+ return acc;
52
+ }, {});
48
53
  for (const ide of detectedIDEs) {
49
54
  try {
50
55
  const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
@@ -135,11 +140,14 @@ const runAddProvider = async (provider, options) => {
135
140
  // Check if provider already configured
136
141
  const providerName = await (0, provider_registry_1.getProviderDisplayName)(provider);
137
142
  const hasExistingConfig = config.tokens[provider] ||
138
- (provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl);
143
+ (provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl) ||
144
+ (provider === 'ado' && config.providerConfigs?.adoConfig?.organization);
139
145
  if (hasExistingConfig) {
140
146
  const displayInfo = provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl
141
147
  ? ` (${config.providerConfigs.jiraConfig.baseUrl})`
142
- : '';
148
+ : provider === 'ado' && config.providerConfigs?.adoConfig?.organization
149
+ ? ` (${config.providerConfigs.adoConfig.organization})`
150
+ : '';
143
151
  console.log(chalk_1.default.yellow(`āš ļø ${providerName} is already configured${displayInfo}`));
144
152
  // Skip prompt if FRAIM_FORCE_OVERWRITE is set (for testing)
145
153
  if (!process.env.FRAIM_FORCE_OVERWRITE) {
@@ -163,11 +171,12 @@ const runAddProvider = async (provider, options) => {
163
171
  if (options.token) {
164
172
  providedTokens[provider] = options.token;
165
173
  }
166
- // Handle provider-specific config options (e.g., Jira URL and email)
167
- if (options.url || options.email) {
174
+ // Handle provider-specific config options (e.g., Jira URL/email or ADO organization)
175
+ if (options.url || options.email || options.organization) {
168
176
  providedConfigs[provider] = {
169
177
  ...(options.url && { baseUrl: options.url.replace(/^https?:\/\//, '').replace(/\/$/, '') }),
170
- ...(options.email && { email: options.email })
178
+ ...(options.email && { email: options.email }),
179
+ ...(options.organization && { organization: options.organization })
171
180
  };
172
181
  }
173
182
  // Get credentials using generic prompt system
@@ -209,6 +218,7 @@ exports.addProviderCommand = new commander_1.Command('add-provider')
209
218
  .description(`Add or update a provider after initial setup`)
210
219
  .argument('<provider>', `Provider to add`)
211
220
  .option('--token <token>', 'Provider token (will prompt if not provided)')
221
+ .option('--organization <organization>', 'Organization (for providers that require it)')
212
222
  .option('--email <email>', 'Email (for providers that require it)')
213
223
  .option('--url <url>', 'Instance URL (for providers that require it)')
214
224
  .option('--no-validate', 'Skip token validation')
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
12
  const prompts_1 = __importDefault(require("prompts"));
13
+ const child_process_1 = require("child_process");
13
14
  const sync_1 = require("./sync");
14
15
  const platform_detection_1 = require("../utils/platform-detection");
15
16
  const version_utils_1 = require("../utils/version-utils");
@@ -21,6 +22,11 @@ const promptForJiraProjectKey = async (jiraBaseUrl) => {
21
22
  console.log(chalk_1.default.blue('\nšŸŽ« Jira Project Configuration'));
22
23
  console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
23
24
  console.log(chalk_1.default.gray('Enter the Jira project key for this repository (e.g., TEAM, PROJ, DEV)\n'));
25
+ if (process.env.FRAIM_NON_INTERACTIVE) {
26
+ const defaultKey = process.env.FRAIM_JIRA_PROJECT_KEY || 'PROJ';
27
+ console.log(chalk_1.default.yellow(`\nā„¹ļø Non-interactive mode: using Jira project key "${defaultKey}"`));
28
+ return defaultKey;
29
+ }
24
30
  const response = await (0, prompts_1.default)({
25
31
  type: 'text',
26
32
  name: 'projectKey',
@@ -60,6 +66,77 @@ const checkGlobalSetup = () => {
60
66
  return { exists: true, mode: 'integrated', tokens: {} };
61
67
  }
62
68
  };
69
+ /**
70
+ * Install GitHub workflow files to .github/workflows/
71
+ * Gracefully handles cases where gh CLI is not available
72
+ */
73
+ const installGitHubWorkflows = (projectRoot) => {
74
+ const workflowsDir = path_1.default.join(projectRoot, '.github', 'workflows');
75
+ // Find registry directory (works in both dev and installed package)
76
+ const registryDir = fs_1.default.existsSync(path_1.default.join(__dirname, '..', '..', '..', 'registry'))
77
+ ? path_1.default.join(__dirname, '..', '..', '..', 'registry')
78
+ : path_1.default.join(__dirname, '..', '..', 'registry');
79
+ const sourceDir = path_1.default.join(registryDir, 'github', 'workflows');
80
+ // Create .github/workflows directory if it doesn't exist
81
+ if (!fs_1.default.existsSync(workflowsDir)) {
82
+ fs_1.default.mkdirSync(workflowsDir, { recursive: true });
83
+ }
84
+ const workflowFiles = ['phase-change.yml', 'status-change.yml', 'sync-on-pr-review.yml'];
85
+ workflowFiles.forEach((file) => {
86
+ const sourcePath = path_1.default.join(sourceDir, file);
87
+ const destPath = path_1.default.join(workflowsDir, file);
88
+ if (fs_1.default.existsSync(sourcePath)) {
89
+ fs_1.default.copyFileSync(sourcePath, destPath);
90
+ console.log(chalk_1.default.green(`Installed workflow: .github/workflows/${file}`));
91
+ }
92
+ else {
93
+ console.log(chalk_1.default.yellow(`Warning: Workflow not found in registry: ${file}`));
94
+ }
95
+ });
96
+ };
97
+ /**
98
+ * Create GitHub labels using gh CLI
99
+ * Gracefully handles cases where gh CLI is not available
100
+ */
101
+ const createGitHubLabels = (projectRoot) => {
102
+ // Check if gh CLI is available
103
+ try {
104
+ (0, child_process_1.execSync)('gh --version', { stdio: 'ignore' });
105
+ }
106
+ catch {
107
+ console.log(chalk_1.default.yellow('GitHub CLI (gh) not found. Skipping label creation.'));
108
+ console.log(chalk_1.default.gray('Install gh CLI to enable automatic label creation: https://cli.github.com/'));
109
+ return;
110
+ }
111
+ // Read labels from labels.json
112
+ const labelsPath = path_1.default.join(__dirname, '..', '..', '..', 'labels.json');
113
+ if (!fs_1.default.existsSync(labelsPath)) {
114
+ console.log(chalk_1.default.yellow('labels.json not found. Skipping label creation.'));
115
+ return;
116
+ }
117
+ try {
118
+ const labels = JSON.parse(fs_1.default.readFileSync(labelsPath, 'utf8'));
119
+ labels.forEach((label) => {
120
+ try {
121
+ // Try to create the label, ignore if it already exists
122
+ (0, child_process_1.execSync)(`gh label create "${label.name}" --color "${label.color}" --description "${label.description}"`, { cwd: projectRoot, stdio: 'ignore' });
123
+ console.log(chalk_1.default.green(`Created label: ${label.name}`));
124
+ }
125
+ catch (error) {
126
+ // Label might already exist, which is fine
127
+ if (error.message && error.message.includes('already exists')) {
128
+ console.log(chalk_1.default.gray(`Label already exists: ${label.name}`));
129
+ }
130
+ else {
131
+ console.log(chalk_1.default.yellow(`Could not create label ${label.name}: ${error.message}`));
132
+ }
133
+ }
134
+ });
135
+ }
136
+ catch (error) {
137
+ console.log(chalk_1.default.yellow(`Error reading labels.json: ${error.message}`));
138
+ }
139
+ };
63
140
  const runInitProject = async () => {
64
141
  console.log(chalk_1.default.blue('Initializing FRAIM project...'));
65
142
  const globalSetup = checkGlobalSetup();
@@ -170,6 +247,13 @@ const runInitProject = async () => {
170
247
  if ((0, fraim_gitignore_1.ensureFraimSyncedContentIgnored)(projectRoot)) {
171
248
  console.log(chalk_1.default.green('Updated .gitignore with FRAIM synced content ignore rules'));
172
249
  }
250
+ // Install GitHub workflows and create labels for GitHub repositories
251
+ const detection = (0, platform_detection_1.detectPlatformFromGit)();
252
+ if (detection.provider === 'github') {
253
+ console.log(chalk_1.default.blue('\nSetting up GitHub workflows and labels...'));
254
+ installGitHubWorkflows(projectRoot);
255
+ createGitHubLabels(projectRoot);
256
+ }
173
257
  if (!process.env.FRAIM_SKIP_SYNC) {
174
258
  await (0, sync_1.runSync)({});
175
259
  }
@@ -180,7 +264,25 @@ const runInitProject = async () => {
180
264
  console.log(chalk_1.default.green(`${status} project Codex config at ${codexLocalResult.path}`));
181
265
  }
182
266
  console.log(chalk_1.default.green('\nFRAIM project initialized!'));
183
- console.log(chalk_1.default.cyan('Try: Ask your AI agent "list fraim workflows"'));
267
+ if (!process.env.FRAIM_NON_INTERACTIVE) {
268
+ const response = await (0, prompts_1.default)({
269
+ type: 'confirm',
270
+ name: 'runOnboarding',
271
+ message: 'Would you like to set up your AI employee for success? Use the "ai-manager/jobs/project-setup/project-onboarding" job now.',
272
+ initial: true
273
+ });
274
+ if (response.runOnboarding) {
275
+ console.log(chalk_1.default.blue('\nšŸš€ Proactive Onboarding: Project Success Setup'));
276
+ console.log(chalk_1.default.gray('Simply tell your AI agent: "run the project-onboarding job under ai-manager/project-setup"'));
277
+ console.log(chalk_1.default.gray('This will help the AI understand your project goals, tech stack, and industry context.'));
278
+ }
279
+ else {
280
+ console.log(chalk_1.default.cyan('\nTip: You can always ask your AI agent to "list fraim jobs" to see what\'s available.'));
281
+ }
282
+ }
283
+ else {
284
+ console.log(chalk_1.default.cyan('\nWould you like to set up your AI employee for success? Use the "ai-manager/jobs/project-setup/project-onboarding" job now.'));
285
+ }
184
286
  };
185
287
  exports.runInitProject = runInitProject;
186
288
  exports.initProjectCommand = new commander_1.Command('init-project')
@@ -0,0 +1,84 @@
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.loginCommand = void 0;
7
+ const commander_1 = require("commander");
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 version_utils_1 = require("../utils/version-utils");
15
+ const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
16
+ exports.loginCommand = new commander_1.Command('login')
17
+ .description('Login to platforms (GitHub, etc.)');
18
+ exports.loginCommand
19
+ .command('github')
20
+ .description('Login to GitHub using Device Flow')
21
+ .action(async () => {
22
+ // Load the token to global config
23
+ const globalConfigDir = (0, script_sync_utils_1.getUserFraimDir)();
24
+ const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
25
+ let config = {};
26
+ if (fs_1.default.existsSync(globalConfigPath)) {
27
+ config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
28
+ }
29
+ if (!config.apiKey) {
30
+ console.error(chalk_1.default.red('\nāŒ No FRAIM API key found. Please run "fraim setup" first.'));
31
+ process.exit(1);
32
+ }
33
+ const client = new provider_client_1.ProviderClient(config.apiKey);
34
+ let githubConfig;
35
+ try {
36
+ const providers = await client.getAllProviders();
37
+ githubConfig = providers.find(p => p.id === 'github')?.deviceFlowConfig;
38
+ if (!githubConfig) {
39
+ console.error(chalk_1.default.red('\nāŒ Device flow configuration for GitHub not found on server.'));
40
+ process.exit(1);
41
+ }
42
+ }
43
+ catch (error) {
44
+ console.error(chalk_1.default.red(`\nāŒ Failed to fetch provider configuration: ${error.message}`));
45
+ process.exit(1);
46
+ }
47
+ const deviceFlow = new device_flow_service_1.DeviceFlowService(githubConfig);
48
+ try {
49
+ const token = await deviceFlow.login();
50
+ config.tokens = {
51
+ ...(config.tokens || {}),
52
+ github: token
53
+ };
54
+ config.version = config.version || (0, version_utils_1.getFraimVersion)();
55
+ config.updatedAt = new Date().toISOString();
56
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
57
+ console.log(chalk_1.default.green('\nāœ… GitHub token saved to global configuration.'));
58
+ // Trigger MCP reconfiguration
59
+ if (config.apiKey) {
60
+ console.log(chalk_1.default.blue('šŸ”Œ Updating MCP server configurations...'));
61
+ const mcpTokens = {};
62
+ Object.entries(config.tokens).forEach(([id, t]) => {
63
+ mcpTokens[id] = t;
64
+ });
65
+ // Re-use existing provider configs if any
66
+ const providerConfigsMap = {};
67
+ if (config.providerConfigs) {
68
+ Object.entries(config.providerConfigs).forEach(([key, val]) => {
69
+ const providerId = key.replace('Config', '');
70
+ providerConfigsMap[providerId] = val;
71
+ });
72
+ }
73
+ await (0, auto_mcp_setup_1.autoConfigureMCP)(config.apiKey, mcpTokens, undefined, providerConfigsMap);
74
+ console.log(chalk_1.default.green('āœ… MCP servers updated with new GitHub token.'));
75
+ }
76
+ else {
77
+ console.log(chalk_1.default.yellow('āš ļø No FRAIM API key found. Run "fraim setup" to complete configuration.'));
78
+ }
79
+ }
80
+ catch (error) {
81
+ console.error(chalk_1.default.red(`\nāŒ Login failed: ${error.message}`));
82
+ process.exit(1);
83
+ }
84
+ });
@@ -245,7 +245,7 @@ const runSetup = async (options) => {
245
245
  let providedConfigs = {};
246
246
  if (isUpdate) {
247
247
  // Update existing setup - add platforms
248
- console.log(chalk_1.default.blue('šŸ“ Updating existing FRAIM configuration...\n'));
248
+ console.log(chalk_1.default.blue('šŸ“ Existing FRAIM configuration detected.\n'));
249
249
  try {
250
250
  const existingConfig = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
251
251
  // Allow updating FRAIM key even without provider changes
@@ -258,11 +258,141 @@ const runSetup = async (options) => {
258
258
  // Check if this is just a key update or a provider update
259
259
  const isKeyUpdate = options.key && options.key !== existingConfig.apiKey;
260
260
  const isProviderUpdate = requestedProviders.length > 0;
261
- // Only proceed if there's something to update
261
+ // If no specific changes requested, offer interactive update
262
262
  if (!isKeyUpdate && !isProviderUpdate) {
263
- console.log(chalk_1.default.yellow('āš ļø No changes specified.'));
264
- console.log(chalk_1.default.gray('Use --key to update FRAIM key, or --github, --gitlab, etc. to add providers.'));
265
- return;
263
+ console.log(chalk_1.default.gray(' Current configuration:'));
264
+ console.log(chalk_1.default.gray(` • Mode: ${existingConfig.mode || 'integrated'}`));
265
+ // Show existing tokens
266
+ const { getProvider } = await Promise.resolve().then(() => __importStar(require('../providers/provider-registry')));
267
+ if (existingConfig.tokens && Object.keys(existingConfig.tokens).length > 0) {
268
+ const providerNames = await Promise.all(Object.keys(existingConfig.tokens).map(async (id) => {
269
+ const provider = await getProvider(id);
270
+ return provider?.displayName || id;
271
+ }));
272
+ console.log(chalk_1.default.gray(` • Providers: ${providerNames.join(', ')}`));
273
+ }
274
+ else {
275
+ console.log(chalk_1.default.gray(' • Providers: none'));
276
+ }
277
+ console.log();
278
+ // Ask user what they want to do
279
+ const response = await (0, prompts_1.default)({
280
+ type: 'select',
281
+ name: 'action',
282
+ message: 'What would you like to do?',
283
+ choices: [
284
+ {
285
+ title: 'Add a provider',
286
+ value: 'add-provider',
287
+ description: 'Add a new platform integration (GitHub, GitLab, etc.)'
288
+ },
289
+ {
290
+ title: 'Update FRAIM key',
291
+ value: 'update-key',
292
+ description: 'Change your FRAIM API key'
293
+ },
294
+ {
295
+ title: 'Reconfigure from scratch',
296
+ value: 'reconfigure',
297
+ description: 'Start fresh setup (will preserve existing config as backup)'
298
+ },
299
+ {
300
+ title: 'Cancel',
301
+ value: 'cancel',
302
+ description: 'Exit without making changes'
303
+ }
304
+ ],
305
+ initial: 0
306
+ });
307
+ if (!response.action || response.action === 'cancel') {
308
+ console.log(chalk_1.default.gray('\nSetup cancelled. No changes made.'));
309
+ return;
310
+ }
311
+ if (response.action === 'add-provider') {
312
+ console.log(chalk_1.default.blue('\nšŸ“¦ Adding a provider...\n'));
313
+ const providerClient = new provider_client_1.ProviderClient(fraimKey, process.env.FRAIM_REMOTE_URL || undefined);
314
+ // Prompt for which provider to add
315
+ const providersToAdd = await (0, provider_prompts_1.promptForProviders)(providerClient);
316
+ requestedProviders = providersToAdd;
317
+ }
318
+ else if (response.action === 'update-key') {
319
+ console.log(chalk_1.default.blue('\nšŸ”‘ Updating FRAIM key...\n'));
320
+ fraimKey = await promptForFraimKey();
321
+ }
322
+ else if (response.action === 'reconfigure') {
323
+ console.log(chalk_1.default.blue('\nšŸ”„ Starting fresh setup...\n'));
324
+ // Backup existing config
325
+ const backupPath = globalConfigPath + '.backup.' + Date.now();
326
+ fs_1.default.copyFileSync(globalConfigPath, backupPath);
327
+ console.log(chalk_1.default.gray(` Backed up existing config to: ${path_1.default.basename(backupPath)}\n`));
328
+ // Treat as fresh setup
329
+ fraimKey = options.key || await promptForFraimKey();
330
+ console.log(chalk_1.default.green('āœ… FRAIM key accepted\n'));
331
+ mode = options.mode ? parseModeOption(options.mode) : await promptForMode();
332
+ const parsed = await parseLegacyOptions(options, fraimKey);
333
+ requestedProviders = parsed.requestedProviders;
334
+ providedTokens = parsed.providedTokens;
335
+ providedConfigs = parsed.providedConfigs;
336
+ // Clear existing tokens/configs for fresh start
337
+ Object.keys(tokens).forEach(key => delete tokens[key]);
338
+ Object.keys(configs).forEach(key => delete configs[key]);
339
+ // Continue with fresh setup flow
340
+ const providerClient = new provider_client_1.ProviderClient(fraimKey, process.env.FRAIM_REMOTE_URL || undefined);
341
+ if (mode === 'integrated') {
342
+ let providersToSetup = requestedProviders;
343
+ if (providersToSetup.length === 0) {
344
+ providersToSetup = await (0, provider_prompts_1.promptForProviders)(providerClient);
345
+ }
346
+ for (const providerId of providersToSetup) {
347
+ try {
348
+ const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, providerId, providedTokens[providerId], providedConfigs[providerId]);
349
+ tokens[providerId] = creds.token;
350
+ if (creds.config) {
351
+ configs[providerId] = creds.config;
352
+ }
353
+ }
354
+ catch (e) {
355
+ const providerName = await (0, provider_registry_1.getProviderDisplayName)(providerId);
356
+ console.log(chalk_1.default.red(`āŒ Failed to get ${providerName} credentials`));
357
+ process.exit(1);
358
+ }
359
+ }
360
+ }
361
+ else if (mode === 'split') {
362
+ // Split mode setup
363
+ console.log(chalk_1.default.blue('\nšŸ”€ Split Mode Configuration'));
364
+ console.log(chalk_1.default.gray('Configure separate platforms for code hosting and issue tracking.\n'));
365
+ const codeRepoProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'code');
366
+ const creds1 = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, codeRepoProvider, providedTokens[codeRepoProvider], providedConfigs[codeRepoProvider]);
367
+ tokens[codeRepoProvider] = creds1.token;
368
+ if (creds1.config) {
369
+ configs[codeRepoProvider] = creds1.config;
370
+ }
371
+ const issueProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'issues');
372
+ if (!tokens[issueProvider]) {
373
+ const creds2 = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, issueProvider, providedTokens[issueProvider], providedConfigs[issueProvider]);
374
+ tokens[issueProvider] = creds2.token;
375
+ if (creds2.config) {
376
+ configs[issueProvider] = creds2.config;
377
+ }
378
+ }
379
+ }
380
+ // Save and configure MCP
381
+ console.log(chalk_1.default.blue('\nšŸ’¾ Saving global configuration...'));
382
+ saveGlobalConfig(fraimKey, mode, tokens, configs);
383
+ console.log(chalk_1.default.blue('\nšŸ”Œ Configuring MCP servers...'));
384
+ const mcpTokens = {};
385
+ Object.entries(tokens).forEach(([id, token]) => {
386
+ mcpTokens[id] = token;
387
+ });
388
+ const providerConfigsMap = {};
389
+ Object.entries(configs).forEach(([providerId, config]) => {
390
+ providerConfigsMap[providerId] = config;
391
+ });
392
+ await (0, auto_mcp_setup_1.autoConfigureMCP)(fraimKey, mcpTokens, options.ide ? [options.ide] : undefined, providerConfigsMap);
393
+ console.log(chalk_1.default.green('\nšŸŽÆ Reconfiguration complete!'));
394
+ return;
395
+ }
266
396
  }
267
397
  mode = existingConfig.mode || 'integrated';
268
398
  // Preserve existing tokens
@@ -276,14 +406,6 @@ const runSetup = async (options) => {
276
406
  configs[providerId] = value;
277
407
  });
278
408
  }
279
- console.log(chalk_1.default.gray(` Current mode: ${mode}`));
280
- // Show existing tokens
281
- const { getProvider } = await Promise.resolve().then(() => __importStar(require('../providers/provider-registry')));
282
- const providerNames = await Promise.all(Object.keys(tokens).map(async (id) => {
283
- const provider = await getProvider(id);
284
- return provider?.displayName || id;
285
- }));
286
- console.log(chalk_1.default.gray(` Existing tokens: ${providerNames.join(', ') || 'none'}\n`));
287
409
  }
288
410
  catch (e) {
289
411
  console.log(chalk_1.default.red('āŒ Failed to read existing configuration'));
@@ -47,6 +47,7 @@ const add_ide_1 = require("./commands/add-ide");
47
47
  const add_provider_1 = require("./commands/add-provider");
48
48
  const override_1 = require("./commands/override");
49
49
  const list_overridable_1 = require("./commands/list-overridable");
50
+ const login_1 = require("./commands/login");
50
51
  const fs_1 = __importDefault(require("fs"));
51
52
  const path_1 = __importDefault(require("path"));
52
53
  const program = new commander_1.Command();
@@ -83,6 +84,7 @@ program.addCommand(add_ide_1.addIDECommand);
83
84
  program.addCommand(add_provider_1.addProviderCommand);
84
85
  program.addCommand(override_1.overrideCommand);
85
86
  program.addCommand(list_overridable_1.listOverridableCommand);
87
+ program.addCommand(login_1.loginCommand);
86
88
  // Wait for async command initialization before parsing
87
89
  (async () => {
88
90
  // Import the initialization promise from setup command
@@ -0,0 +1,83 @@
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.DeviceFlowService = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class DeviceFlowService {
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ /**
14
+ * Start the Device Flow Login
15
+ */
16
+ async login() {
17
+ console.log(chalk_1.default.blue('\nšŸ”— Starting Authentication...'));
18
+ try {
19
+ // 1. Request device and user codes
20
+ const deviceCode = await this.requestDeviceCode();
21
+ console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
22
+ console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
23
+ console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
24
+ console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
25
+ // 2. Poll for the access token
26
+ const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
27
+ console.log(chalk_1.default.green('\nāœ… Authentication Successful!'));
28
+ return token;
29
+ }
30
+ catch (error) {
31
+ console.error(chalk_1.default.red(`\nāŒ Authentication failed: ${error.message}`));
32
+ throw error;
33
+ }
34
+ }
35
+ async requestDeviceCode() {
36
+ const response = await axios_1.default.post(this.config.authUrl, {
37
+ client_id: this.config.clientId,
38
+ scope: this.config.scope
39
+ }, {
40
+ headers: { Accept: 'application/json' }
41
+ });
42
+ return response.data;
43
+ }
44
+ async pollForToken(deviceCode, interval) {
45
+ let currentInterval = interval * 1000;
46
+ return new Promise((resolve, reject) => {
47
+ const poll = async () => {
48
+ try {
49
+ const response = await axios_1.default.post(this.config.tokenUrl, {
50
+ client_id: this.config.clientId,
51
+ device_code: deviceCode,
52
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
53
+ }, {
54
+ headers: { Accept: 'application/json' }
55
+ });
56
+ if (response.data.access_token) {
57
+ resolve(response.data.access_token);
58
+ return;
59
+ }
60
+ if (response.data.error) {
61
+ const error = response.data.error;
62
+ if (error === 'authorization_pending') {
63
+ // Keep polling
64
+ setTimeout(poll, currentInterval);
65
+ }
66
+ else if (error === 'slow_down') {
67
+ currentInterval += 5000;
68
+ setTimeout(poll, currentInterval);
69
+ }
70
+ else {
71
+ reject(new Error(response.data.error_description || error));
72
+ }
73
+ }
74
+ }
75
+ catch (error) {
76
+ reject(error);
77
+ }
78
+ };
79
+ setTimeout(poll, currentInterval);
80
+ });
81
+ }
82
+ }
83
+ exports.DeviceFlowService = DeviceFlowService;
@@ -99,19 +99,20 @@ function buildStdioServer(mcpConfig, token, config) {
99
99
  if (!mcpConfig.command) {
100
100
  throw new Error('Stdio MCP server requires command');
101
101
  }
102
+ const resolveTemplate = (template) => {
103
+ let value = template.replace('{token}', token);
104
+ if (config) {
105
+ for (const [configKey, configValue] of Object.entries(config)) {
106
+ value = value.replace(`{config.${configKey}}`, String(configValue));
107
+ }
108
+ }
109
+ return value;
110
+ };
102
111
  // Build environment variables from template
103
112
  const env = {};
104
113
  if (mcpConfig.envTemplate) {
105
114
  for (const [key, template] of Object.entries(mcpConfig.envTemplate)) {
106
- let value = template;
107
- // Replace {token} placeholder
108
- value = value.replace('{token}', token);
109
- // Replace {config.key} placeholders
110
- if (config) {
111
- for (const [configKey, configValue] of Object.entries(config)) {
112
- value = value.replace(`{config.${configKey}}`, String(configValue));
113
- }
114
- }
115
+ let value = resolveTemplate(template);
115
116
  // Handle URL normalization for Jira
116
117
  if (key === 'JIRA_URL' && value && !value.startsWith('http')) {
117
118
  value = `https://${value}`;
@@ -121,7 +122,7 @@ function buildStdioServer(mcpConfig, token, config) {
121
122
  }
122
123
  return {
123
124
  command: mcpConfig.command,
124
- args: mcpConfig.args || [],
125
+ args: (mcpConfig.args || []).map(resolveTemplate),
125
126
  env
126
127
  };
127
128
  }