@intellectronica/ruler 0.3.9 → 0.3.10

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
@@ -82,6 +82,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
82
82
  | Trae AI | `.trae/rules/project_rules.md` | - |
83
83
  | Warp | `WARP.md` | - |
84
84
  | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | - |
85
+ | Firebender | `firebender.json` | - |
85
86
 
86
87
  ## Getting Started
87
88
 
@@ -214,7 +215,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
214
215
  | Option | Description |
215
216
  | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
216
217
  | `--project-root <path>` | Path to your project's root (default: current directory) |
217
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (agentsmd, aider, amazonqcli, amp, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
218
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (agentsmd, aider, amazonqcli, amp, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
218
219
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
219
220
  | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
220
221
  | `--no-mcp` | Disable applying MCP server configurations |
@@ -307,7 +308,7 @@ ruler revert [options]
307
308
  | Option | Description |
308
309
  | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
309
310
  | `--project-root <path>` | Path to your project's root (default: current directory) |
310
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
311
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
311
312
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
312
313
  | `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
313
314
  | `--dry-run` | Preview changes without actually reverting files |
@@ -0,0 +1,205 @@
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.FirebenderAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
+ /**
41
+ * Firebender agent adapter.
42
+ */
43
+ class FirebenderAgent {
44
+ /**
45
+ * Type guard function to safely check if an object is a FirebenderRule.
46
+ */
47
+ isFirebenderRule(rule) {
48
+ return (typeof rule === 'object' &&
49
+ rule !== null &&
50
+ 'filePathMatches' in rule &&
51
+ 'rulesPaths' in rule &&
52
+ typeof rule.filePathMatches === 'string' &&
53
+ typeof rule.rulesPaths === 'string');
54
+ }
55
+ getIdentifier() {
56
+ return 'firebender';
57
+ }
58
+ getName() {
59
+ return 'Firebender';
60
+ }
61
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
62
+ const rulesPath = this.resolveOutputPath(projectRoot, agentConfig);
63
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(rulesPath));
64
+ const firebenderConfig = await this.loadExistingConfig(rulesPath);
65
+ const newRules = this.createRulesFromConcatenatedRules(concatenatedRules, projectRoot);
66
+ firebenderConfig.rules.push(...newRules);
67
+ this.removeDuplicateRules(firebenderConfig);
68
+ const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
69
+ if (mcpEnabled && rulerMcpJson) {
70
+ await this.handleMcpConfiguration(firebenderConfig, rulerMcpJson, agentConfig);
71
+ }
72
+ await this.saveConfig(rulesPath, firebenderConfig, backup);
73
+ }
74
+ resolveOutputPath(projectRoot, agentConfig) {
75
+ const outputPaths = this.getDefaultOutputPath(projectRoot);
76
+ const output = agentConfig?.outputPath ??
77
+ agentConfig?.outputPathInstructions ??
78
+ outputPaths['instructions'];
79
+ return path.resolve(projectRoot, output);
80
+ }
81
+ async loadExistingConfig(rulesPath) {
82
+ try {
83
+ const existingContent = await fs.promises.readFile(rulesPath, 'utf8');
84
+ const config = JSON.parse(existingContent);
85
+ if (!config.rules) {
86
+ config.rules = [];
87
+ }
88
+ return config;
89
+ }
90
+ catch (error) {
91
+ if (error &&
92
+ typeof error === 'object' &&
93
+ 'code' in error &&
94
+ error.code === 'ENOENT') {
95
+ return { rules: [] };
96
+ }
97
+ console.warn(`Failed to read/parse existing firebender.json: ${error}`);
98
+ return { rules: [] };
99
+ }
100
+ }
101
+ createRulesFromConcatenatedRules(concatenatedRules, projectRoot) {
102
+ const filePaths = this.extractFilePathsFromRules(concatenatedRules, projectRoot);
103
+ if (filePaths.length > 0) {
104
+ return this.createRuleObjectsFromFilePaths(filePaths);
105
+ }
106
+ else {
107
+ return this.createRulesFromPlainText(concatenatedRules);
108
+ }
109
+ }
110
+ createRuleObjectsFromFilePaths(filePaths) {
111
+ return filePaths.map((filePath) => ({
112
+ filePathMatches: '**/*',
113
+ rulesPaths: filePath,
114
+ }));
115
+ }
116
+ createRulesFromPlainText(concatenatedRules) {
117
+ return concatenatedRules.split('\n').filter((rule) => rule.trim());
118
+ }
119
+ removeDuplicateRules(firebenderConfig) {
120
+ const seen = new Set();
121
+ firebenderConfig.rules = firebenderConfig.rules.filter((rule) => {
122
+ let key;
123
+ if (this.isFirebenderRule(rule)) {
124
+ const filePathMatchesPart = rule.filePathMatches;
125
+ const rulesPathsPart = rule.rulesPaths;
126
+ key = `${filePathMatchesPart}::${rulesPathsPart}`;
127
+ }
128
+ else {
129
+ key = String(rule);
130
+ }
131
+ if (seen.has(key)) {
132
+ return false;
133
+ }
134
+ seen.add(key);
135
+ return true;
136
+ });
137
+ }
138
+ async saveConfig(rulesPath, config, backup) {
139
+ const updatedContent = JSON.stringify(config, null, 2);
140
+ if (backup) {
141
+ await (0, FileSystemUtils_1.backupFile)(rulesPath);
142
+ }
143
+ await (0, FileSystemUtils_1.writeGeneratedFile)(rulesPath, updatedContent);
144
+ }
145
+ /**
146
+ * Handle MCP server configuration for Firebender.
147
+ * Merges or overwrites MCP servers in the firebender.json configuration based on strategy.
148
+ */
149
+ async handleMcpConfiguration(firebenderConfig, rulerMcpJson, agentConfig) {
150
+ const strategy = agentConfig?.mcp?.strategy ?? 'merge';
151
+ const incomingServers = rulerMcpJson.mcpServers || {};
152
+ if (!firebenderConfig.mcpServers) {
153
+ firebenderConfig.mcpServers = {};
154
+ }
155
+ if (strategy === 'overwrite') {
156
+ firebenderConfig.mcpServers = { ...incomingServers };
157
+ }
158
+ else if (strategy === 'merge') {
159
+ const existingServers = firebenderConfig.mcpServers || {};
160
+ firebenderConfig.mcpServers = { ...existingServers, ...incomingServers };
161
+ }
162
+ }
163
+ getDefaultOutputPath(projectRoot) {
164
+ return {
165
+ instructions: path.join(projectRoot, 'firebender.json'),
166
+ mcp: path.join(projectRoot, 'firebender.json'),
167
+ };
168
+ }
169
+ getMcpServerKey() {
170
+ return 'mcpServers';
171
+ }
172
+ supportsMcpStdio() {
173
+ return true;
174
+ }
175
+ supportsMcpRemote() {
176
+ return true;
177
+ }
178
+ /**
179
+ * Extracts file paths from concatenated rules by parsing HTML source comments.
180
+ * @param concatenatedRules The concatenated rules string with HTML comments
181
+ * @param projectRoot The project root directory
182
+ * @returns Array of file paths relative to project root
183
+ */
184
+ extractFilePathsFromRules(concatenatedRules, projectRoot) {
185
+ const sourceCommentRegex = /<!-- Source: (.+?) -->/g;
186
+ const filePaths = [];
187
+ let match;
188
+ while ((match = sourceCommentRegex.exec(concatenatedRules)) !== null) {
189
+ const relativePath = match[1];
190
+ const absolutePath = path.resolve(projectRoot, relativePath);
191
+ const normalizedProjectRoot = path.resolve(projectRoot);
192
+ // Ensure the absolutePath is within the project root (cross-platform compatible)
193
+ // This prevents path traversal attacks while handling Windows/Unix path differences
194
+ const isWithinProject = absolutePath.startsWith(normalizedProjectRoot) &&
195
+ (absolutePath.length === normalizedProjectRoot.length ||
196
+ absolutePath[normalizedProjectRoot.length] === path.sep);
197
+ if (isWithinProject) {
198
+ const projectRelativePath = path.relative(projectRoot, absolutePath);
199
+ filePaths.push(projectRelativePath);
200
+ }
201
+ }
202
+ return filePaths;
203
+ }
204
+ }
205
+ exports.FirebenderAgent = FirebenderAgent;
@@ -30,6 +30,7 @@ const WarpAgent_1 = require("./WarpAgent");
30
30
  const RooCodeAgent_1 = require("./RooCodeAgent");
31
31
  const TraeAgent_1 = require("./TraeAgent");
32
32
  const AmazonQCliAgent_1 = require("./AmazonQCliAgent");
33
+ const FirebenderAgent_1 = require("./FirebenderAgent");
33
34
  exports.allAgents = [
34
35
  new CopilotAgent_1.CopilotAgent(),
35
36
  new ClaudeAgent_1.ClaudeAgent(),
@@ -57,6 +58,7 @@ exports.allAgents = [
57
58
  new RooCodeAgent_1.RooCodeAgent(),
58
59
  new TraeAgent_1.TraeAgent(),
59
60
  new AmazonQCliAgent_1.AmazonQCliAgent(),
61
+ new FirebenderAgent_1.FirebenderAgent(),
60
62
  ];
61
63
  /**
62
64
  * Generates a comma-separated list of agent identifiers for CLI help text.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {