@intellectronica/ruler 0.2.5 → 0.2.7

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
@@ -40,6 +40,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
40
40
  | GitHub Copilot | `.github/copilot-instructions.md` |
41
41
  | Claude Code | `CLAUDE.md` |
42
42
  | OpenAI Codex CLI | `AGENTS.md` |
43
+ | Jules | `AGENTS.md` |
43
44
  | Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` |
44
45
  | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
45
46
  | Cline | `.clinerules` |
@@ -78,13 +79,20 @@ npx @intellectronica/ruler apply
78
79
  - `.ruler/ruler.toml`: The main configuration file for Ruler
79
80
  - `.ruler/mcp.json`: An example MCP server configuration
80
81
 
82
+ Additionally, you can create a global configuration to use when no local `.ruler/` directory is found:
83
+ ```bash
84
+ ruler init --global
85
+ ```
86
+
87
+ The global configuration will be created to `$XDG_CONFIG_HOME/ruler` (default: `~/.config/ruler`).
88
+
81
89
  ## Core Concepts
82
90
 
83
91
  ### The `.ruler/` Directory
84
92
 
85
93
  This is your central hub for all AI agent instructions:
86
94
 
87
- - **Rule Files (`*.md`)**: Discovered recursively from `.ruler/` and alphabetically concatenated
95
+ - **Rule Files (`*.md`)**: Discovered recursively from `.ruler/` or `$XDG_CONFIG_HOME/ruler` and alphabetically concatenated
88
96
  - **Concatenation Marker**: Each file's content is prepended with `--- Source: <relative_path_to_md_file> ---` for traceability
89
97
  - **`ruler.toml`**: Master configuration for Ruler's behavior, agent selection, and output paths
90
98
  - **`mcp.json`**: Shared MCP server settings
@@ -128,6 +136,8 @@ This is your central hub for all AI agent instructions:
128
136
  ruler apply [options]
129
137
  ```
130
138
 
139
+ The `apply` command looks for `.ruler/` in the current directory tree, reading the first match. If no such directory is found, it will look for a global configuration in `$XDG_CONFIG_HOME/ruler`.
140
+
131
141
  ### Options
132
142
 
133
143
  | Option | Description |
@@ -140,6 +150,7 @@ ruler apply [options]
140
150
  | `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
141
151
  | `--gitignore` | Enable automatic .gitignore updates (default: true) |
142
152
  | `--no-gitignore` | Disable automatic .gitignore updates |
153
+ | `--local-only` | Do not look for configuration in `$XDG_CONFIG_HOME` |
143
154
  | `--verbose` / `-v` | Display detailed output during execution |
144
155
 
145
156
  ### Common Examples
@@ -226,6 +237,9 @@ output_path = ".idx/airules.md"
226
237
  [agents.gemini-cli]
227
238
  enabled = true
228
239
 
240
+ [agents.jules]
241
+ enabled = true
242
+
229
243
  # Agent-specific MCP configuration
230
244
  [agents.cursor.mcp]
231
245
  enabled = true
@@ -0,0 +1,55 @@
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.JulesAgent = void 0;
37
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
38
+ const path = __importStar(require("path"));
39
+ class JulesAgent {
40
+ getIdentifier() {
41
+ return 'jules';
42
+ }
43
+ getName() {
44
+ return 'Jules';
45
+ }
46
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
47
+ const outputPath = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ const absolutePath = path.resolve(projectRoot, outputPath);
49
+ await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, concatenatedRules);
50
+ }
51
+ getDefaultOutputPath(projectRoot) {
52
+ return path.join(projectRoot, 'AGENTS.md');
53
+ }
54
+ }
55
+ exports.JulesAgent = JulesAgent;
@@ -41,6 +41,7 @@ const yargs_1 = __importDefault(require("yargs"));
41
41
  const helpers_1 = require("yargs/helpers");
42
42
  const lib_1 = require("../lib");
43
43
  const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
44
45
  const fs_1 = require("fs");
45
46
  const constants_1 = require("../constants");
46
47
  /**
@@ -90,6 +91,11 @@ function run() {
90
91
  description: 'Preview changes without writing files',
91
92
  default: false,
92
93
  });
94
+ y.option('local-only', {
95
+ type: 'boolean',
96
+ description: 'Only search for local .ruler directories, ignore global config',
97
+ default: false,
98
+ });
93
99
  }, async (argv) => {
94
100
  const projectRoot = argv['project-root'];
95
101
  const agents = argv.agents
@@ -102,6 +108,7 @@ function run() {
102
108
  : undefined;
103
109
  const verbose = argv.verbose;
104
110
  const dryRun = argv['dry-run'];
111
+ const localOnly = argv['local-only'];
105
112
  // Determine gitignore preference: CLI > TOML > Default (enabled)
106
113
  // yargs handles --no-gitignore by setting gitignore to false
107
114
  let gitignorePreference;
@@ -112,7 +119,7 @@ function run() {
112
119
  gitignorePreference = undefined; // Let TOML/default decide
113
120
  }
114
121
  try {
115
- await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun);
122
+ await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly);
116
123
  console.log('Ruler apply completed successfully.');
117
124
  }
118
125
  catch (err) {
@@ -126,10 +133,17 @@ function run() {
126
133
  type: 'string',
127
134
  description: 'Project root directory',
128
135
  default: process.cwd(),
136
+ }).option('global', {
137
+ type: 'boolean',
138
+ description: 'Initialize in global config directory (XDG_CONFIG_HOME/ruler)',
139
+ default: false,
129
140
  });
130
141
  }, async (argv) => {
131
142
  const projectRoot = argv['project-root'];
132
- const rulerDir = path.join(projectRoot, '.ruler');
143
+ const isGlobal = argv['global'];
144
+ const rulerDir = isGlobal
145
+ ? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'ruler')
146
+ : path.join(projectRoot, '.ruler');
133
147
  await fs_1.promises.mkdir(rulerDir, { recursive: true });
134
148
  const instructionsPath = path.join(rulerDir, 'instructions.md');
135
149
  const tomlPath = path.join(rulerDir, 'ruler.toml');
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.loadConfig = loadConfig;
40
40
  const fs_1 = require("fs");
41
41
  const path = __importStar(require("path"));
42
+ const os = __importStar(require("os"));
42
43
  const toml_1 = __importDefault(require("@iarna/toml"));
43
44
  const zod_1 = require("zod");
44
45
  const constants_1 = require("../constants");
@@ -78,9 +79,23 @@ const rulerConfigSchema = zod_1.z.object({
78
79
  */
79
80
  async function loadConfig(options) {
80
81
  const { projectRoot, configPath, cliAgents } = options;
81
- const configFile = configPath
82
- ? path.resolve(configPath)
83
- : path.join(projectRoot, '.ruler', 'ruler.toml');
82
+ let configFile;
83
+ if (configPath) {
84
+ configFile = path.resolve(configPath);
85
+ }
86
+ else {
87
+ // Try local .ruler/ruler.toml first
88
+ const localConfigFile = path.join(projectRoot, '.ruler', 'ruler.toml');
89
+ try {
90
+ await fs_1.promises.access(localConfigFile);
91
+ configFile = localConfigFile;
92
+ }
93
+ catch {
94
+ // If local config doesn't exist, try global config
95
+ const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
96
+ configFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
97
+ }
98
+ }
84
99
  let raw = {};
85
100
  try {
86
101
  const text = await fs_1.promises.readFile(configFile, 'utf8');
@@ -40,11 +40,20 @@ exports.backupFile = backupFile;
40
40
  exports.ensureDirExists = ensureDirExists;
41
41
  const fs_1 = require("fs");
42
42
  const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ /**
45
+ * Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
46
+ */
47
+ function getXdgConfigDir() {
48
+ return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
49
+ }
43
50
  /**
44
51
  * Searches upwards from startPath to find a directory named .ruler.
52
+ * If not found locally and checkGlobal is true, checks for global config at XDG_CONFIG_HOME/ruler.
45
53
  * Returns the path to the .ruler directory, or null if not found.
46
54
  */
47
- async function findRulerDir(startPath) {
55
+ async function findRulerDir(startPath, checkGlobal = true) {
56
+ // First, search upwards from startPath for local .ruler directory
48
57
  let current = startPath;
49
58
  while (current) {
50
59
  const candidate = path.join(current, '.ruler');
@@ -63,6 +72,19 @@ async function findRulerDir(startPath) {
63
72
  }
64
73
  current = parent;
65
74
  }
75
+ // If no local .ruler found and checkGlobal is true, check global config directory
76
+ if (checkGlobal) {
77
+ const globalConfigDir = path.join(getXdgConfigDir(), 'ruler');
78
+ try {
79
+ const stat = await fs_1.promises.stat(globalConfigDir);
80
+ if (stat.isDirectory()) {
81
+ return globalConfigDir;
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.error(`[ruler] Error checking global config directory ${globalConfigDir}:`, err);
86
+ }
87
+ }
66
88
  return null;
67
89
  }
68
90
  /**
package/dist/lib.js CHANGED
@@ -50,6 +50,7 @@ const AiderAgent_1 = require("./agents/AiderAgent");
50
50
  const FirebaseAgent_1 = require("./agents/FirebaseAgent");
51
51
  const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
52
52
  const GeminiCliAgent_1 = require("./agents/GeminiCliAgent");
53
+ const JulesAgent_1 = require("./agents/JulesAgent");
53
54
  const merge_1 = require("./mcp/merge");
54
55
  const validate_1 = require("./mcp/validate");
55
56
  const mcp_1 = require("./paths/mcp");
@@ -100,6 +101,7 @@ const agents = [
100
101
  new FirebaseAgent_1.FirebaseAgent(),
101
102
  new OpenHandsAgent_1.OpenHandsAgent(),
102
103
  new GeminiCliAgent_1.GeminiCliAgent(),
104
+ new JulesAgent_1.JulesAgent(),
103
105
  ];
104
106
  /**
105
107
  * Applies ruler configurations for all supported AI agents.
@@ -110,7 +112,7 @@ const agents = [
110
112
  * @param projectRoot Root directory of the project
111
113
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
112
114
  */
113
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false) {
115
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false) {
114
116
  // Load configuration (default_agents, per-agent overrides, CLI filters)
115
117
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
116
118
  if (configPath) {
@@ -137,13 +139,13 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
137
139
  }
138
140
  }
139
141
  config.agentConfigs = mappedConfigs;
140
- const rulerDir = await FileSystemUtils.findRulerDir(projectRoot);
142
+ const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
141
143
  if (!rulerDir) {
142
144
  throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
143
145
  }
144
146
  (0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
145
147
  const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
146
- (0, constants_1.logVerbose)(`Found ${files.length} markdown files in .ruler directory`, verbose);
148
+ (0, constants_1.logVerbose)(`Found ${files.length} markdown files in ruler configuration directory`, verbose);
147
149
  const concatenated = (0, RuleProcessor_1.concatenateRules)(files);
148
150
  (0, constants_1.logVerbose)(`Concatenated rules length: ${concatenated.length} characters`, verbose);
149
151
  const mcpFile = path.join(rulerDir, 'mcp.json');
@@ -187,6 +189,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
187
189
  }
188
190
  // Collect all generated file paths for .gitignore
189
191
  const generatedPaths = [];
192
+ let agentsMdWritten = false;
190
193
  for (const agent of selected) {
191
194
  const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
192
195
  console.log(`${actionPrefix} Applying rules for ${agent.getName()}...`);
@@ -196,10 +199,20 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
196
199
  const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
197
200
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
198
201
  generatedPaths.push(...outputPaths);
202
+ // Also add the backup file paths to the gitignore list
203
+ const backupPaths = outputPaths.map((p) => `${p}.bak`);
204
+ generatedPaths.push(...backupPaths);
199
205
  if (dryRun) {
200
206
  (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose);
201
207
  }
202
208
  else {
209
+ if (agent.getIdentifier() === 'jules' ||
210
+ agent.getIdentifier() === 'codex') {
211
+ if (agentsMdWritten) {
212
+ continue;
213
+ }
214
+ agentsMdWritten = true;
215
+ }
203
216
  await agent.applyRulerConfig(concatenated, projectRoot, rulerMcpJson, agentConfig);
204
217
  }
205
218
  const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
@@ -207,6 +220,13 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
207
220
  (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
208
221
  const rulerMcpFile = path.join(rulerDir, 'mcp.json');
209
222
  if (dest && mcpEnabledForAgent) {
223
+ // Include MCP config file in .gitignore only if it's within the project directory
224
+ if (dest.startsWith(projectRoot)) {
225
+ const relativeDest = path.relative(projectRoot, dest);
226
+ generatedPaths.push(relativeDest);
227
+ // Also add the backup for the MCP file
228
+ generatedPaths.push(`${relativeDest}.bak`);
229
+ }
210
230
  if (agent.getIdentifier() === 'openhands') {
211
231
  // *** Special handling for Open Hands ***
212
232
  if (dryRun) {
@@ -215,8 +235,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
215
235
  else {
216
236
  await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(rulerMcpFile, dest);
217
237
  }
218
- // Include Open Hands config file in .gitignore
219
- generatedPaths.push(dest);
238
+ // Open Hands config file is already included above
220
239
  }
221
240
  else {
222
241
  if (rulerMcpJson) {
@@ -252,9 +271,9 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
252
271
  gitignoreEnabled = true; // Default enabled
253
272
  }
254
273
  if (gitignoreEnabled && generatedPaths.length > 0) {
255
- // Filter out .bak files as specified in requirements
256
- const pathsToIgnore = generatedPaths.filter((p) => !p.endsWith('.bak'));
257
- const uniquePaths = [...new Set(pathsToIgnore)];
274
+ const uniquePaths = [...new Set(generatedPaths)];
275
+ // Add wildcard pattern for backup files
276
+ uniquePaths.push('*.bak');
258
277
  if (uniquePaths.length > 0) {
259
278
  const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
260
279
  if (dryRun) {
package/dist/paths/mcp.js CHANGED
@@ -71,6 +71,9 @@ async function getNativeMcpPath(adapterName, projectRoot) {
71
71
  // For Open Hands, we target the main config file, not a separate mcp.json
72
72
  candidates.push(path.join(projectRoot, '.openhands', 'config.toml'));
73
73
  break;
74
+ case 'Gemini CLI':
75
+ candidates.push(path.join(projectRoot, '.gemini', 'settings.json'));
76
+ break;
74
77
  default:
75
78
  return null;
76
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {