fraim 2.0.270 → 2.0.272

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/src/cli/commands/add-ide.js +37 -132
  2. package/dist/src/cli/commands/add-provider.js +32 -268
  3. package/dist/src/cli/commands/learning-usage.js +412 -0
  4. package/dist/src/cli/commands/login.js +5 -5
  5. package/dist/src/cli/commands/setup.js +15 -52
  6. package/dist/src/cli/commands/sync.js +111 -80
  7. package/dist/src/cli/fraim.js +1 -42
  8. package/dist/src/cli/mcp/ide-formats.js +1 -1
  9. package/dist/src/cli/mcp/mcp-server-registry.js +3 -3
  10. package/dist/src/cli/providers/local-provider-registry.js +4 -4
  11. package/dist/src/cli/setup/auto-mcp-setup.js +4 -13
  12. package/dist/src/cli/utils/remote-sync.js +41 -25
  13. package/dist/src/core/ai-mentor.js +27 -14
  14. package/dist/src/core/config-loader.js +48 -3
  15. package/dist/src/core/fraim-config-schema.generated.js +18 -0
  16. package/dist/src/core/handoff-contracts.js +37 -1
  17. package/dist/src/core/job-phases.js +2 -14
  18. package/dist/src/core/resolve-phase-edge.js +75 -0
  19. package/dist/src/core/types.js +7 -1
  20. package/dist/src/core/utils/git-utils.js +24 -14
  21. package/dist/src/core/utils/project-fraim-paths.js +16 -1
  22. package/dist/src/first-run/types.js +1 -1
  23. package/dist/src/local-mcp-server/artifact-retention-cleanup.js +8 -0
  24. package/dist/src/local-mcp-server/learning-context-builder.js +448 -95
  25. package/dist/src/local-mcp-server/learning-firing-parser.js +247 -0
  26. package/dist/src/local-mcp-server/learning-usage-analysis.js +347 -0
  27. package/dist/src/local-mcp-server/learning-usage-attestation.js +191 -0
  28. package/dist/src/local-mcp-server/learning-usage-command.js +408 -0
  29. package/dist/src/local-mcp-server/learning-usage-projection.js +79 -0
  30. package/dist/src/local-mcp-server/learning-usage-store.js +417 -0
  31. package/dist/src/local-mcp-server/stdio-server.js +43 -0
  32. package/dist/src/services/provider-service.js +4 -4
  33. package/package.json +1 -1
@@ -36,63 +36,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.saveProviderTokenToConfig = exports.loadGlobalConfig = exports.addIDECommand = exports.runAddIDE = void 0;
39
+ exports.loadGlobalConfig = exports.addIDECommand = exports.runAddIDE = void 0;
40
40
  const commander_1 = require("commander");
41
41
  const chalk_1 = __importDefault(require("chalk"));
42
42
  const prompts_1 = __importDefault(require("prompts"));
43
43
  const fs_1 = __importDefault(require("fs"));
44
44
  const path_1 = __importDefault(require("path"));
45
+ const os_1 = __importDefault(require("os"));
45
46
  const ide_detector_1 = require("../setup/ide-detector");
46
47
  const mcp_config_generator_1 = require("../setup/mcp-config-generator");
47
48
  const claude_code_telemetry_1 = require("../setup/claude-code-telemetry");
48
49
  const script_sync_utils_1 = require("../utils/script-sync-utils");
49
50
  const mcp_server_registry_1 = require("../mcp/mcp-server-registry");
50
- const get_provider_client_1 = require("../api/get-provider-client");
51
- const provider_prompts_1 = require("../setup/provider-prompts");
52
- const provider_registry_1 = require("../providers/provider-registry");
53
51
  const user_config_1 = require("../utils/user-config");
52
+ const resolveGlobalConfigPath = () => {
53
+ const primary = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
54
+ if (fs_1.default.existsSync(primary)) {
55
+ try {
56
+ const config = JSON.parse(fs_1.default.readFileSync(primary, 'utf8'));
57
+ if (config && config.apiKey)
58
+ return primary;
59
+ }
60
+ catch {
61
+ return primary;
62
+ }
63
+ }
64
+ // The aggregate test runner sets FRAIM_USER_DIR for process isolation, while
65
+ // older command tests isolate by monkey-patching os.homedir(). In test mode,
66
+ // honor that fixture home if it already contains the config under test.
67
+ if (process.env.NODE_ENV === 'test') {
68
+ const homedirConfig = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
69
+ if (homedirConfig !== primary && fs_1.default.existsSync(homedirConfig)) {
70
+ return homedirConfig;
71
+ }
72
+ }
73
+ return primary;
74
+ };
54
75
  const loadGlobalConfig = async () => {
55
- const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
76
+ const globalConfigPath = resolveGlobalConfigPath();
56
77
  if (!fs_1.default.existsSync(globalConfigPath)) {
57
78
  return null;
58
79
  }
59
80
  try {
60
81
  const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
61
- // Support both old and new token format
62
- const tokens = config.tokens || {};
63
- // Backward compatibility: map old format to new
64
- // Only try to fetch provider IDs if we have a valid FRAIM key
65
- if (config.apiKey) {
66
- try {
67
- const client = (0, get_provider_client_1.getProviderClient)();
68
- const providerIds = await client.getAllProviderIds();
69
- for (const id of providerIds) {
70
- const oldKey = `${id}Token`;
71
- if (config[oldKey] && !tokens[id]) {
72
- tokens[id] = config[oldKey];
73
- }
74
- }
75
- }
76
- catch (e) {
77
- // If provider client fails (network error, invalid key, etc.),
78
- // skip backward compatibility mapping and use local fallback
79
- // This is fine - the config will still work with the new format
80
- }
81
- }
82
- // Load all provider configs
83
- const providerConfigs = {};
84
- // New format: providerConfigs object
85
- if (config.providerConfigs) {
86
- Object.entries(config.providerConfigs).forEach(([key, value]) => {
87
- const providerId = key.replace('Config', '');
88
- providerConfigs[providerId] = value;
89
- });
90
- }
82
+ if (!config.apiKey)
83
+ return null;
91
84
  return {
92
85
  fraimKey: config.apiKey,
93
- tokens,
94
86
  mode: config.mode,
95
- providerConfigs
96
87
  };
97
88
  }
98
89
  catch (e) {
@@ -100,65 +91,7 @@ const loadGlobalConfig = async () => {
100
91
  }
101
92
  };
102
93
  exports.loadGlobalConfig = loadGlobalConfig;
103
- const promptForProviderTokenIfNeeded = async (providerId, isOptional = false) => {
104
- const client = (0, get_provider_client_1.getProviderClient)();
105
- const provider = await client.getProvider(providerId);
106
- if (!provider)
107
- return '';
108
- if (isOptional) {
109
- console.log(chalk_1.default.yellow(`\n🔑 ${provider.displayName} token (optional for conversational mode)`));
110
- console.log(chalk_1.default.gray(`${provider.displayName} token enables ${provider.displayName}-specific MCP features.\n`));
111
- const wantsToken = await (0, prompts_1.default)({
112
- type: 'confirm',
113
- name: 'addToken',
114
- message: `Do you want to add a ${provider.displayName} token?`,
115
- initial: false
116
- });
117
- if (!wantsToken.addToken) {
118
- console.log(chalk_1.default.blue(`ℹ️ Skipping ${provider.displayName} token - ${provider.displayName} MCP server will not be configured`));
119
- return '';
120
- }
121
- }
122
- else {
123
- console.log(chalk_1.default.yellow(`\n🔑 ${provider.displayName} token needed for MCP configuration`));
124
- console.log(chalk_1.default.gray(`This is required for ${provider.displayName} MCP servers to function properly.\n`));
125
- }
126
- try {
127
- return await (0, provider_prompts_1.promptForProviderToken)(client, providerId);
128
- }
129
- catch (error) {
130
- if (isOptional) {
131
- console.log(chalk_1.default.blue(`ℹ️ No ${provider.displayName} token provided - ${provider.displayName} MCP server will not be configured`));
132
- return '';
133
- }
134
- console.log(chalk_1.default.red(`${provider.displayName} token is required. Exiting.`));
135
- process.exit(1);
136
- }
137
- };
138
- const saveProviderTokenToConfig = async (providerId, token) => {
139
- const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
140
- if (fs_1.default.existsSync(globalConfigPath)) {
141
- try {
142
- const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
143
- // Use new tokens structure
144
- if (!config.tokens) {
145
- config.tokens = {};
146
- }
147
- config.tokens[providerId] = token;
148
- fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
149
- const { getProvider } = await Promise.resolve().then(() => __importStar(require('../providers/provider-registry')));
150
- const provider = await getProvider(providerId);
151
- console.log(chalk_1.default.green(`✅ ${provider?.displayName || providerId} token saved to global config`));
152
- }
153
- catch (e) {
154
- const { getProvider } = await Promise.resolve().then(() => __importStar(require('../providers/provider-registry')));
155
- const provider = await getProvider(providerId);
156
- console.log(chalk_1.default.yellow(`⚠️ Could not save ${provider?.displayName || providerId} token to config`));
157
- }
158
- }
159
- };
160
- exports.saveProviderTokenToConfig = saveProviderTokenToConfig;
161
- const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
94
+ const configureIDEMCP = async (ide, fraimKey) => {
162
95
  const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
163
96
  console.log(chalk_1.default.blue(`🔧 Configuring ${ide.name}...`));
164
97
  // Create backup if config exists
@@ -193,13 +126,10 @@ const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
193
126
  existingTomlContent = fs_1.default.readFileSync(configPath, 'utf8');
194
127
  console.log(chalk_1.default.gray(` 📋 Found existing TOML config`));
195
128
  }
196
- const newTomlContent = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokens, providerConfigs);
129
+ const newTomlContent = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, {});
197
130
  const { getAllMCPServerIds } = await Promise.resolve().then(() => __importStar(require('../mcp/mcp-server-registry')));
198
131
  const baseServerIds = getAllMCPServerIds();
199
- // Add provider server IDs from tokens
200
- const providerServerIds = Object.keys(tokens).filter(id => tokens[id]);
201
- const serversToAdd = [...baseServerIds, ...providerServerIds];
202
- const mergeResult = (0, mcp_config_generator_1.mergeTomlMCPServers)(existingTomlContent, newTomlContent, serversToAdd);
132
+ const mergeResult = (0, mcp_config_generator_1.mergeTomlMCPServers)(existingTomlContent, newTomlContent, baseServerIds);
203
133
  fs_1.default.writeFileSync(configPath, mergeResult.content);
204
134
  mergeResult.addedServers.forEach(server => {
205
135
  console.log(chalk_1.default.green(` ✅ Added ${server} MCP server`));
@@ -213,14 +143,14 @@ const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
213
143
  }
214
144
  else {
215
145
  // For JSON configs - intelligent merging
216
- const newConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokens, providerConfigs);
146
+ const newConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, {});
217
147
  const newMCPServers = newConfig[serversKey] || newConfig.mcpServers || {};
218
148
  // Merge MCP servers intelligently
219
149
  const mergedMCPServers = { ...existingMCPServers };
220
150
  const addedServers = [];
221
151
  const updatedServers = [];
222
152
  const skippedServers = [];
223
- const alwaysUpdateServers = new Set(['fraim', 'github', 'gitlab', 'jira', 'ado', 'linear']);
153
+ const alwaysUpdateServers = new Set(['fraim']);
224
154
  for (const [serverName, serverConfig] of Object.entries(newMCPServers)) {
225
155
  if (!existingMCPServers[serverName]) {
226
156
  mergedMCPServers[serverName] = serverConfig;
@@ -276,7 +206,7 @@ const listSupportedIDEs = () => {
276
206
  console.log(chalk_1.default.yellow(' Gemini aliases: gemini, gemini-cli, gemini cli'));
277
207
  console.log(chalk_1.default.yellow(' GitHub Copilot CLI aliases: copilot, copilot-cli, github copilot cli'));
278
208
  };
279
- const promptForIDESelection = async (availableIDEs, tokens) => {
209
+ const promptForIDESelection = async (availableIDEs) => {
280
210
  console.log(chalk_1.default.green(`✅ Found ${availableIDEs.length} IDEs that can be configured:\n`));
281
211
  availableIDEs.forEach((ide, index) => {
282
212
  const configExists = fs_1.default.existsSync((0, ide_detector_1.expandPath)(ide.configPath));
@@ -289,14 +219,6 @@ const promptForIDESelection = async (availableIDEs, tokens) => {
289
219
  for (const server of mcp_server_registry_1.BASE_MCP_SERVERS) {
290
220
  console.log(chalk_1.default.gray(` • ${server.id} (${server.description})`));
291
221
  }
292
- // Show provider servers (only if tokens exist)
293
- const allProviders = await (0, provider_registry_1.getAllProviders)();
294
- for (const provider of allProviders) {
295
- const hasToken = tokens?.[provider.id];
296
- if (hasToken && provider.mcpServer) {
297
- console.log(chalk_1.default.gray(` - ${provider.id} (${provider.description})`));
298
- }
299
- }
300
222
  const response = await (0, prompts_1.default)({
301
223
  type: 'text',
302
224
  name: 'selection',
@@ -334,25 +256,8 @@ const runAddIDE = async (options) => {
334
256
  console.log(chalk_1.default.yellow('💡 Please run "fraim setup" first to configure your FRAIM keys.'));
335
257
  process.exit(1);
336
258
  }
337
- const platformTokens = globalConfig.tokens || {};
338
259
  const isConversationalMode = globalConfig.mode === 'conversational';
339
- // Check if any provider tokens exist
340
- const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
341
- const hasAnyToken = allProviderIds.some(id => platformTokens[id]);
342
- if (!hasAnyToken && !isConversationalMode && !options.skipTokenPrompts) {
343
- console.log(chalk_1.default.yellow('⚠️ No provider tokens found in configuration.'));
344
- // Prompt for first integrated provider as default
345
- const integratedProviders = await (0, provider_registry_1.getProvidersWithCapability)('integrated');
346
- const defaultProviderId = integratedProviders[0]?.id;
347
- if (defaultProviderId) {
348
- const token = await promptForProviderTokenIfNeeded(defaultProviderId, false);
349
- if (token) {
350
- await saveProviderTokenToConfig(defaultProviderId, token);
351
- platformTokens[defaultProviderId] = token;
352
- }
353
- }
354
- }
355
- if (isConversationalMode && !hasAnyToken) {
260
+ if (isConversationalMode) {
356
261
  console.log(chalk_1.default.blue('ℹ️ Conversational mode: Configuring MCP without platform integration\n'));
357
262
  }
358
263
  else {
@@ -389,7 +294,7 @@ const runAddIDE = async (options) => {
389
294
  }
390
295
  else {
391
296
  // Interactive selection
392
- idesToConfigure = await promptForIDESelection(detectedIDEs, platformTokens);
297
+ idesToConfigure = await promptForIDESelection(detectedIDEs);
393
298
  }
394
299
  }
395
300
  if (idesToConfigure.length === 0) {
@@ -403,7 +308,7 @@ const runAddIDE = async (options) => {
403
308
  };
404
309
  for (const ide of idesToConfigure) {
405
310
  try {
406
- await configureIDEMCP(ide, globalConfig.fraimKey, platformTokens, globalConfig.providerConfigs);
311
+ await configureIDEMCP(ide, globalConfig.fraimKey);
407
312
  results.successful.push(ide.name);
408
313
  (0, user_config_1.addInstalledIde)((0, ide_detector_1.getAdapterConfigType)(ide));
409
314
  }
@@ -1,275 +1,39 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.addProviderCommand = exports.runAddProvider = void 0;
3
+ exports.addProviderCommand = exports.runAddProvider = exports.explainAgentManagedProviderSetup = void 0;
7
4
  const commander_1 = require("commander");
8
- const chalk_1 = __importDefault(require("chalk"));
9
- const prompts_1 = __importDefault(require("prompts"));
10
- const fs_1 = __importDefault(require("fs"));
11
- const path_1 = __importDefault(require("path"));
12
- const child_process_1 = require("child_process");
13
- const mcp_config_generator_1 = require("../setup/mcp-config-generator");
14
- const ide_detector_1 = require("../setup/ide-detector");
15
- const script_sync_utils_1 = require("../utils/script-sync-utils");
16
- const mcp_config_generator_2 = require("../setup/mcp-config-generator");
17
- const provider_registry_1 = require("../providers/provider-registry");
18
- const provider_prompts_1 = require("../setup/provider-prompts");
19
- const get_provider_client_1 = require("../api/get-provider-client");
20
- const loadGlobalConfig = () => {
21
- const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
22
- if (!fs_1.default.existsSync(globalConfigPath)) {
23
- return null;
24
- }
25
- try {
26
- return JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
27
- }
28
- catch (e) {
29
- return null;
30
- }
31
- };
32
- const saveGlobalConfig = (config) => {
33
- const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
34
- fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
35
- };
36
- const forEachInstalledIDE = async (callback) => {
37
- const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
38
- if (detectedIDEs.length === 0) {
39
- console.log(chalk_1.default.yellow('⚠️ No IDEs detected. MCP configs not updated.'));
40
- return;
41
- }
42
- console.log(chalk_1.default.blue(`\n🔧 Updating ${detectedIDEs.length} IDE configuration(s)...\n`));
43
- for (const ide of detectedIDEs) {
44
- try {
45
- await callback(ide, (0, ide_detector_1.expandPath)(ide.configPath));
46
- }
47
- catch (error) {
48
- console.log(chalk_1.default.red(`❌ Failed to update ${ide.name}: ${error instanceof Error ? error.message : 'Unknown error'}`));
49
- }
50
- }
51
- };
52
- const writeOAuthProviderToIDEConfigs = async (provider, mcpUrl) => {
53
- const urlOnlyEntry = { type: 'http', url: mcpUrl };
54
- await forEachInstalledIDE(async (ide, configPath) => {
55
- if (provider === 'github' && ide.configType === 'copilot-cli') {
56
- console.log(chalk_1.default.gray(`Skipped ${ide.name} GitHub MCP entry (built into Copilot CLI)`));
57
- return;
58
- }
59
- if (ide.configFormat === 'json') {
60
- let ideConfig = {};
61
- if (fs_1.default.existsSync(configPath)) {
62
- ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
63
- }
64
- const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
65
- if (!ideConfig[serversKey]) {
66
- ideConfig[serversKey] = {};
67
- }
68
- ideConfig[serversKey][provider] = ide.configType === 'copilot-cli'
69
- ? { ...urlOnlyEntry, tools: ['*'] }
70
- : urlOnlyEntry;
71
- fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
72
- console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
73
- }
74
- else if (ide.configFormat === 'toml') {
75
- console.log(chalk_1.default.yellow(`⚠️ ${ide.name} uses TOML format — update manually: add [tools.${provider}] url = "${mcpUrl}"`));
76
- }
77
- });
78
- };
79
- const updateIDEConfigs = async (provider, fraimKey, token, providerConfig) => {
80
- const tokenConfig = { [provider]: token };
81
- const providerConfigs = providerConfig ? { [provider]: providerConfig } : {};
82
- await forEachInstalledIDE(async (ide, configPath) => {
83
- if (ide.configFormat === 'json') {
84
- let ideConfig = {};
85
- if (fs_1.default.existsSync(configPath)) {
86
- ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
87
- }
88
- const mcpConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokenConfig, providerConfigs);
89
- const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
90
- if (!ideConfig[serversKey]) {
91
- ideConfig[serversKey] = {};
92
- }
93
- const providerServerKey = provider === 'ado' ? 'ado' : provider;
94
- if (mcpConfig[serversKey] && mcpConfig[serversKey][providerServerKey]) {
95
- ideConfig[serversKey][providerServerKey] = mcpConfig[serversKey][providerServerKey];
96
- fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
97
- console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
98
- }
99
- }
100
- else if (ide.configFormat === 'toml') {
101
- const mcpConfigToml = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, tokenConfig, providerConfigs);
102
- if (fs_1.default.existsSync(configPath)) {
103
- const existingContent = fs_1.default.readFileSync(configPath, 'utf8');
104
- const serversToMerge = [provider === 'ado' ? 'ado' : provider];
105
- const mergeResult = (0, mcp_config_generator_2.mergeTomlMCPServers)(existingContent, mcpConfigToml, serversToMerge);
106
- fs_1.default.writeFileSync(configPath, mergeResult.content);
107
- console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
108
- }
109
- else {
110
- console.log(chalk_1.default.yellow(`⚠️ ${ide.name} config not found, skipping`));
111
- }
112
- }
113
- });
114
- };
115
- const offerModeSwitch = async (config) => {
116
- if (config.mode === 'split') {
117
- console.log(chalk_1.default.gray('\nAlready in split mode.'));
118
- return;
119
- }
120
- console.log(chalk_1.default.blue('\n🔄 Mode Switch Available'));
121
- console.log(chalk_1.default.gray('You can now use split mode (separate platforms for code and issues).'));
122
- console.log(chalk_1.default.gray('Example: GitHub for code, Jira for issue tracking\n'));
123
- // Skip prompt if FRAIM_AUTO_SWITCH_MODE is set (for testing)
124
- const shouldSwitch = process.env.FRAIM_AUTO_SWITCH_MODE === 'true';
125
- if (!shouldSwitch && !process.env.FRAIM_AUTO_SWITCH_MODE) {
126
- const response = await (0, prompts_1.default)({
127
- type: 'confirm',
128
- name: 'switchMode',
129
- message: 'Switch to split mode?',
130
- initial: false
131
- });
132
- if (response.switchMode) {
133
- config.mode = 'split';
134
- saveGlobalConfig(config);
135
- console.log(chalk_1.default.green('✅ Switched to split mode'));
136
- console.log(chalk_1.default.gray('Run "fraim init-project" in your projects to use split mode'));
137
- }
138
- }
139
- else if (shouldSwitch) {
140
- config.mode = 'split';
141
- saveGlobalConfig(config);
142
- console.log(chalk_1.default.green('✅ Switched to split mode'));
143
- console.log(chalk_1.default.gray('Run "fraim init-project" in your projects to use split mode'));
144
- }
145
- };
146
- const runAddProvider = async (provider, options) => {
147
- console.log(chalk_1.default.blue(`\n🔧 Adding ${provider.toUpperCase()} provider...\n`));
148
- // Check global setup exists
149
- const config = loadGlobalConfig();
150
- if (!config) {
151
- console.log(chalk_1.default.red('❌ Global FRAIM setup not found.'));
152
- console.log(chalk_1.default.yellow('Please run: fraim setup --key=<your-fraim-key>'));
153
- process.exit(1);
154
- }
155
- // Initialize tokens object if it doesn't exist
156
- if (!config.tokens) {
157
- config.tokens = {};
158
- }
159
- // Check if provider already configured
160
- const providerName = await (0, provider_registry_1.getProviderDisplayName)(provider);
161
- const hasExistingConfig = config.tokens[provider] ||
162
- (provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl) ||
163
- (provider === 'ado' && config.providerConfigs?.adoConfig?.organization);
164
- if (hasExistingConfig) {
165
- const displayInfo = provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl
166
- ? ` (${config.providerConfigs.jiraConfig.baseUrl})`
167
- : provider === 'ado' && config.providerConfigs?.adoConfig?.organization
168
- ? ` (${config.providerConfigs.adoConfig.organization})`
169
- : '';
170
- console.log(chalk_1.default.yellow(`⚠️ ${providerName} is already configured${displayInfo}`));
171
- // Skip prompt if FRAIM_FORCE_OVERWRITE is set (for testing)
172
- if (!process.env.FRAIM_FORCE_OVERWRITE) {
173
- const response = await (0, prompts_1.default)({
174
- type: 'confirm',
175
- name: 'overwrite',
176
- message: `Overwrite existing ${providerName} configuration?`,
177
- initial: false
178
- });
179
- if (!response.overwrite) {
180
- console.log(chalk_1.default.gray('Cancelled.'));
181
- process.exit(0);
182
- }
183
- }
184
- }
185
- // Check prerequisites for the provider's MCP server command
186
- const providerDefCheck = await (0, provider_registry_1.getProvider)(provider);
187
- if (providerDefCheck?.mcpServer?.type === 'stdio' && providerDefCheck.mcpServer.command && !process.env.FRAIM_SKIP_PREREQ_CHECK) {
188
- const cmd = providerDefCheck.mcpServer.command;
189
- // Only check non-standard commands (npx/node are assumed to be available)
190
- if (!['npx', 'node', 'npm'].includes(cmd)) {
191
- try {
192
- (0, child_process_1.execSync)(`${cmd} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
193
- }
194
- catch {
195
- const installInstructions = {
196
- uvx: 'Install uv first: https://docs.astral.sh/uv/getting-started/installation/\n' +
197
- ' macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n' +
198
- ' Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"',
199
- };
200
- console.log(chalk_1.default.red(`\n❌ "${cmd}" is required for ${providerName} but was not found on your system.\n`));
201
- console.log(chalk_1.default.yellow(installInstructions[cmd] || `Please install "${cmd}" and try again.`));
202
- console.log(chalk_1.default.gray('\nAfter installing, restart your terminal and run this command again.'));
203
- process.exit(1);
204
- }
205
- }
206
- }
207
- // Detect OAuth-first providers: HTTP MCP with no authHeaderTemplate means the IDE
208
- // handles OAuth natively. Skip credential collection and write a URL-only IDE entry.
209
- const providerDefOAuth = await (0, provider_registry_1.getProvider)(provider);
210
- if (providerDefOAuth?.mcpServer?.type === 'http' &&
211
- !providerDefOAuth.mcpServer.authHeaderTemplate) {
212
- const mcpUrl = providerDefOAuth.mcpServer.url;
213
- await writeOAuthProviderToIDEConfigs(provider, mcpUrl);
214
- console.log(chalk_1.default.green(`\n✅ ${providerName} configured for IDE-native OAuth.`));
215
- console.log(chalk_1.default.cyan('\n💡 Next steps:'));
216
- console.log(chalk_1.default.gray(' 1. Restart your IDE(s) to load the new MCP server'));
217
- console.log(chalk_1.default.gray(' 2. On first use, your IDE will prompt you to sign in to ' + providerName));
218
- return;
219
- }
220
- // Get credentials using generic prompt system
221
- try {
222
- // Build provided tokens/configs from CLI options
223
- const providedTokens = {};
224
- const providedConfigs = {};
225
- if (options.token) {
226
- providedTokens[provider] = options.token;
227
- }
228
- // Handle provider-specific config options (e.g., Jira URL/email or ADO organization)
229
- if (options.url || options.email || options.organization) {
230
- providedConfigs[provider] = {
231
- ...(options.url && { baseUrl: options.url.replace(/^https?:\/\//, '').replace(/\/$/, '') }),
232
- ...(options.email && { email: options.email }),
233
- ...(options.organization && { organization: options.organization })
234
- };
235
- }
236
- // Get credentials using generic prompt system
237
- const client = (0, get_provider_client_1.getProviderClient)();
238
- const creds = await (0, provider_prompts_1.promptForProviderCredentials)(client, provider, providedTokens[provider], providedConfigs[provider]);
239
- // Write provider MCP entry directly to IDE configs (token not stored in global config)
240
- await updateIDEConfigs(provider, config.apiKey, creds.token, creds.config);
241
- // Offer mode switch if adding a provider with issues capability
242
- const providerDef = await (0, provider_registry_1.getProvider)(provider);
243
- if (providerDef?.capabilities.includes('issues') && provider === 'jira') {
244
- await offerModeSwitch(config);
245
- }
246
- console.log(chalk_1.default.green('\n🎉 Provider added successfully!'));
247
- console.log(chalk_1.default.cyan('\n💡 Next steps:'));
248
- console.log(chalk_1.default.gray(' 1. Restart your IDE(s) to load new MCP servers'));
249
- if (provider === 'jira' && config.mode === 'split') {
250
- console.log(chalk_1.default.gray(' 2. Run "fraim init-project" in your projects to use split mode'));
251
- }
252
- }
253
- catch (error) {
254
- console.log(chalk_1.default.red(`\n❌ Failed to add provider: ${error instanceof Error ? error.message : 'Unknown error'}`));
255
- process.exit(1);
256
- }
5
+ const MAX_PROVIDER_LABEL_LENGTH = 80;
6
+ const safeProviderLabel = (provider) => {
7
+ const sanitized = provider
8
+ .replace(/\p{C}/gu, '')
9
+ .trim()
10
+ .slice(0, MAX_PROVIDER_LABEL_LENGTH);
11
+ return sanitized || 'this provider';
12
+ };
13
+ const explainAgentManagedProviderSetup = (provider) => {
14
+ const safeProvider = safeProviderLabel(provider);
15
+ return {
16
+ provider: safeProvider,
17
+ changed: false,
18
+ exitCode: 1,
19
+ message: [
20
+ 'Provider setup is managed by your FRAIM agent.',
21
+ `Ask your agent: Use the FRAIM connect-mcp skill to connect ${safeProvider}.`,
22
+ 'The legacy fraim add-provider command made no changes.',
23
+ ].join('\n'),
24
+ };
25
+ };
26
+ exports.explainAgentManagedProviderSetup = explainAgentManagedProviderSetup;
27
+ const runAddProvider = (provider) => {
28
+ const explanation = (0, exports.explainAgentManagedProviderSetup)(provider);
29
+ console.error(explanation.message);
30
+ return explanation;
257
31
  };
258
32
  exports.runAddProvider = runAddProvider;
259
33
  exports.addProviderCommand = new commander_1.Command('add-provider')
260
- .description(`Add or update a provider after initial setup`)
261
- .argument('<provider>', `Provider to add`)
262
- .option('--token <token>', 'Provider token (will prompt if not provided)')
263
- .option('--organization <organization>', 'Organization (for providers that require it)')
264
- .option('--email <email>', 'Email (for providers that require it)')
265
- .option('--url <url>', 'Instance URL (for providers that require it)')
266
- .option('--no-validate', 'Skip token validation')
267
- .action(async (provider, options) => {
268
- if (!(await (0, provider_registry_1.isValidProviderId)(provider))) {
269
- const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
270
- console.log(chalk_1.default.red(`❌ Invalid provider: ${provider}`));
271
- console.log(chalk_1.default.yellow(`Valid providers: ${allProviderIds.join(', ')}`));
272
- process.exit(1);
273
- }
274
- await (0, exports.runAddProvider)(provider, options);
34
+ .description('Deprecated: provider setup is agent-managed through the FRAIM connect-mcp skill')
35
+ .argument('<provider>', 'Provider to connect through your FRAIM agent')
36
+ .action((provider) => {
37
+ const result = (0, exports.runAddProvider)(provider);
38
+ process.exitCode = result.exitCode;
275
39
  });