@intellectronica/ruler 0.3.19 → 0.3.21

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
@@ -84,6 +84,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
84
84
  | Warp | `WARP.md` | - |
85
85
  | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` |
86
86
  | Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) |
87
+ | Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` |
87
88
 
88
89
  ## Getting Started
89
90
 
@@ -319,7 +320,7 @@ ruler revert [options]
319
320
  | Option | Description |
320
321
  | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
321
322
  | `--project-root <path>` | Path to your project's root (default: current directory) |
322
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
323
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, mistral, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
323
324
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
324
325
  | `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
325
326
  | `--dry-run` | Preview changes without actually reverting files |
@@ -69,17 +69,35 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
69
69
  const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
70
70
  if (mcpEnabled && rulerMcpJson) {
71
71
  const strategy = agentConfig?.mcp?.strategy ?? 'merge';
72
+ // Gemini CLI (since v0.21.0) no longer accepts the "type" field in MCP server entries.
73
+ // Following the MCP spec update from Nov 25, 2025, the transport type is now inferred
74
+ // from the presence of specific keys (command/args -> stdio, url -> sse/http).
75
+ // Strip 'type' field from all incoming servers before merging.
76
+ const stripTypeField = (servers) => {
77
+ const cleaned = {};
78
+ for (const [name, def] of Object.entries(servers)) {
79
+ if (def && typeof def === 'object') {
80
+ const copy = { ...def };
81
+ delete copy.type;
82
+ cleaned[name] = copy;
83
+ }
84
+ else {
85
+ cleaned[name] = def;
86
+ }
87
+ }
88
+ return cleaned;
89
+ };
72
90
  if (strategy === 'overwrite') {
73
91
  // For overwrite, preserve existing settings except MCP servers
74
92
  const incomingServers = rulerMcpJson.mcpServers || {};
75
- updated[this.getMcpServerKey()] = incomingServers;
93
+ updated[this.getMcpServerKey()] = stripTypeField(incomingServers);
76
94
  }
77
95
  else {
78
96
  // For merge strategy, merge with existing MCP servers
79
97
  const baseServers = existingSettings[this.getMcpServerKey()] || {};
80
98
  const incomingServers = rulerMcpJson.mcpServers || {};
81
99
  const mergedServers = { ...baseServers, ...incomingServers };
82
- updated[this.getMcpServerKey()] = mergedServers;
100
+ updated[this.getMcpServerKey()] = stripTypeField(mergedServers);
83
101
  }
84
102
  }
85
103
  await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MistralVibeAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs_1 = require("fs");
39
+ const toml_1 = require("@iarna/toml");
40
+ const AgentsMdAgent_1 = require("./AgentsMdAgent");
41
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
42
+ const constants_1 = require("../constants");
43
+ /**
44
+ * Mistral Vibe CLI agent adapter.
45
+ * Propagates rules to AGENTS.md and MCP servers to .vibe/config.toml.
46
+ */
47
+ class MistralVibeAgent {
48
+ constructor() {
49
+ this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
50
+ }
51
+ getIdentifier() {
52
+ return 'mistral';
53
+ }
54
+ getName() {
55
+ return 'Mistral';
56
+ }
57
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
58
+ // First perform idempotent AGENTS.md write via composed AgentsMdAgent
59
+ await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
60
+ outputPath: agentConfig?.outputPath ||
61
+ agentConfig?.outputPathInstructions ||
62
+ undefined,
63
+ }, backup);
64
+ // Handle MCP configuration
65
+ const defaults = this.getDefaultOutputPath(projectRoot);
66
+ const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
67
+ if (mcpEnabled && rulerMcpJson) {
68
+ // Apply MCP server filtering and transformation
69
+ const { filterMcpConfigForAgent } = await Promise.resolve().then(() => __importStar(require('../mcp/capabilities')));
70
+ const filteredMcpConfig = filterMcpConfigForAgent(rulerMcpJson, this);
71
+ if (!filteredMcpConfig) {
72
+ return; // No compatible servers found
73
+ }
74
+ const filteredRulerMcpJson = filteredMcpConfig;
75
+ // Determine the config file path
76
+ const configPath = agentConfig?.outputPathConfig ?? defaults.config;
77
+ // Ensure the parent directory exists
78
+ await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
79
+ // Get the merge strategy
80
+ const strategy = agentConfig?.mcp?.strategy ?? 'merge';
81
+ // Transform ruler MCP servers to Vibe format
82
+ const rulerServers = filteredRulerMcpJson.mcpServers || {};
83
+ const vibeServers = [];
84
+ for (const [serverName, serverConfig] of Object.entries(rulerServers)) {
85
+ const vibeServer = {
86
+ name: serverName,
87
+ transport: this.determineTransport(serverConfig),
88
+ };
89
+ // Handle stdio servers
90
+ if (serverConfig.command) {
91
+ vibeServer.command = serverConfig.command;
92
+ if (serverConfig.args) {
93
+ vibeServer.args = serverConfig.args;
94
+ }
95
+ }
96
+ // Handle remote servers
97
+ if (serverConfig.url) {
98
+ vibeServer.url = serverConfig.url;
99
+ }
100
+ // Handle headers
101
+ if (serverConfig.headers) {
102
+ vibeServer.headers = serverConfig.headers;
103
+ }
104
+ // Handle env
105
+ if (serverConfig.env) {
106
+ vibeServer.env = serverConfig.env;
107
+ }
108
+ vibeServers.push(vibeServer);
109
+ }
110
+ // Read existing TOML config if it exists
111
+ let existingConfig = {};
112
+ try {
113
+ const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
114
+ existingConfig = (0, toml_1.parse)(existingContent);
115
+ }
116
+ catch {
117
+ // File doesn't exist or can't be parsed, use empty config
118
+ }
119
+ // Create the updated config
120
+ const updatedConfig = { ...existingConfig };
121
+ if (strategy === 'overwrite') {
122
+ // For overwrite strategy, replace the entire mcp_servers array
123
+ updatedConfig.mcp_servers = vibeServers;
124
+ }
125
+ else {
126
+ // For merge strategy, merge by server name
127
+ const existingServers = updatedConfig.mcp_servers || [];
128
+ // Keep existing servers that aren't being overwritten by ruler
129
+ const mergedServers = existingServers.filter((s) => !rulerServers[s.name]);
130
+ // Add all ruler servers
131
+ mergedServers.push(...vibeServers);
132
+ updatedConfig.mcp_servers = mergedServers;
133
+ }
134
+ // Convert to TOML and write
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ const tomlContent = (0, toml_1.stringify)(updatedConfig);
137
+ await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
138
+ }
139
+ }
140
+ /**
141
+ * Determines the transport type based on server configuration.
142
+ */
143
+ determineTransport(server) {
144
+ if (server.command) {
145
+ return 'stdio';
146
+ }
147
+ if (server.url) {
148
+ // Default to http for remote servers
149
+ // Could potentially detect streamable-http based on URL patterns if needed
150
+ return 'http';
151
+ }
152
+ return 'stdio';
153
+ }
154
+ getDefaultOutputPath(projectRoot) {
155
+ return {
156
+ instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
157
+ config: path.join(projectRoot, '.vibe', 'config.toml'),
158
+ };
159
+ }
160
+ supportsMcpStdio() {
161
+ return true;
162
+ }
163
+ supportsMcpRemote() {
164
+ return true; // Mistral Vibe supports http and streamable-http transports
165
+ }
166
+ supportsNativeSkills() {
167
+ // Mistral Vibe supports native skills in .vibe/skills/
168
+ return true;
169
+ }
170
+ }
171
+ exports.MistralVibeAgent = MistralVibeAgent;
@@ -32,6 +32,7 @@ const TraeAgent_1 = require("./TraeAgent");
32
32
  const AmazonQCliAgent_1 = require("./AmazonQCliAgent");
33
33
  const FirebenderAgent_1 = require("./FirebenderAgent");
34
34
  const AntigravityAgent_1 = require("./AntigravityAgent");
35
+ const MistralVibeAgent_1 = require("./MistralVibeAgent");
35
36
  exports.allAgents = [
36
37
  new CopilotAgent_1.CopilotAgent(),
37
38
  new ClaudeAgent_1.ClaudeAgent(),
@@ -61,6 +62,7 @@ exports.allAgents = [
61
62
  new AmazonQCliAgent_1.AmazonQCliAgent(),
62
63
  new FirebenderAgent_1.FirebenderAgent(),
63
64
  new AntigravityAgent_1.AntigravityAgent(),
65
+ new MistralVibeAgent_1.MistralVibeAgent(),
64
66
  ];
65
67
  /**
66
68
  * Generates a comma-separated list of agent identifiers for CLI help text.
@@ -43,11 +43,20 @@ const os = __importStar(require("os"));
43
43
  const fs = __importStar(require("fs/promises"));
44
44
  const constants_1 = require("../constants");
45
45
  const ConfigLoader_1 = require("../core/ConfigLoader");
46
+ function assertNotInsideRulerDir(projectRoot) {
47
+ const normalized = path.resolve(projectRoot);
48
+ const segments = normalized.split(path.sep);
49
+ if (segments.includes('.ruler')) {
50
+ console.error(`${constants_1.ERROR_PREFIX} Cannot run from inside a .ruler directory. Please run from your project root.`);
51
+ process.exit(1);
52
+ }
53
+ }
46
54
  /**
47
55
  * Handler for the 'apply' command.
48
56
  */
49
57
  async function applyHandler(argv) {
50
58
  const projectRoot = argv['project-root'];
59
+ assertNotInsideRulerDir(projectRoot);
51
60
  const agents = argv.agents
52
61
  ? argv.agents.split(',').map((a) => a.trim())
53
62
  : undefined;
@@ -192,6 +201,7 @@ async function initHandler(argv) {
192
201
  */
193
202
  async function revertHandler(argv) {
194
203
  const projectRoot = argv['project-root'];
204
+ assertNotInsideRulerDir(projectRoot);
195
205
  const agents = argv.agents
196
206
  ? argv.agents.split(',').map((a) => a.trim())
197
207
  : undefined;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.GOOSE_SKILLS_PATH = exports.OPENCODE_SKILLS_PATH = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
3
+ exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.VIBE_SKILLS_PATH = exports.GOOSE_SKILLS_PATH = exports.OPENCODE_SKILLS_PATH = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
4
4
  exports.actionPrefix = actionPrefix;
5
5
  exports.createRulerError = createRulerError;
6
6
  exports.logVerbose = logVerbose;
@@ -56,6 +56,7 @@ exports.CLAUDE_SKILLS_PATH = '.claude/skills';
56
56
  exports.CODEX_SKILLS_PATH = '.codex/skills';
57
57
  exports.OPENCODE_SKILLS_PATH = '.opencode/skill';
58
58
  exports.GOOSE_SKILLS_PATH = '.agents/skills';
59
+ exports.VIBE_SKILLS_PATH = '.vibe/skills';
59
60
  exports.SKILLZ_DIR = '.skillz';
60
61
  exports.SKILL_MD_FILENAME = 'SKILL.md';
61
62
  exports.SKILLZ_MCP_SERVER_NAME = 'skillz';
@@ -40,6 +40,7 @@ exports.propagateSkillsForClaude = propagateSkillsForClaude;
40
40
  exports.propagateSkillsForCodex = propagateSkillsForCodex;
41
41
  exports.propagateSkillsForOpenCode = propagateSkillsForOpenCode;
42
42
  exports.propagateSkillsForGoose = propagateSkillsForGoose;
43
+ exports.propagateSkillsForVibe = propagateSkillsForVibe;
43
44
  exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
44
45
  exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
45
46
  const path = __importStar(require("path"));
@@ -77,12 +78,13 @@ async function getSkillsGitignorePaths(projectRoot) {
77
78
  return [];
78
79
  }
79
80
  // Import here to avoid circular dependency
80
- const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, GOOSE_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
81
+ const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
81
82
  return [
82
83
  path.join(projectRoot, CLAUDE_SKILLS_PATH),
83
84
  path.join(projectRoot, CODEX_SKILLS_PATH),
84
85
  path.join(projectRoot, OPENCODE_SKILLS_PATH),
85
86
  path.join(projectRoot, GOOSE_SKILLS_PATH),
87
+ path.join(projectRoot, VIBE_SKILLS_PATH),
86
88
  path.join(projectRoot, SKILLZ_DIR),
87
89
  ];
88
90
  }
@@ -114,6 +116,7 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
114
116
  const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
115
117
  const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
116
118
  const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
119
+ const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
117
120
  const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
118
121
  // Clean up .claude/skills
119
122
  try {
@@ -171,6 +174,20 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
171
174
  catch {
172
175
  // Directory doesn't exist, nothing to clean
173
176
  }
177
+ // Clean up .vibe/skills
178
+ try {
179
+ await fs.access(vibeSkillsPath);
180
+ if (dryRun) {
181
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.VIBE_SKILLS_PATH}`, verbose, dryRun);
182
+ }
183
+ else {
184
+ await fs.rm(vibeSkillsPath, { recursive: true, force: true });
185
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.VIBE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
186
+ }
187
+ }
188
+ catch {
189
+ // Directory doesn't exist, nothing to clean
190
+ }
174
191
  // Clean up .skillz
175
192
  try {
176
193
  await fs.access(skillzPath);
@@ -237,6 +254,8 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
237
254
  await propagateSkillsForOpenCode(projectRoot, { dryRun });
238
255
  (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose`, verbose, dryRun);
239
256
  await propagateSkillsForGoose(projectRoot, { dryRun });
257
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.VIBE_SKILLS_PATH} for Mistral Vibe`, verbose, dryRun);
258
+ await propagateSkillsForVibe(projectRoot, { dryRun });
240
259
  }
241
260
  // Copy to .skillz directory if needed
242
261
  if (hasMcpAgent) {
@@ -444,6 +463,56 @@ async function propagateSkillsForGoose(projectRoot, options) {
444
463
  }
445
464
  return [];
446
465
  }
466
+ /**
467
+ * Propagates skills for Mistral Vibe by copying .ruler/skills to .vibe/skills.
468
+ * Uses atomic replace to ensure safe overwriting of existing skills.
469
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
470
+ */
471
+ async function propagateSkillsForVibe(projectRoot, options) {
472
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
473
+ const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
474
+ const vibeDir = path.dirname(vibeSkillsPath);
475
+ // Check if source skills directory exists
476
+ try {
477
+ await fs.access(skillsDir);
478
+ }
479
+ catch {
480
+ // No skills directory - return empty
481
+ return [];
482
+ }
483
+ if (options.dryRun) {
484
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.VIBE_SKILLS_PATH}`];
485
+ }
486
+ // Ensure .vibe directory exists
487
+ await fs.mkdir(vibeDir, { recursive: true });
488
+ // Use atomic replace: copy to temp, then rename
489
+ const tempDir = path.join(vibeDir, `skills.tmp-${Date.now()}`);
490
+ try {
491
+ // Copy to temp directory
492
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
493
+ // Atomically replace the target
494
+ // First, remove existing target if it exists
495
+ try {
496
+ await fs.rm(vibeSkillsPath, { recursive: true, force: true });
497
+ }
498
+ catch {
499
+ // Target didn't exist, that's fine
500
+ }
501
+ // Rename temp to target
502
+ await fs.rename(tempDir, vibeSkillsPath);
503
+ }
504
+ catch (error) {
505
+ // Clean up temp directory on error
506
+ try {
507
+ await fs.rm(tempDir, { recursive: true, force: true });
508
+ }
509
+ catch {
510
+ // Ignore cleanup errors
511
+ }
512
+ throw error;
513
+ }
514
+ return [];
515
+ }
447
516
  /**
448
517
  * Propagates skills for MCP agents by copying .ruler/skills to .skillz.
449
518
  * Uses atomic replace to ensure safe overwriting of existing skills.
@@ -577,7 +577,31 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
577
577
  out[serverKey] = cleanedServers;
578
578
  return out;
579
579
  };
580
- const toWrite = sanitizeForFirebase(merged);
580
+ // Gemini CLI (since v0.21.0) no longer accepts the "type" field in MCP server entries.
581
+ // Following the MCP spec update from Nov 25, 2025, the transport type is now inferred
582
+ // from the presence of specific keys (command/args -> stdio, url -> sse/http).
583
+ // Sanitize merged config by stripping 'type' from each server when targeting Gemini.
584
+ const sanitizeForGemini = (obj) => {
585
+ if (agent.getIdentifier() !== 'gemini-cli')
586
+ return obj;
587
+ const out = { ...obj };
588
+ const servers = out[serverKey] || {};
589
+ const cleanedServers = {};
590
+ for (const [name, def] of Object.entries(servers)) {
591
+ if (def && typeof def === 'object') {
592
+ const copy = { ...def };
593
+ delete copy.type;
594
+ cleanedServers[name] = copy;
595
+ }
596
+ else {
597
+ cleanedServers[name] = def;
598
+ }
599
+ }
600
+ out[serverKey] = cleanedServers;
601
+ return out;
602
+ };
603
+ let toWrite = sanitizeForFirebase(merged);
604
+ toWrite = sanitizeForGemini(toWrite);
581
605
  // Only backup and write if content would actually change (idempotent)
582
606
  const currentContent = JSON.stringify(existing, null, 2);
583
607
  const newContent = JSON.stringify(toWrite, null, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {