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.
- package/dist/src/cli/commands/add-ide.js +37 -132
- package/dist/src/cli/commands/add-provider.js +32 -268
- package/dist/src/cli/commands/learning-usage.js +412 -0
- package/dist/src/cli/commands/login.js +5 -5
- package/dist/src/cli/commands/setup.js +15 -52
- package/dist/src/cli/commands/sync.js +111 -80
- package/dist/src/cli/fraim.js +1 -42
- package/dist/src/cli/mcp/ide-formats.js +1 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +3 -3
- package/dist/src/cli/providers/local-provider-registry.js +4 -4
- package/dist/src/cli/setup/auto-mcp-setup.js +4 -13
- package/dist/src/cli/utils/remote-sync.js +41 -25
- package/dist/src/core/ai-mentor.js +27 -14
- package/dist/src/core/config-loader.js +48 -3
- package/dist/src/core/fraim-config-schema.generated.js +18 -0
- package/dist/src/core/handoff-contracts.js +37 -1
- package/dist/src/core/job-phases.js +2 -14
- package/dist/src/core/resolve-phase-edge.js +75 -0
- package/dist/src/core/types.js +7 -1
- package/dist/src/core/utils/git-utils.js +24 -14
- package/dist/src/core/utils/project-fraim-paths.js +16 -1
- package/dist/src/first-run/types.js +1 -1
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +8 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +448 -95
- package/dist/src/local-mcp-server/learning-firing-parser.js +247 -0
- package/dist/src/local-mcp-server/learning-usage-analysis.js +347 -0
- package/dist/src/local-mcp-server/learning-usage-attestation.js +191 -0
- package/dist/src/local-mcp-server/learning-usage-command.js +408 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +79 -0
- package/dist/src/local-mcp-server/learning-usage-store.js +417 -0
- package/dist/src/local-mcp-server/stdio-server.js +43 -0
- package/dist/src/services/provider-service.js +4 -4
- 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.
|
|
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 =
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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'
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
261
|
-
.argument('<provider>',
|
|
262
|
-
.
|
|
263
|
-
|
|
264
|
-
.
|
|
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
|
});
|