@intellectronica/ruler 0.3.42 → 0.3.44
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 +98 -11
- package/dist/agents/AbstractAgent.js +3 -2
- package/dist/agents/AgentsMdAgent.js +3 -2
- package/dist/agents/AiderAgent.js +4 -3
- package/dist/agents/AmazonQCliAgent.js +6 -4
- package/dist/agents/AugmentCodeAgent.js +3 -2
- package/dist/agents/CodexCliAgent.js +1 -1
- package/dist/agents/CopilotAgent.js +1 -1
- package/dist/agents/CrushAgent.d.ts +1 -1
- package/dist/agents/CrushAgent.js +15 -6
- package/dist/agents/FirebenderAgent.js +5 -4
- package/dist/agents/GeminiCliAgent.d.ts +1 -0
- package/dist/agents/GeminiCliAgent.js +11 -5
- package/dist/agents/IAgent.d.ts +2 -0
- package/dist/agents/MistralVibeAgent.js +14 -3
- package/dist/agents/OpenCodeAgent.d.ts +1 -1
- package/dist/agents/OpenCodeAgent.js +10 -3
- package/dist/agents/QwenCodeAgent.d.ts +1 -0
- package/dist/agents/QwenCodeAgent.js +9 -3
- package/dist/agents/RooCodeAgent.js +3 -2
- package/dist/agents/ZedAgent.js +3 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/ConfigLoader.d.ts +2 -0
- package/dist/core/ConfigLoader.js +87 -6
- package/dist/core/FileSystemUtils.d.ts +5 -2
- package/dist/core/FileSystemUtils.js +121 -3
- package/dist/core/GitignoreUtils.d.ts +10 -0
- package/dist/core/GitignoreUtils.js +62 -31
- package/dist/core/SkillsProcessor.d.ts +2 -2
- package/dist/core/SkillsProcessor.js +46 -37
- package/dist/core/SkillsUtils.js +4 -1
- package/dist/core/SubagentsProcessor.js +8 -5
- package/dist/core/UnifiedConfigLoader.js +96 -11
- package/dist/core/UnifiedConfigTypes.d.ts +3 -1
- package/dist/core/agent-selection.js +6 -4
- package/dist/core/apply-engine.d.ts +1 -0
- package/dist/core/apply-engine.js +38 -15
- package/dist/core/revert-engine.d.ts +2 -1
- package/dist/core/revert-engine.js +79 -27
- package/dist/lib.js +9 -6
- package/dist/mcp/capabilities.js +2 -2
- package/dist/mcp/merge.js +28 -26
- package/dist/mcp/propagateOpenCodeMcp.d.ts +1 -1
- package/dist/mcp/propagateOpenCodeMcp.js +10 -3
- package/dist/mcp/propagateOpenHandsMcp.d.ts +1 -1
- package/dist/mcp/propagateOpenHandsMcp.js +18 -7
- package/dist/paths/mcp.d.ts +1 -1
- package/dist/paths/mcp.js +12 -5
- package/dist/revert.js +29 -27
- package/dist/vscode/settings.d.ts +1 -1
- package/dist/vscode/settings.js +3 -3
- package/package.json +6 -4
|
@@ -105,6 +105,7 @@ async function createHierarchicalConfiguration(rulerDir, files, config, cliConfi
|
|
|
105
105
|
config,
|
|
106
106
|
concatenatedRules,
|
|
107
107
|
rulerMcpJson,
|
|
108
|
+
projectRoot: directoryRoot,
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
async function loadConfigForRulerDir(rulerDir, cliConfigPath, resolvedNested, localOnly) {
|
|
@@ -139,6 +140,12 @@ function cloneLoadedConfig(config) {
|
|
|
139
140
|
clonedAgentConfigs[agent] = {
|
|
140
141
|
...agentConfig,
|
|
141
142
|
mcp: agentConfig.mcp ? { ...agentConfig.mcp } : undefined,
|
|
143
|
+
mcpServers: agentConfig.mcpServers
|
|
144
|
+
? Object.fromEntries(Object.entries(agentConfig.mcpServers).map(([name, server]) => [
|
|
145
|
+
name,
|
|
146
|
+
{ ...server },
|
|
147
|
+
]))
|
|
148
|
+
: undefined,
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
151
|
return {
|
|
@@ -194,9 +201,10 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
|
|
|
194
201
|
const { dirs: rulerDirs, primaryDir } = await findRulerDirectories(projectRoot, localOnly, false);
|
|
195
202
|
// Warn about legacy mcp.json
|
|
196
203
|
await warnAboutLegacyMcpJson(primaryDir);
|
|
204
|
+
const effectiveProjectRoot = FileSystemUtils.resolveProjectRootForRulerDir(projectRoot, primaryDir);
|
|
197
205
|
// Load the ruler.toml configuration
|
|
198
206
|
const config = await (0, ConfigLoader_1.loadConfig)({
|
|
199
|
-
projectRoot,
|
|
207
|
+
projectRoot: effectiveProjectRoot,
|
|
200
208
|
configPath,
|
|
201
209
|
checkGlobal: !localOnly,
|
|
202
210
|
});
|
|
@@ -210,7 +218,7 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
|
|
|
210
218
|
// Load unified config to get merged MCP configuration
|
|
211
219
|
const { loadUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('./UnifiedConfigLoader')));
|
|
212
220
|
const unifiedConfig = await loadUnifiedConfig({
|
|
213
|
-
projectRoot,
|
|
221
|
+
projectRoot: effectiveProjectRoot,
|
|
214
222
|
configPath,
|
|
215
223
|
checkGlobal: !localOnly,
|
|
216
224
|
});
|
|
@@ -225,6 +233,7 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
|
|
|
225
233
|
config,
|
|
226
234
|
concatenatedRules,
|
|
227
235
|
rulerMcpJson,
|
|
236
|
+
projectRoot: effectiveProjectRoot,
|
|
228
237
|
};
|
|
229
238
|
}
|
|
230
239
|
/**
|
|
@@ -282,7 +291,7 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
|
|
|
282
291
|
(0, constants_1.logInfo)(`Applying rules for ${agent.getName()}...`, dryRun);
|
|
283
292
|
(0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
|
|
284
293
|
const agentConfig = config.agentConfigs[agent.getIdentifier()];
|
|
285
|
-
const agentRulerMcpJson = rulerMcpJson;
|
|
294
|
+
const agentRulerMcpJson = mergeAgentMcpServers(rulerMcpJson, agentConfig);
|
|
286
295
|
// Collect output paths for .gitignore
|
|
287
296
|
const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
|
|
288
297
|
(0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
|
|
@@ -358,6 +367,19 @@ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson,
|
|
|
358
367
|
function shouldUseEngineManagedMcp(agent) {
|
|
359
368
|
return (agent.getIdentifier() === 'codex' || agent.getIdentifier() === 'opencode');
|
|
360
369
|
}
|
|
370
|
+
function mergeAgentMcpServers(rulerMcpJson, agentConfig) {
|
|
371
|
+
const baseServers = rulerMcpJson?.mcpServers && typeof rulerMcpJson.mcpServers === 'object'
|
|
372
|
+
? rulerMcpJson.mcpServers
|
|
373
|
+
: {};
|
|
374
|
+
const agentServers = agentConfig?.mcpServers ?? {};
|
|
375
|
+
const mergedServers = {
|
|
376
|
+
...baseServers,
|
|
377
|
+
...agentServers,
|
|
378
|
+
};
|
|
379
|
+
return Object.keys(mergedServers).length > 0
|
|
380
|
+
? { mcpServers: mergedServers }
|
|
381
|
+
: null;
|
|
382
|
+
}
|
|
361
383
|
async function resolveMcpDestination(agent, agentConfig, projectRoot) {
|
|
362
384
|
if (agentConfig?.outputPathConfig) {
|
|
363
385
|
return path.resolve(projectRoot, agentConfig.outputPathConfig);
|
|
@@ -412,10 +434,10 @@ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig,
|
|
|
412
434
|
}
|
|
413
435
|
const agentMcpJson = sanitizeMcpTimeoutsForAgent(agent, filteredMcpJson, dryRun);
|
|
414
436
|
if (agent.getIdentifier() === 'openhands') {
|
|
415
|
-
return await applyOpenHandsMcpConfiguration(agentMcpJson, dest, cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy, dryRun, verbose, backup);
|
|
437
|
+
return await applyOpenHandsMcpConfiguration(agentMcpJson, dest, projectRoot, cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy, dryRun, verbose, backup);
|
|
416
438
|
}
|
|
417
439
|
if (agent.getIdentifier() === 'opencode') {
|
|
418
|
-
return await applyOpenCodeMcpConfiguration(agentMcpJson, dest, cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy, dryRun, verbose, backup);
|
|
440
|
+
return await applyOpenCodeMcpConfiguration(agentMcpJson, dest, projectRoot, cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy, dryRun, verbose, backup);
|
|
419
441
|
}
|
|
420
442
|
// Agents that handle MCP configuration internally should not have external MCP handling
|
|
421
443
|
if (agent.getIdentifier() === 'zed' ||
|
|
@@ -425,22 +447,22 @@ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig,
|
|
|
425
447
|
(0, constants_1.logVerbose)(`Skipping external MCP config for ${agent.getName()} - handled internally by agent`, verbose);
|
|
426
448
|
return;
|
|
427
449
|
}
|
|
428
|
-
return await applyStandardMcpConfiguration(agent, agentMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
|
|
450
|
+
return await applyStandardMcpConfiguration(agent, agentMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup);
|
|
429
451
|
}
|
|
430
|
-
async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, strategy, dryRun, verbose, backup = true) {
|
|
452
|
+
async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, projectRoot, strategy, dryRun, verbose, backup = true) {
|
|
431
453
|
if (dryRun) {
|
|
432
454
|
(0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose);
|
|
433
455
|
}
|
|
434
456
|
else {
|
|
435
|
-
await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest, backup, strategy);
|
|
457
|
+
await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest, backup, strategy, projectRoot);
|
|
436
458
|
}
|
|
437
459
|
}
|
|
438
|
-
async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, strategy, dryRun, verbose, backup = true) {
|
|
460
|
+
async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, projectRoot, strategy, dryRun, verbose, backup = true) {
|
|
439
461
|
if (dryRun) {
|
|
440
462
|
(0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose);
|
|
441
463
|
}
|
|
442
464
|
else {
|
|
443
|
-
await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest, backup, strategy);
|
|
465
|
+
await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest, backup, strategy, projectRoot);
|
|
444
466
|
}
|
|
445
467
|
}
|
|
446
468
|
/**
|
|
@@ -536,7 +558,7 @@ function transformMcpForFactoryDroid(mcpJson) {
|
|
|
536
558
|
transformedMcp.mcpServers = transformedServers;
|
|
537
559
|
return transformedMcp;
|
|
538
560
|
}
|
|
539
|
-
async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup = true) {
|
|
561
|
+
async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup = true) {
|
|
540
562
|
const strategy = cliMcpStrategy ??
|
|
541
563
|
agentConfig?.mcp?.strategy ??
|
|
542
564
|
config.mcp?.strategy ??
|
|
@@ -554,7 +576,8 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
|
|
|
554
576
|
else {
|
|
555
577
|
// Transform MCP config for agent-specific compatibility
|
|
556
578
|
let mcpToMerge = filteredMcpJson;
|
|
557
|
-
if (agent.getIdentifier() === 'claude'
|
|
579
|
+
if (agent.getIdentifier() === 'claude' ||
|
|
580
|
+
agent.getIdentifier() === 'aider') {
|
|
558
581
|
mcpToMerge = transformMcpForClaude(filteredMcpJson);
|
|
559
582
|
}
|
|
560
583
|
else if (agent.getIdentifier() === 'kilocode') {
|
|
@@ -633,13 +656,13 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
|
|
|
633
656
|
if (currentContent !== newContent) {
|
|
634
657
|
if (backup) {
|
|
635
658
|
const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
|
|
636
|
-
await backupFile(dest);
|
|
659
|
+
await backupFile(dest, projectRoot);
|
|
637
660
|
}
|
|
638
661
|
if (isCodexToml) {
|
|
639
|
-
await FileSystemUtils.writeGeneratedFile(dest, (0, toml_1.stringify)(toWrite));
|
|
662
|
+
await FileSystemUtils.writeGeneratedFile(dest, (0, toml_1.stringify)(toWrite), projectRoot);
|
|
640
663
|
}
|
|
641
664
|
else {
|
|
642
|
-
await (0, mcp_1.writeNativeMcp)(dest, toWrite);
|
|
665
|
+
await (0, mcp_1.writeNativeMcp)(dest, toWrite, projectRoot);
|
|
643
666
|
}
|
|
644
667
|
}
|
|
645
668
|
else {
|
|
@@ -20,7 +20,7 @@ export interface CleanUpResult {
|
|
|
20
20
|
* @param agent The agent to revert
|
|
21
21
|
* @param projectRoot Root directory of the project
|
|
22
22
|
* @param agentConfig Agent-specific configuration
|
|
23
|
-
* @param keepBackups Whether
|
|
23
|
+
* @param keepBackups Whether restored backup files should be preserved
|
|
24
24
|
* @param verbose Whether to enable verbose logging
|
|
25
25
|
* @param dryRun Whether to perform a dry run
|
|
26
26
|
* @returns Promise resolving to revert statistics
|
|
@@ -34,3 +34,4 @@ export declare function revertAgentConfiguration(agent: IAgent, projectRoot: str
|
|
|
34
34
|
* @returns Promise resolving to cleanup statistics
|
|
35
35
|
*/
|
|
36
36
|
export declare function cleanUpAuxiliaryFiles(projectRoot: string, verbose: boolean, dryRun: boolean): Promise<CleanUpResult>;
|
|
37
|
+
export declare function cleanUpAgentDirectories(agent: IAgent, projectRoot: string, agentConfig: IAgentConfig | undefined, verbose: boolean, dryRun: boolean): Promise<number>;
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.revertAgentConfiguration = revertAgentConfiguration;
|
|
37
37
|
exports.cleanUpAuxiliaryFiles = cleanUpAuxiliaryFiles;
|
|
38
|
+
exports.cleanUpAgentDirectories = cleanUpAgentDirectories;
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const fs_1 = require("fs");
|
|
40
41
|
const agent_utils_1 = require("../agents/agent-utils");
|
|
@@ -43,9 +44,20 @@ const constants_1 = require("../constants");
|
|
|
43
44
|
const GitignoreUtils_1 = require("./GitignoreUtils");
|
|
44
45
|
const settings_1 = require("../vscode/settings");
|
|
45
46
|
const path_utils_1 = require("./path-utils");
|
|
47
|
+
const FileSystemUtils_1 = require("./FileSystemUtils");
|
|
46
48
|
const RULER_START_MARKER = '# START Ruler Generated Files';
|
|
47
49
|
const RULER_END_MARKER = '# END Ruler Generated Files';
|
|
48
50
|
const RULER_GENERATED_MARKER = '<!-- Generated by Ruler -->';
|
|
51
|
+
const RULER_SOURCE_MARKER_PREFIXES = [
|
|
52
|
+
'<!-- Source: .ruler/',
|
|
53
|
+
'<!-- Source: ruler/',
|
|
54
|
+
];
|
|
55
|
+
async function resolveMcpPathForRevert(agent, projectRoot, agentConfig) {
|
|
56
|
+
if (agentConfig?.outputPathConfig) {
|
|
57
|
+
return path.resolve(projectRoot, agentConfig.outputPathConfig);
|
|
58
|
+
}
|
|
59
|
+
return await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
|
|
60
|
+
}
|
|
49
61
|
/**
|
|
50
62
|
* Checks if a file exists.
|
|
51
63
|
*/
|
|
@@ -90,6 +102,10 @@ async function hasRulerGeneratedProvenance(filePath, projectRoot) {
|
|
|
90
102
|
if (content.startsWith(RULER_GENERATED_MARKER)) {
|
|
91
103
|
return true;
|
|
92
104
|
}
|
|
105
|
+
const trimmedContent = content.trimStart();
|
|
106
|
+
if (RULER_SOURCE_MARKER_PREFIXES.some((prefix) => trimmedContent.startsWith(prefix))) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
93
109
|
}
|
|
94
110
|
catch {
|
|
95
111
|
return false;
|
|
@@ -109,7 +125,7 @@ async function hasRulerGeneratedProvenance(filePath, projectRoot) {
|
|
|
109
125
|
/**
|
|
110
126
|
* Restores a file from its backup if the backup exists.
|
|
111
127
|
*/
|
|
112
|
-
async function restoreFromBackup(filePath, verbose, dryRun) {
|
|
128
|
+
async function restoreFromBackup(filePath, verbose, dryRun, projectRoot) {
|
|
113
129
|
const backupPath = `${filePath}.bak`;
|
|
114
130
|
const backupExists = await fileExists(backupPath);
|
|
115
131
|
if (!backupExists) {
|
|
@@ -121,6 +137,12 @@ async function restoreFromBackup(filePath, verbose, dryRun) {
|
|
|
121
137
|
(0, constants_1.logVerbose)(`${prefix} Would restore: ${filePath} from backup`, verbose);
|
|
122
138
|
}
|
|
123
139
|
else {
|
|
140
|
+
if (projectRoot) {
|
|
141
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to restore backup through symlinked output path');
|
|
142
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to restore from backup through symlinked backup path');
|
|
143
|
+
}
|
|
144
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(filePath, 'Refusing to restore backup through symlinked output path');
|
|
145
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(backupPath, 'Refusing to restore from symlinked backup path');
|
|
124
146
|
await fs_1.promises.copyFile(backupPath, filePath);
|
|
125
147
|
(0, constants_1.logVerbose)(`${prefix} Restored: ${filePath} from backup`, verbose);
|
|
126
148
|
}
|
|
@@ -150,6 +172,10 @@ async function removeGeneratedFile(filePath, verbose, dryRun, projectRoot) {
|
|
|
150
172
|
(0, constants_1.logVerbose)(`${prefix} Would remove generated file: ${filePath}`, verbose);
|
|
151
173
|
}
|
|
152
174
|
else {
|
|
175
|
+
if (projectRoot) {
|
|
176
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to remove generated file through symlinked path');
|
|
177
|
+
}
|
|
178
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(filePath, 'Refusing to remove symlinked generated file');
|
|
153
179
|
await fs_1.promises.unlink(filePath);
|
|
154
180
|
(0, constants_1.logVerbose)(`${prefix} Removed generated file: ${filePath}`, verbose);
|
|
155
181
|
}
|
|
@@ -158,7 +184,7 @@ async function removeGeneratedFile(filePath, verbose, dryRun, projectRoot) {
|
|
|
158
184
|
/**
|
|
159
185
|
* Removes backup files.
|
|
160
186
|
*/
|
|
161
|
-
async function removeBackupFile(filePath, verbose, dryRun) {
|
|
187
|
+
async function removeBackupFile(filePath, verbose, dryRun, projectRoot) {
|
|
162
188
|
const backupPath = `${filePath}.bak`;
|
|
163
189
|
const backupExists = await fileExists(backupPath);
|
|
164
190
|
if (!backupExists) {
|
|
@@ -169,6 +195,10 @@ async function removeBackupFile(filePath, verbose, dryRun) {
|
|
|
169
195
|
(0, constants_1.logVerbose)(`${prefix} Would remove backup file: ${backupPath}`, verbose);
|
|
170
196
|
}
|
|
171
197
|
else {
|
|
198
|
+
if (projectRoot) {
|
|
199
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to remove backup file through symlinked path');
|
|
200
|
+
}
|
|
201
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(backupPath, 'Refusing to remove symlinked backup file');
|
|
172
202
|
await fs_1.promises.unlink(backupPath);
|
|
173
203
|
(0, constants_1.logVerbose)(`${prefix} Removed backup file: ${backupPath}`, verbose);
|
|
174
204
|
}
|
|
@@ -303,7 +333,6 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
303
333
|
const additionalFiles = [
|
|
304
334
|
'.gemini/settings.json',
|
|
305
335
|
'.mcp.json',
|
|
306
|
-
'.vscode/mcp.json',
|
|
307
336
|
'.cursor/mcp.json',
|
|
308
337
|
'.junie/mcp/mcp.json',
|
|
309
338
|
'.kilocode/mcp.json',
|
|
@@ -320,7 +349,7 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
320
349
|
}
|
|
321
350
|
const backupExists = await fileExists(`${fullPath}.bak`);
|
|
322
351
|
if (backupExists) {
|
|
323
|
-
const restored = await restoreFromBackup(fullPath, verbose, dryRun);
|
|
352
|
+
const restored = await restoreFromBackup(fullPath, verbose, dryRun, projectRoot);
|
|
324
353
|
if (restored) {
|
|
325
354
|
filesRemoved++;
|
|
326
355
|
}
|
|
@@ -333,6 +362,8 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
333
362
|
(0, constants_1.logVerbose)(`${prefix} Would remove additional file: ${fullPath}`, verbose);
|
|
334
363
|
}
|
|
335
364
|
else {
|
|
365
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(fullPath, projectRoot, 'Refusing to remove additional file through symlinked path');
|
|
366
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(fullPath, 'Refusing to remove symlinked additional file');
|
|
336
367
|
await fs_1.promises.unlink(fullPath);
|
|
337
368
|
(0, constants_1.logVerbose)(`${prefix} Removed additional file: ${fullPath}`, verbose);
|
|
338
369
|
}
|
|
@@ -346,7 +377,7 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
346
377
|
const settingsPath = (0, settings_1.getVSCodeSettingsPath)(projectRoot);
|
|
347
378
|
const backupPath = `${settingsPath}.bak`;
|
|
348
379
|
if (await fileExists(backupPath)) {
|
|
349
|
-
const restored = await restoreFromBackup(settingsPath, verbose, dryRun);
|
|
380
|
+
const restored = await restoreFromBackup(settingsPath, verbose, dryRun, projectRoot);
|
|
350
381
|
if (restored) {
|
|
351
382
|
filesRemoved++;
|
|
352
383
|
(0, constants_1.logVerbose)(`${prefix} Restored VSCode settings from backup`, verbose);
|
|
@@ -374,11 +405,12 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
374
405
|
delete settings['augment.advanced'];
|
|
375
406
|
const remainingKeys = Object.keys(settings);
|
|
376
407
|
if (remainingKeys.length === 0) {
|
|
408
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(settingsPath, projectRoot, 'Refusing to remove VSCode settings through symlinked path');
|
|
377
409
|
await fs_1.promises.unlink(settingsPath);
|
|
378
410
|
(0, constants_1.logVerbose)(`${prefix} Removed empty VSCode settings file`, verbose);
|
|
379
411
|
}
|
|
380
412
|
else {
|
|
381
|
-
await (0, settings_1.writeVSCodeSettings)(settingsPath, settings);
|
|
413
|
+
await (0, settings_1.writeVSCodeSettings)(settingsPath, settings, projectRoot);
|
|
382
414
|
(0, constants_1.logVerbose)(`${prefix} Removed augment.advanced section from VSCode settings`, verbose);
|
|
383
415
|
}
|
|
384
416
|
filesRemoved++;
|
|
@@ -399,7 +431,7 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
399
431
|
* @param agent The agent to revert
|
|
400
432
|
* @param projectRoot Root directory of the project
|
|
401
433
|
* @param agentConfig Agent-specific configuration
|
|
402
|
-
* @param keepBackups Whether
|
|
434
|
+
* @param keepBackups Whether restored backup files should be preserved
|
|
403
435
|
* @param verbose Whether to enable verbose logging
|
|
404
436
|
* @param dryRun Whether to perform a dry run
|
|
405
437
|
* @returns Promise resolving to revert statistics
|
|
@@ -411,13 +443,20 @@ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBac
|
|
|
411
443
|
backupsRemoved: 0,
|
|
412
444
|
};
|
|
413
445
|
const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
|
|
446
|
+
const processedPaths = new Set();
|
|
414
447
|
(0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
|
|
415
|
-
|
|
416
|
-
const
|
|
448
|
+
const processPath = async (outputPath) => {
|
|
449
|
+
const resolvedPath = path.resolve(projectRoot, outputPath);
|
|
450
|
+
if (processedPaths.has(resolvedPath)) {
|
|
451
|
+
(0, constants_1.logVerbose)(`Skipping already processed path: ${outputPath}`, verbose);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
processedPaths.add(resolvedPath);
|
|
455
|
+
const restored = await restoreFromBackup(outputPath, verbose, dryRun, projectRoot);
|
|
417
456
|
if (restored) {
|
|
418
457
|
result.restored++;
|
|
419
458
|
if (!keepBackups) {
|
|
420
|
-
const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun);
|
|
459
|
+
const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun, projectRoot);
|
|
421
460
|
if (backupRemoved) {
|
|
422
461
|
result.backupsRemoved++;
|
|
423
462
|
}
|
|
@@ -429,31 +468,19 @@ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBac
|
|
|
429
468
|
result.removed++;
|
|
430
469
|
}
|
|
431
470
|
}
|
|
471
|
+
};
|
|
472
|
+
for (const outputPath of outputPaths) {
|
|
473
|
+
await processPath(outputPath);
|
|
432
474
|
}
|
|
433
475
|
// Handle MCP files
|
|
434
|
-
const mcpPath = await (
|
|
476
|
+
const mcpPath = await resolveMcpPathForRevert(agent, projectRoot, agentConfig);
|
|
435
477
|
if (mcpPath && (0, path_utils_1.isPathInsideOrEqual)(projectRoot, mcpPath)) {
|
|
436
478
|
if (agent.getName() === 'AugmentCode' &&
|
|
437
479
|
mcpPath.endsWith('.vscode/settings.json')) {
|
|
438
480
|
(0, constants_1.logVerbose)(`Skipping MCP handling for AugmentCode settings.json - handled separately`, verbose);
|
|
439
481
|
}
|
|
440
482
|
else {
|
|
441
|
-
|
|
442
|
-
if (mcpRestored) {
|
|
443
|
-
result.restored++;
|
|
444
|
-
if (!keepBackups) {
|
|
445
|
-
const mcpBackupRemoved = await removeBackupFile(mcpPath, verbose, dryRun);
|
|
446
|
-
if (mcpBackupRemoved) {
|
|
447
|
-
result.backupsRemoved++;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun, projectRoot);
|
|
453
|
-
if (mcpRemoved) {
|
|
454
|
-
result.removed++;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
483
|
+
await processPath(mcpPath);
|
|
457
484
|
}
|
|
458
485
|
}
|
|
459
486
|
return result;
|
|
@@ -473,3 +500,28 @@ async function cleanUpAuxiliaryFiles(projectRoot, verbose, dryRun) {
|
|
|
473
500
|
directoriesRemoved,
|
|
474
501
|
};
|
|
475
502
|
}
|
|
503
|
+
async function cleanUpAgentDirectories(agent, projectRoot, agentConfig, verbose, dryRun) {
|
|
504
|
+
const candidateFiles = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
|
|
505
|
+
const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
|
|
506
|
+
if (mcpPath && (0, path_utils_1.isPathInsideOrEqual)(projectRoot, mcpPath)) {
|
|
507
|
+
candidateFiles.push(mcpPath);
|
|
508
|
+
}
|
|
509
|
+
const candidateDirs = [
|
|
510
|
+
...new Set(candidateFiles.map((filePath) => path.dirname(filePath))),
|
|
511
|
+
];
|
|
512
|
+
let directoriesRemoved = 0;
|
|
513
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
514
|
+
for (const candidateDir of candidateDirs) {
|
|
515
|
+
let currentDir = path.resolve(candidateDir);
|
|
516
|
+
while (currentDir !== resolvedProjectRoot &&
|
|
517
|
+
(0, path_utils_1.isPathInsideOrEqual)(resolvedProjectRoot, currentDir)) {
|
|
518
|
+
const removed = await removeEmptyDirectory(currentDir, verbose, dryRun);
|
|
519
|
+
if (!removed) {
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
directoriesRemoved++;
|
|
523
|
+
currentDir = path.dirname(currentDir);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return directoriesRemoved;
|
|
527
|
+
}
|
package/dist/lib.js
CHANGED
|
@@ -102,6 +102,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
102
102
|
let selectedAgents;
|
|
103
103
|
let generatedPaths;
|
|
104
104
|
let loadedConfig;
|
|
105
|
+
let outputProjectRoot = projectRoot;
|
|
105
106
|
if (nested) {
|
|
106
107
|
const hierarchicalConfigs = await (0, apply_engine_1.loadNestedConfigurations)(projectRoot, configPath, localOnly, nested);
|
|
107
108
|
if (hierarchicalConfigs.length === 0) {
|
|
@@ -147,6 +148,8 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
147
148
|
}
|
|
148
149
|
else {
|
|
149
150
|
const singleConfig = await (0, apply_engine_1.loadSingleConfiguration)(projectRoot, configPath, localOnly);
|
|
151
|
+
const singleProjectRoot = singleConfig.projectRoot;
|
|
152
|
+
outputProjectRoot = singleProjectRoot;
|
|
150
153
|
loadedConfig = singleConfig.config;
|
|
151
154
|
singleConfig.config.cliAgents = includedAgents;
|
|
152
155
|
(0, constants_1.logVerbose)(`Loaded configuration with ${Object.keys(singleConfig.config.agentConfigs).length} agent configs`, verbose);
|
|
@@ -158,7 +161,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
158
161
|
const skillsEnabledResolved = resolveSkillsEnabled(skillsEnabled, singleConfig.config.skills?.enabled);
|
|
159
162
|
if (skillsEnabledResolved) {
|
|
160
163
|
const { propagateSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
|
|
161
|
-
await propagateSkills(
|
|
164
|
+
await propagateSkills(singleProjectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
|
|
162
165
|
}
|
|
163
166
|
// Propagate subagents (mirrors skills handling).
|
|
164
167
|
const subagentsEnabledResolvedSingle = resolveSubagentsEnabled(subagentsEnabled, singleConfig.config.subagents?.enabled);
|
|
@@ -166,9 +169,9 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
166
169
|
const backupEnabledResolvedSingle = resolveBackupEnabled(backup, singleConfig.config.backup?.enabled);
|
|
167
170
|
{
|
|
168
171
|
const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
|
|
169
|
-
await propagateSubagents(
|
|
172
|
+
await propagateSubagents(singleProjectRoot, selectedAgents, subagentsEnabledResolvedSingle, subagentsCleanupOrphanedSingle, verbose, dryRun);
|
|
170
173
|
}
|
|
171
|
-
generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig,
|
|
174
|
+
generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, singleProjectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backupEnabledResolvedSingle);
|
|
172
175
|
}
|
|
173
176
|
// Add skills-generated paths to gitignore if skills are enabled
|
|
174
177
|
let allGeneratedPaths = generatedPaths;
|
|
@@ -176,17 +179,17 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
176
179
|
if (skillsEnabledForGitignore) {
|
|
177
180
|
// Skills enabled by default or explicitly
|
|
178
181
|
const { getSkillsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
|
|
179
|
-
const skillsPaths = await getSkillsGitignorePaths(
|
|
182
|
+
const skillsPaths = await getSkillsGitignorePaths(outputProjectRoot, selectedAgents);
|
|
180
183
|
allGeneratedPaths = [...allGeneratedPaths, ...skillsPaths];
|
|
181
184
|
}
|
|
182
185
|
// Add subagents-generated paths to gitignore if subagents are enabled.
|
|
183
186
|
const subagentsEnabledForGitignore = resolveSubagentsEnabled(subagentsEnabled, loadedConfig.subagents?.enabled);
|
|
184
187
|
if (subagentsEnabledForGitignore) {
|
|
185
188
|
const { getSubagentsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
|
|
186
|
-
const subagentPaths = await getSubagentsGitignorePaths(
|
|
189
|
+
const subagentPaths = await getSubagentsGitignorePaths(outputProjectRoot, selectedAgents);
|
|
187
190
|
allGeneratedPaths = [...allGeneratedPaths, ...subagentPaths];
|
|
188
191
|
}
|
|
189
|
-
await (0, apply_engine_1.updateGitignore)(
|
|
192
|
+
await (0, apply_engine_1.updateGitignore)(outputProjectRoot, allGeneratedPaths, loadedConfig, cliGitignoreEnabled, dryRun, cliGitignoreLocal);
|
|
190
193
|
}
|
|
191
194
|
/**
|
|
192
195
|
* Normalizes per-agent config keys to agent identifiers for consistent lookup.
|
package/dist/mcp/capabilities.js
CHANGED
|
@@ -35,8 +35,8 @@ function filterMcpConfigForAgent(mcpConfig, agent) {
|
|
|
35
35
|
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
36
36
|
const config = serverConfig;
|
|
37
37
|
// Determine server type
|
|
38
|
-
const hasCommand =
|
|
39
|
-
const hasUrl =
|
|
38
|
+
const hasCommand = typeof config.command === 'string' || Array.isArray(config.command);
|
|
39
|
+
const hasUrl = typeof config.url === 'string';
|
|
40
40
|
const isStdio = hasCommand && !hasUrl;
|
|
41
41
|
const isRemote = hasUrl && !hasCommand;
|
|
42
42
|
// Include server if agent supports its type
|
package/dist/mcp/merge.js
CHANGED
|
@@ -8,6 +8,29 @@ const MCP_SERVER_KEYS = [
|
|
|
8
8
|
'mcp_servers',
|
|
9
9
|
'context_servers',
|
|
10
10
|
];
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function collectMcpServers(config, serverKey) {
|
|
15
|
+
const servers = {};
|
|
16
|
+
const aliases = MCP_SERVER_KEYS.filter((key) => key !== serverKey);
|
|
17
|
+
for (const key of [...aliases, serverKey]) {
|
|
18
|
+
const value = config[key];
|
|
19
|
+
if (isRecord(value)) {
|
|
20
|
+
Object.assign(servers, value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return servers;
|
|
24
|
+
}
|
|
25
|
+
function removeServerAliases(config, serverKey) {
|
|
26
|
+
const result = { ...config };
|
|
27
|
+
for (const key of MCP_SERVER_KEYS) {
|
|
28
|
+
if (key !== serverKey) {
|
|
29
|
+
delete result[key];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
11
34
|
/**
|
|
12
35
|
* Merge native and incoming MCP server configurations according to strategy.
|
|
13
36
|
* @param base Existing native MCP config object.
|
|
@@ -18,38 +41,17 @@ const MCP_SERVER_KEYS = [
|
|
|
18
41
|
*/
|
|
19
42
|
function mergeMcp(base, incoming, strategy, serverKey) {
|
|
20
43
|
if (strategy === 'overwrite') {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const incomingServers = incoming[serverKey] ||
|
|
24
|
-
incoming.mcpServers ||
|
|
25
|
-
incoming.mcp ||
|
|
26
|
-
{};
|
|
27
|
-
const preservedBase = { ...base };
|
|
28
|
-
for (const key of MCP_SERVER_KEYS) {
|
|
29
|
-
if (key !== serverKey) {
|
|
30
|
-
delete preservedBase[key];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
44
|
+
const incomingServers = collectMcpServers(incoming, serverKey);
|
|
45
|
+
const preservedBase = removeServerAliases(base, serverKey);
|
|
33
46
|
return {
|
|
34
47
|
...preservedBase,
|
|
35
48
|
[serverKey]: incomingServers,
|
|
36
49
|
};
|
|
37
50
|
}
|
|
38
|
-
const baseServers = base
|
|
39
|
-
|
|
40
|
-
base.mcp ||
|
|
41
|
-
{};
|
|
42
|
-
const incomingServers = incoming[serverKey] ||
|
|
43
|
-
incoming.mcpServers ||
|
|
44
|
-
incoming.mcp ||
|
|
45
|
-
{};
|
|
51
|
+
const baseServers = collectMcpServers(base, serverKey);
|
|
52
|
+
const incomingServers = collectMcpServers(incoming, serverKey);
|
|
46
53
|
const mergedServers = { ...baseServers, ...incomingServers };
|
|
47
|
-
const newBase =
|
|
48
|
-
for (const key of MCP_SERVER_KEYS) {
|
|
49
|
-
if (key !== serverKey) {
|
|
50
|
-
delete newBase[key];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
54
|
+
const newBase = removeServerAliases(base, serverKey);
|
|
53
55
|
return {
|
|
54
56
|
...newBase,
|
|
55
57
|
[serverKey]: mergedServers,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { McpStrategy } from '../types';
|
|
2
|
-
export declare function propagateMcpToOpenCode(rulerMcpData: Record<string, unknown> | null, openCodeConfigPath: string, backup?: boolean, strategy?: McpStrategy): Promise<void>;
|
|
2
|
+
export declare function propagateMcpToOpenCode(rulerMcpData: Record<string, unknown> | null, openCodeConfigPath: string, backup?: boolean, strategy?: McpStrategy, containmentRoot?: string): Promise<void>;
|
|
@@ -91,8 +91,11 @@ function transformToOpenCodeFormat(rulerMcp) {
|
|
|
91
91
|
mcp: openCodeServers,
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
-
async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true, strategy = 'merge') {
|
|
94
|
+
async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true, strategy = 'merge', containmentRoot) {
|
|
95
95
|
const rulerMcp = rulerMcpData || {};
|
|
96
|
+
if (containmentRoot) {
|
|
97
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(openCodeConfigPath, containmentRoot, 'Refusing to write generated file outside project');
|
|
98
|
+
}
|
|
96
99
|
// Read existing OpenCode config if it exists
|
|
97
100
|
let existingConfig = {};
|
|
98
101
|
let existingContent;
|
|
@@ -125,10 +128,14 @@ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup =
|
|
|
125
128
|
...transformedConfig.mcp,
|
|
126
129
|
},
|
|
127
130
|
};
|
|
131
|
+
const finalContent = JSON.stringify(finalConfig, null, 2) + '\n';
|
|
132
|
+
if (existingContent === finalContent) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
128
135
|
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openCodeConfigPath));
|
|
129
136
|
if (backup) {
|
|
130
137
|
const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
|
|
131
|
-
await backupFile(openCodeConfigPath);
|
|
138
|
+
await backupFile(openCodeConfigPath, containmentRoot);
|
|
132
139
|
}
|
|
133
|
-
await
|
|
140
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(openCodeConfigPath, finalContent, containmentRoot);
|
|
134
141
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { McpStrategy } from '../types';
|
|
2
|
-
export declare function propagateMcpToOpenHands(rulerMcpData: Record<string, unknown> | null, openHandsConfigPath: string, backup?: boolean, strategy?: McpStrategy): Promise<void>;
|
|
2
|
+
export declare function propagateMcpToOpenHands(rulerMcpData: Record<string, unknown> | null, openHandsConfigPath: string, backup?: boolean, strategy?: McpStrategy, containmentRoot?: string): Promise<void>;
|
|
@@ -67,10 +67,14 @@ function extractApiKey(headers) {
|
|
|
67
67
|
}
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
|
-
function createRemoteServerEntry(url, headers) {
|
|
70
|
+
function createRemoteServerEntry(name, url, headers) {
|
|
71
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
71
72
|
const apiKey = extractApiKey(headers);
|
|
72
|
-
if (
|
|
73
|
-
|
|
73
|
+
if (hasHeaders) {
|
|
74
|
+
if (apiKey) {
|
|
75
|
+
return { url, api_key: apiKey };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`OpenHands MCP remote server "${name}" has unsupported headers. OpenHands config.toml can only represent a Bearer Authorization header as api_key.`);
|
|
74
78
|
}
|
|
75
79
|
return url;
|
|
76
80
|
}
|
|
@@ -89,8 +93,11 @@ function normalizeRemoteServerArray(entries) {
|
|
|
89
93
|
// All entries are strings, keep as is
|
|
90
94
|
return entries;
|
|
91
95
|
}
|
|
92
|
-
async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true, strategy = 'merge') {
|
|
96
|
+
async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true, strategy = 'merge', containmentRoot) {
|
|
93
97
|
const rulerMcp = rulerMcpData || {};
|
|
98
|
+
if (containmentRoot) {
|
|
99
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(openHandsConfigPath, containmentRoot, 'Refusing to write generated file outside project');
|
|
100
|
+
}
|
|
94
101
|
// Always use the legacy Ruler MCP config format as input (top-level "mcpServers" key)
|
|
95
102
|
const rulerServers = rulerMcp.mcpServers || {};
|
|
96
103
|
// Return early if no servers to process
|
|
@@ -162,7 +169,7 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
|
|
|
162
169
|
else if (serverDef.url) {
|
|
163
170
|
// Remote server
|
|
164
171
|
const classification = classifyRemoteServer(serverDef.url);
|
|
165
|
-
const entry = createRemoteServerEntry(serverDef.url, serverDef.headers);
|
|
172
|
+
const entry = createRemoteServerEntry(name, serverDef.url, serverDef.headers);
|
|
166
173
|
if (classification === 'sse') {
|
|
167
174
|
existingSseServers.set(serverDef.url, entry);
|
|
168
175
|
}
|
|
@@ -176,10 +183,14 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
|
|
|
176
183
|
config.mcp.stdio_servers = Array.from(existingStdioServers.values());
|
|
177
184
|
config.mcp.sse_servers = normalizeRemoteServerArray(Array.from(existingSseServers.values()));
|
|
178
185
|
config.mcp.shttp_servers = normalizeRemoteServerArray(Array.from(existingShttpServers.values()));
|
|
186
|
+
const finalContent = (0, toml_1.stringify)(config);
|
|
187
|
+
if (tomlContent === finalContent) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
179
190
|
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openHandsConfigPath));
|
|
180
191
|
if (backup) {
|
|
181
192
|
const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
|
|
182
|
-
await backupFile(openHandsConfigPath);
|
|
193
|
+
await backupFile(openHandsConfigPath, containmentRoot);
|
|
183
194
|
}
|
|
184
|
-
await
|
|
195
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(openHandsConfigPath, finalContent, containmentRoot);
|
|
185
196
|
}
|
package/dist/paths/mcp.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export declare function readNativeMcp(filePath: string): Promise<Record<string,
|
|
|
5
5
|
/** Read native Codex TOML MCP config from disk, or return empty object if missing. */
|
|
6
6
|
export declare function readNativeMcpToml(filePath: string, parseToml: (text: string) => Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
7
7
|
/** Write native MCP config to disk, creating parent directories as needed. */
|
|
8
|
-
export declare function writeNativeMcp(filePath: string, data: unknown): Promise<void>;
|
|
8
|
+
export declare function writeNativeMcp(filePath: string, data: unknown, containmentRoot?: string): Promise<void>;
|