@intellectronica/ruler 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -633,7 +633,7 @@ For agents that support MCP but don't have native skills support, Ruler automati
633
633
 
634
634
  1. Copies skills to `.skillz/` directory
635
635
  2. Configures a Skillz MCP server in the agent's configuration
636
- 3. Uses `uvx` to launch the server with the absolute path to `.skillz`
636
+ 3. Uses `uvx` to launch the server with the project-relative path to `.skillz`
637
637
 
638
638
  Agents using native skills support (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Mistral Vibe, Roo Code, Gemini CLI, and Cursor) **do not** use the Skillz MCP server and instead use their own native skills directories.
639
639
 
@@ -642,7 +642,7 @@ Example auto-generated MCP server configuration:
642
642
  ```toml
643
643
  [mcp_servers.skillz]
644
644
  command = "uvx"
645
- args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
645
+ args = ["skillz@latest", ".skillz"]
646
646
  ```
647
647
 
648
648
  ### `.gitignore` Integration
@@ -79,6 +79,13 @@ class AbstractAgent {
79
79
  supportsMcpRemote() {
80
80
  return false;
81
81
  }
82
+ /**
83
+ * Returns whether this agent supports MCP server timeout configuration.
84
+ * Defaults to false if not overridden.
85
+ */
86
+ supportsMcpTimeout() {
87
+ return false;
88
+ }
82
89
  /**
83
90
  * Returns whether this agent has native skills support.
84
91
  * Defaults to false if not overridden.
@@ -95,6 +95,9 @@ class OpenCodeAgent {
95
95
  supportsMcpRemote() {
96
96
  return true;
97
97
  }
98
+ supportsMcpTimeout() {
99
+ return true;
100
+ }
98
101
  supportsNativeSkills() {
99
102
  return true;
100
103
  }
@@ -808,7 +808,7 @@ async function propagateSkillsForSkillz(projectRoot, options) {
808
808
  if (options.dryRun) {
809
809
  return [
810
810
  `Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.SKILLZ_DIR}`,
811
- `Configure Skillz MCP server with absolute path to ${constants_1.SKILLZ_DIR}`,
811
+ `Configure Skillz MCP server with path to ${constants_1.SKILLZ_DIR}`,
812
812
  ];
813
813
  }
814
814
  // Use atomic replace: copy to temp, then rename
@@ -843,11 +843,11 @@ async function propagateSkillsForSkillz(projectRoot, options) {
843
843
  * Builds MCP config for Skillz server.
844
844
  */
845
845
  function buildSkillzMcpConfig(projectRoot) {
846
- const skillzAbsPath = path.resolve(projectRoot, constants_1.SKILLZ_DIR);
846
+ void projectRoot;
847
847
  return {
848
848
  [constants_1.SKILLZ_MCP_SERVER_NAME]: {
849
849
  command: 'uvx',
850
- args: ['skillz@latest', skillzAbsPath],
850
+ args: ['skillz@latest', constants_1.SKILLZ_DIR],
851
851
  },
852
852
  };
853
853
  }
@@ -182,6 +182,9 @@ async function loadUnifiedConfig(options) {
182
182
  if (serverDef.headers && typeof serverDef.headers === 'object') {
183
183
  server.headers = Object.fromEntries(Object.entries(serverDef.headers).filter(([, v]) => typeof v === 'string'));
184
184
  }
185
+ if (typeof serverDef.timeout === 'number') {
186
+ server.timeout = serverDef.timeout;
187
+ }
185
188
  // Validate server configuration
186
189
  const hasCommand = !!server.command;
187
190
  const hasUrl = !!server.url;
@@ -301,6 +304,9 @@ async function loadUnifiedConfig(options) {
301
304
  if (def.headers && typeof def.headers === 'object') {
302
305
  server.headers = Object.fromEntries(Object.entries(def.headers).filter(([, v]) => typeof v === 'string'));
303
306
  }
307
+ if (typeof def.timeout === 'number') {
308
+ server.timeout = def.timeout;
309
+ }
304
310
  // Derive type
305
311
  if (server.url)
306
312
  server.type = 'remote';
@@ -260,13 +260,12 @@ async function processSingleConfiguration(agents, configuration, projectRoot, ve
260
260
  return await applyConfigurationsToAgents(agents, configuration.concatenatedRules, configuration.rulerMcpJson, configuration.config, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
261
261
  }
262
262
  /**
263
- * Adds Skillz MCP server to rulerMcpJson if skills exist and any agent needs it.
263
+ * Adds Skillz MCP server to rulerMcpJson if skills exist and this agent needs it.
264
264
  * Returns augmented MCP config or original if no changes needed.
265
265
  */
266
- async function addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, verbose) {
267
- // Check if any agent supports MCP stdio but not native skills
268
- const hasAgentNeedingSkillz = agents.some((agent) => agent.supportsMcpStdio?.() && !agent.supportsNativeSkills?.());
269
- if (!hasAgentNeedingSkillz) {
266
+ async function addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agent, verbose) {
267
+ // Check if this agent supports MCP stdio but not native skills
268
+ if (!agent.supportsMcpStdio?.() || agent.supportsNativeSkills?.()) {
270
269
  return rulerMcpJson;
271
270
  }
272
271
  // Check if .skillz directory exists
@@ -280,7 +279,7 @@ async function addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, ver
280
279
  // Initialize empty config if null
281
280
  const baseConfig = rulerMcpJson || { mcpServers: {} };
282
281
  const mcpServers = baseConfig.mcpServers || {};
283
- (0, constants_1.logVerbose)('Adding Skillz MCP server to configuration for agents that need it', verbose);
282
+ (0, constants_1.logVerbose)(`Adding Skillz MCP server to configuration for ${agent.getName()}`, verbose);
284
283
  return {
285
284
  ...baseConfig,
286
285
  mcpServers: {
@@ -308,17 +307,13 @@ async function addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, ver
308
307
  async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true, skillsEnabled = true) {
309
308
  const generatedPaths = [];
310
309
  let agentsMdWritten = false;
311
- // Add Skillz MCP server to rulerMcpJson if skills are enabled
312
- // This must happen before calling agent.applyRulerConfig() so that agents
313
- // that handle MCP internally (e.g. Codex, Gemini) receive the Skillz server
314
- let augmentedRulerMcpJson = rulerMcpJson;
315
- if (skillsEnabled && !dryRun) {
316
- augmentedRulerMcpJson = await addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, verbose);
317
- }
318
310
  for (const agent of agents) {
319
311
  (0, constants_1.logInfo)(`Applying rules for ${agent.getName()}...`, dryRun);
320
312
  (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
321
313
  const agentConfig = config.agentConfigs[agent.getIdentifier()];
314
+ const agentRulerMcpJson = skillsEnabled && !dryRun
315
+ ? await addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agent, verbose)
316
+ : rulerMcpJson;
322
317
  // Collect output paths for .gitignore
323
318
  const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
324
319
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
@@ -344,7 +339,7 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
344
339
  }
345
340
  }
346
341
  let finalAgentConfig = agentConfig;
347
- if (agent.getIdentifier() === 'augmentcode' && augmentedRulerMcpJson) {
342
+ if (agent.getIdentifier() === 'augmentcode' && agentRulerMcpJson) {
348
343
  const resolvedStrategy = cliMcpStrategy ??
349
344
  agentConfig?.mcp?.strategy ??
350
345
  config.mcp?.strategy ??
@@ -358,11 +353,11 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
358
353
  };
359
354
  }
360
355
  if (!skipApplyForThisAgent) {
361
- await agent.applyRulerConfig(concatenatedRules, projectRoot, augmentedRulerMcpJson, finalAgentConfig, backup);
356
+ await agent.applyRulerConfig(concatenatedRules, projectRoot, agentRulerMcpJson, finalAgentConfig, backup);
362
357
  }
363
358
  }
364
359
  // Handle MCP configuration
365
- await handleMcpConfiguration(agent, agentConfig, config, augmentedRulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
360
+ await handleMcpConfiguration(agent, agentConfig, config, agentRulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
366
361
  }
367
362
  return generatedPaths;
368
363
  }
@@ -427,17 +422,49 @@ async function updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, back
427
422
  }
428
423
  }
429
424
  }
425
+ function sanitizeMcpTimeoutsForAgent(agent, mcpJson, dryRun) {
426
+ if (agent.supportsMcpTimeout?.()) {
427
+ return mcpJson;
428
+ }
429
+ if (!mcpJson.mcpServers || typeof mcpJson.mcpServers !== 'object') {
430
+ return mcpJson;
431
+ }
432
+ const servers = mcpJson.mcpServers;
433
+ const sanitizedServers = {};
434
+ const strippedTimeouts = [];
435
+ for (const [name, serverDef] of Object.entries(servers)) {
436
+ if (serverDef && typeof serverDef === 'object') {
437
+ const copy = { ...serverDef };
438
+ if ('timeout' in copy) {
439
+ delete copy.timeout;
440
+ strippedTimeouts.push(name);
441
+ }
442
+ sanitizedServers[name] = copy;
443
+ }
444
+ else {
445
+ sanitizedServers[name] = serverDef;
446
+ }
447
+ }
448
+ if (strippedTimeouts.length > 0) {
449
+ (0, constants_1.logWarn)(`${agent.getName()} does not support MCP server timeout configuration; ignoring timeout for: ${strippedTimeouts.join(', ')}`, dryRun);
450
+ }
451
+ return {
452
+ ...mcpJson,
453
+ mcpServers: sanitizedServers,
454
+ };
455
+ }
430
456
  async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup = true) {
431
457
  // Prevent writing MCP configs outside the project root (e.g., legacy home-directory targets)
432
458
  if (!dest.startsWith(projectRoot)) {
433
459
  (0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} because target path is outside project: ${dest}`, verbose);
434
460
  return;
435
461
  }
462
+ const agentMcpJson = sanitizeMcpTimeoutsForAgent(agent, filteredMcpJson, dryRun);
436
463
  if (agent.getIdentifier() === 'openhands') {
437
- return await applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
464
+ return await applyOpenHandsMcpConfiguration(agentMcpJson, dest, dryRun, verbose, backup);
438
465
  }
439
466
  if (agent.getIdentifier() === 'opencode') {
440
- return await applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
467
+ return await applyOpenCodeMcpConfiguration(agentMcpJson, dest, dryRun, verbose, backup);
441
468
  }
442
469
  // Agents that handle MCP configuration internally should not have external MCP handling
443
470
  if (agent.getIdentifier() === 'zed' ||
@@ -447,7 +474,7 @@ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig,
447
474
  (0, constants_1.logVerbose)(`Skipping external MCP config for ${agent.getName()} - handled internally by agent`, verbose);
448
475
  return;
449
476
  }
450
- return await applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
477
+ return await applyStandardMcpConfiguration(agent, agentMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
451
478
  }
452
479
  async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
453
480
  if (dryRun) {
@@ -63,6 +63,9 @@ function transformToOpenCodeFormat(rulerMcp) {
63
63
  if (serverDef.headers) {
64
64
  openCodeServer.headers = serverDef.headers;
65
65
  }
66
+ if (typeof serverDef.timeout === 'number') {
67
+ openCodeServer.timeout = serverDef.timeout;
68
+ }
66
69
  }
67
70
  else if (isLocalServer(serverDef)) {
68
71
  openCodeServer.type = 'local';
@@ -74,6 +77,9 @@ function transformToOpenCodeFormat(rulerMcp) {
74
77
  if (serverDef.env) {
75
78
  openCodeServer.environment = serverDef.env;
76
79
  }
80
+ if (typeof serverDef.timeout === 'number') {
81
+ openCodeServer.timeout = serverDef.timeout;
82
+ }
77
83
  }
78
84
  else {
79
85
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {