@intellectronica/ruler 0.3.43 → 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 CHANGED
@@ -57,7 +57,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
57
57
  | Agent | Rules File(s) | MCP Configuration / Notes | Skills Support / Location | Subagents Support / Location |
58
58
  | ---------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------- | ---------------------------- |
59
59
  | AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - | - |
60
- | GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` | `.claude/skills/` | `.github/agents/` |
60
+ | GitHub Copilot | `AGENTS.md` | `.mcp.json` | `.claude/skills/` | `.github/agents/` |
61
61
  | Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` | `.claude/agents/` |
62
62
  | OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.agents/skills/` | `.codex/agents/` (`.toml`) |
63
63
  | Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` | - |
@@ -953,7 +953,7 @@ ruler apply
953
953
 
954
954
  ### Scenario 2: Working with worktrees
955
955
 
956
- When using the default `git add worktree` command (which is also run by agents apps such as Claude code or Codex through the interface), the gitignored files are not copied over. You will need to ask your agent to run `ruler apply` at the start of every session.
956
+ When using the default `git worktree add` command (which is also run by agent apps such as Claude Code or Codex through the interface), the gitignored files are not copied over. You will need to ask your agent to run `ruler apply` at the start of every session.
957
957
 
958
958
  As an alternative you can commit your default agents files to source control.
959
959
 
@@ -979,7 +979,7 @@ enabled = false
979
979
  To avoid having other contributors commit instructions outside of .ruler you can setup a github action to check there is no diff when running `ruler apply` in CI.
980
980
 
981
981
  ```yml
982
- # .github/workflows/ruler-check/yml
982
+ # .github/workflows/ruler-check.yml
983
983
 
984
984
  # Verifies the committed agent files (AGENTS.md, CLAUDE.md, skills) match the .ruler/ source.
985
985
  # They are committed so a fresh clone/worktree has guidance immediately; this guards against drift.
@@ -1014,7 +1014,7 @@ jobs:
1014
1014
 
1015
1015
  - name: Verify committed agent files match .ruler/
1016
1016
  run: |
1017
- pnpm dlx @intellectronica/ruler@0.3.42 apply --no-gitignore --no-mcp
1017
+ pnpm dlx @intellectronica/ruler apply --no-gitignore --no-mcp
1018
1018
  DRIFT="$(git status --porcelain -- AGENTS.md CLAUDE.md .claude/skills .codex/skills)"
1019
1019
  if [ -n "$DRIFT" ]; then
1020
1020
  echo "::error::Committed agent files are out of sync with .ruler/. Run 'pnpm dlx @intellectronica/ruler apply --no-gitignore --no-mcp' and commit the result."
@@ -31,7 +31,7 @@ class CopilotAgent {
31
31
  }, backup);
32
32
  }
33
33
  getMcpServerKey() {
34
- return 'servers';
34
+ return 'mcpServers';
35
35
  }
36
36
  supportsMcpStdio() {
37
37
  return true;
@@ -172,7 +172,10 @@ function parseAgentMcpServers(sectionObj) {
172
172
  const servers = {};
173
173
  for (const [name, def] of Object.entries(sectionObj.mcp_servers)) {
174
174
  if (def && typeof def === 'object' && !Array.isArray(def)) {
175
- servers[name] = normalizeAgentMcpServer(def);
175
+ const server = normalizeAgentMcpServer(def);
176
+ if (server.command || server.url) {
177
+ servers[name] = server;
178
+ }
176
179
  }
177
180
  }
178
181
  return Object.keys(servers).length > 0 ? servers : undefined;
@@ -181,6 +184,17 @@ function normalizeAgentMcpServer(def) {
181
184
  const server = { ...def };
182
185
  const hasCommand = typeof server.command === 'string';
183
186
  const hasUrl = typeof server.url === 'string';
187
+ if (!hasCommand) {
188
+ delete server.command;
189
+ delete server.args;
190
+ delete server.env;
191
+ }
192
+ if (!hasUrl) {
193
+ delete server.url;
194
+ delete server.headers;
195
+ delete server.auth;
196
+ delete server.oauth;
197
+ }
184
198
  if (hasCommand && hasUrl) {
185
199
  delete server.command;
186
200
  delete server.args;
@@ -1,3 +1,4 @@
1
+ export declare function assertNotSymbolicLink(filePath: string, action: string): Promise<void>;
1
2
  export declare function assertManagedPathInsideRoot(managedPath: string, rootPath: string, action: string): Promise<void>;
2
3
  /**
3
4
  * Searches upwards from startPath to find a directory named .ruler.
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.assertNotSymbolicLink = assertNotSymbolicLink;
36
37
  exports.assertManagedPathInsideRoot = assertManagedPathInsideRoot;
37
38
  exports.findRulerDir = findRulerDir;
38
39
  exports.resolveProjectRootForRulerDir = resolveProjectRootForRulerDir;
@@ -137,7 +137,10 @@ function formatValidationWarnings(warnings) {
137
137
  * Recursively copies a directory and all its contents.
138
138
  */
139
139
  async function copyRecursive(src, dest) {
140
- const stat = await fs.stat(src);
140
+ const stat = await fs.lstat(src);
141
+ if (stat.isSymbolicLink()) {
142
+ return;
143
+ }
141
144
  if (stat.isDirectory()) {
142
145
  await fs.mkdir(dest, { recursive: true });
143
146
  const entries = await fs.readdir(src, { withFileTypes: true });
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadUnifiedConfig = loadUnifiedConfig;
37
37
  const fs_1 = require("fs");
38
38
  const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
39
40
  const toml_1 = require("@iarna/toml");
40
41
  const hash_1 = require("./hash");
41
42
  const RuleProcessor_1 = require("./RuleProcessor");
@@ -52,6 +53,39 @@ const KNOWN_MCP_SERVER_FIELDS = new Set([
52
53
  function copyAdditionalMcpServerFields(def) {
53
54
  return Object.fromEntries(Object.entries(def).filter(([key]) => !KNOWN_MCP_SERVER_FIELDS.has(key)));
54
55
  }
56
+ async function resolveImplicitTomlFile(projectRoot, rulerDir, checkGlobal) {
57
+ const localTomlFile = path.join(rulerDir, 'ruler.toml');
58
+ if (await fileExists(localTomlFile)) {
59
+ return localTomlFile;
60
+ }
61
+ const projectTomlFile = path.join(projectRoot, '.ruler', 'ruler.toml');
62
+ if (path.resolve(projectTomlFile) !== path.resolve(localTomlFile) &&
63
+ (await fileExists(projectTomlFile))) {
64
+ return projectTomlFile;
65
+ }
66
+ if (!checkGlobal) {
67
+ return undefined;
68
+ }
69
+ const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
70
+ const globalTomlFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
71
+ if (path.resolve(globalTomlFile) !== path.resolve(localTomlFile) &&
72
+ (await fileExists(globalTomlFile))) {
73
+ return globalTomlFile;
74
+ }
75
+ return undefined;
76
+ }
77
+ async function fileExists(filePath) {
78
+ try {
79
+ await fs_1.promises.access(filePath);
80
+ return true;
81
+ }
82
+ catch (err) {
83
+ if (err.code === 'ENOENT') {
84
+ return false;
85
+ }
86
+ throw err;
87
+ }
88
+ }
55
89
  async function loadUnifiedConfig(options) {
56
90
  // Resolve the effective .ruler directory (local or global), mirroring the main loader behavior
57
91
  const resolvedRulerDir = (await FileSystemUtils.findRulerDir(options.projectRoot, options.checkGlobal ?? true)) || path.join(options.projectRoot, '.ruler');
@@ -66,15 +100,14 @@ async function loadUnifiedConfig(options) {
66
100
  let tomlRaw = {};
67
101
  const tomlFile = options.configPath
68
102
  ? path.resolve(options.configPath)
69
- : path.join(meta.rulerDir, 'ruler.toml');
70
- try {
71
- const text = await fs_1.promises.readFile(tomlFile, 'utf8');
72
- tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
73
- meta.configFile = tomlFile;
74
- }
75
- catch (err) {
76
- if (options.configPath ||
77
- err.code !== 'ENOENT') {
103
+ : await resolveImplicitTomlFile(options.projectRoot, meta.rulerDir, options.checkGlobal ?? true);
104
+ if (tomlFile) {
105
+ try {
106
+ const text = await fs_1.promises.readFile(tomlFile, 'utf8');
107
+ tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
108
+ meta.configFile = tomlFile;
109
+ }
110
+ catch (err) {
78
111
  diagnostics.push({
79
112
  severity: options.configPath ? 'error' : 'warning',
80
113
  code: 'TOML_READ_ERROR',
@@ -141,6 +141,8 @@ async function restoreFromBackup(filePath, verbose, dryRun, projectRoot) {
141
141
  await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to restore backup through symlinked output path');
142
142
  await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to restore from backup through symlinked backup path');
143
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');
144
146
  await fs_1.promises.copyFile(backupPath, filePath);
145
147
  (0, constants_1.logVerbose)(`${prefix} Restored: ${filePath} from backup`, verbose);
146
148
  }
@@ -173,6 +175,7 @@ async function removeGeneratedFile(filePath, verbose, dryRun, projectRoot) {
173
175
  if (projectRoot) {
174
176
  await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to remove generated file through symlinked path');
175
177
  }
178
+ await (0, FileSystemUtils_1.assertNotSymbolicLink)(filePath, 'Refusing to remove symlinked generated file');
176
179
  await fs_1.promises.unlink(filePath);
177
180
  (0, constants_1.logVerbose)(`${prefix} Removed generated file: ${filePath}`, verbose);
178
181
  }
@@ -195,6 +198,7 @@ async function removeBackupFile(filePath, verbose, dryRun, projectRoot) {
195
198
  if (projectRoot) {
196
199
  await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to remove backup file through symlinked path');
197
200
  }
201
+ await (0, FileSystemUtils_1.assertNotSymbolicLink)(backupPath, 'Refusing to remove symlinked backup file');
198
202
  await fs_1.promises.unlink(backupPath);
199
203
  (0, constants_1.logVerbose)(`${prefix} Removed backup file: ${backupPath}`, verbose);
200
204
  }
@@ -329,7 +333,6 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
329
333
  const additionalFiles = [
330
334
  '.gemini/settings.json',
331
335
  '.mcp.json',
332
- '.vscode/mcp.json',
333
336
  '.cursor/mcp.json',
334
337
  '.junie/mcp/mcp.json',
335
338
  '.kilocode/mcp.json',
@@ -359,6 +362,8 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
359
362
  (0, constants_1.logVerbose)(`${prefix} Would remove additional file: ${fullPath}`, verbose);
360
363
  }
361
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');
362
367
  await fs_1.promises.unlink(fullPath);
363
368
  (0, constants_1.logVerbose)(`${prefix} Removed additional file: ${fullPath}`, verbose);
364
369
  }
@@ -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 = 'command' in config;
39
- const hasUrl = 'url' in config;
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/paths/mcp.js CHANGED
@@ -48,7 +48,7 @@ async function getNativeMcpPath(adapterName, projectRoot) {
48
48
  const candidates = [];
49
49
  switch (adapterName) {
50
50
  case 'GitHub Copilot':
51
- candidates.push(path.join(projectRoot, '.vscode', 'mcp.json'));
51
+ candidates.push(path.join(projectRoot, '.mcp.json'));
52
52
  break;
53
53
  case 'Visual Studio':
54
54
  candidates.push(path.join(projectRoot, '.mcp.json'));
package/dist/revert.js CHANGED
@@ -174,6 +174,8 @@ async function cleanIgnoreFile(projectRoot, ignoreFile, verbose, dryRun) {
174
174
  (0, constants_1.logVerbose)(`No ${ignoreFile} file found`, verbose);
175
175
  return false;
176
176
  }
177
+ await FileSystemUtils.assertManagedPathInsideRoot(ignorePath, projectRoot, `Refusing to clean ${ignoreFile} through symlinked path`);
178
+ await FileSystemUtils.assertNotSymbolicLink(ignorePath, `Refusing to clean symlinked ${ignoreFile}`);
177
179
  const content = await fs_1.promises.readFile(ignorePath, 'utf8');
178
180
  const cleaned = (0, GitignoreUtils_1.removeCompleteRulerBlocks)(content);
179
181
  if (!cleaned.removed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.43",
3
+ "version": "0.3.44",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "types": "dist/lib.d.ts",
@@ -12,7 +12,8 @@
12
12
  "test:watch": "jest --watch",
13
13
  "test:coverage": "jest --coverage",
14
14
  "test:integration": "jest tests/e2e/ruler.integration.test.ts --verbose",
15
- "build": "tsc",
15
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
16
+ "build": "npm run clean && tsc",
16
17
  "check:package-lock": "node -e \"const pkg=require('./package.json'); const lock=require('./package-lock.json'); const root=lock.packages?.['']; if (lock.version !== pkg.version || root?.version !== pkg.version) { console.error('package-lock.json version metadata must match package.json version'); process.exit(1); }\"",
17
18
  "prepublishOnly": "npm run build"
18
19
  },