agentloom 0.1.4 → 0.1.6
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 +92 -6
- package/dist/cli.js +11 -4
- package/dist/commands/add.js +14 -0
- package/dist/commands/delete.js +89 -3
- package/dist/commands/entity-utils.js +3 -0
- package/dist/commands/find.js +146 -12
- package/dist/commands/rule.d.ts +2 -0
- package/dist/commands/rule.js +86 -0
- package/dist/commands/sync.js +13 -4
- package/dist/commands/update.js +90 -7
- package/dist/core/agents.js +1 -1
- package/dist/core/argv.js +2 -0
- package/dist/core/commands.d.ts +12 -0
- package/dist/core/commands.js +106 -6
- package/dist/core/copy.js +12 -5
- package/dist/core/importer.d.ts +10 -0
- package/dist/core/importer.js +629 -13
- package/dist/core/lockfile.js +8 -0
- package/dist/core/manifest.js +123 -6
- package/dist/core/migration.js +655 -66
- package/dist/core/provider-entity-validation.d.ts +8 -0
- package/dist/core/provider-entity-validation.js +34 -0
- package/dist/core/provider-paths.d.ts +8 -1
- package/dist/core/provider-paths.js +69 -5
- package/dist/core/router.js +7 -1
- package/dist/core/rules.d.ts +34 -0
- package/dist/core/rules.js +149 -0
- package/dist/core/scope.js +1 -0
- package/dist/core/skills.d.ts +1 -0
- package/dist/core/skills.js +21 -2
- package/dist/core/sources.d.ts +2 -0
- package/dist/core/sources.js +34 -5
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.js +16 -0
- package/dist/sync/index.js +376 -18
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ For monorepo-level documentation and architecture context, see the [root README]
|
|
|
10
10
|
npx agentloom init
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
That's all you need. Agentloom picks up your existing provider configs, migrates them into a unified `.agents/` directory, and syncs everything back out to all your tools. From here on, manage your agents, commands, skills, and MCP servers in one place and run `agentloom sync` whenever you make changes.
|
|
13
|
+
That's all you need. Agentloom picks up your existing provider configs, migrates them into a unified `.agents/` directory, and syncs everything back out to all your tools. From here on, manage your agents, commands, rules, skills, and MCP servers in one place and run `agentloom sync` whenever you make changes.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -32,6 +32,9 @@ Project scope:
|
|
|
32
32
|
commands/
|
|
33
33
|
review.md
|
|
34
34
|
ship.md
|
|
35
|
+
rules/
|
|
36
|
+
always-test.md
|
|
37
|
+
enforce-logs.md
|
|
35
38
|
skills/
|
|
36
39
|
reviewing/
|
|
37
40
|
SKILL.md
|
|
@@ -59,7 +62,7 @@ Global scope uses `~/.agents` with the same file layout.
|
|
|
59
62
|
- `agentloom sync`
|
|
60
63
|
- `agentloom delete <source|name>`
|
|
61
64
|
|
|
62
|
-
Aggregate `add` imports discoverable entities from a source (agents, commands, MCP servers, skills). In interactive sessions, each entity supports two tracking modes:
|
|
65
|
+
Aggregate `add` imports discoverable entities from a source (agents, commands, rules, MCP servers, skills). In interactive sessions, each entity supports two tracking modes:
|
|
63
66
|
|
|
64
67
|
- `Sync everything from source` (default): updates include newly added source items.
|
|
65
68
|
- `Use custom selection`: updates stay pinned to the selected items, even if all current items were selected.
|
|
@@ -67,7 +70,8 @@ Aggregate `add` imports discoverable entities from a source (agents, commands, M
|
|
|
67
70
|
Source path resolution is additive and priority-ordered:
|
|
68
71
|
|
|
69
72
|
- Agents: `.agents/agents` -> `agents`
|
|
70
|
-
- Commands: `.agents/commands` -> `commands` -> `prompts`
|
|
73
|
+
- Commands: `.agents/commands` -> `commands` -> `prompts` -> provider fallbacks `.github/prompts` + `.gemini/commands`
|
|
74
|
+
- Rules: `.agents/rules` -> `rules`
|
|
71
75
|
- Skills: `.agents/skills` -> `skills` -> root `SKILL.md` fallback
|
|
72
76
|
- MCP: `.agents/mcp.json` -> `mcp.json`
|
|
73
77
|
|
|
@@ -78,6 +82,7 @@ Aggregate `agentloom add <source>` can import command/skill/MCP-only repositorie
|
|
|
78
82
|
- `agentloom agent <add|list|delete|find|update|sync>`
|
|
79
83
|
- `agentloom command <add|list|delete|find|update|sync>`
|
|
80
84
|
- `agentloom mcp <add|list|delete|find|update|sync>`
|
|
85
|
+
- `agentloom rule <add|list|delete|find|update|sync>`
|
|
81
86
|
- `agentloom skill <add|list|delete|find|update|sync>`
|
|
82
87
|
|
|
83
88
|
### Selector flags
|
|
@@ -85,11 +90,13 @@ Aggregate `agentloom add <source>` can import command/skill/MCP-only repositorie
|
|
|
85
90
|
- `--agents <csv>`
|
|
86
91
|
- `--commands <csv>`
|
|
87
92
|
- `--mcps <csv>`
|
|
93
|
+
- `--rule <csv>` (alias)
|
|
94
|
+
- `--rules <csv>`
|
|
88
95
|
- `--skills <csv>`
|
|
89
96
|
- `--selection-mode <all|sync-all|custom>`
|
|
90
97
|
- `--source <value>`
|
|
91
98
|
- `--name <value>`
|
|
92
|
-
- `--entity <agent|command|mcp|skill>`
|
|
99
|
+
- `--entity <agent|command|mcp|rule|skill>`
|
|
93
100
|
|
|
94
101
|
### MCP manual server mode
|
|
95
102
|
|
|
@@ -107,6 +114,7 @@ agentloom add farnoodma/agents
|
|
|
107
114
|
agentloom agent add farnoodma/agents --agents issue-creator
|
|
108
115
|
agentloom command add farnoodma/agents --commands review
|
|
109
116
|
agentloom mcp add farnoodma/agents --mcps browser
|
|
117
|
+
agentloom rule add farnoodma/agents --rules always-test
|
|
110
118
|
agentloom skill add farnoodma/agents --skills pr-review
|
|
111
119
|
agentloom delete farnoodma/agents
|
|
112
120
|
agentloom mcp server add browser-tools --command npx --arg browser-tools-mcp
|
|
@@ -128,6 +136,7 @@ agentloom command --help
|
|
|
128
136
|
agentloom command add --help
|
|
129
137
|
agentloom mcp --help
|
|
130
138
|
agentloom mcp add --help
|
|
139
|
+
agentloom rule --help
|
|
131
140
|
agentloom mcp server --help
|
|
132
141
|
```
|
|
133
142
|
|
|
@@ -166,6 +175,7 @@ The install runs after the requested command completes so scope/provider selecti
|
|
|
166
175
|
Successful GitHub-based `agentloom add` imports can send anonymous telemetry
|
|
167
176
|
to the Agentloom directory API.
|
|
168
177
|
|
|
178
|
+
- tracked entities: agents, commands, rules, skills, MCP servers
|
|
169
179
|
- disable telemetry via `AGENTLOOM_DISABLE_TELEMETRY=1`
|
|
170
180
|
- override endpoint via `AGENTLOOM_TELEMETRY_ENDPOINT`
|
|
171
181
|
|
|
@@ -188,14 +198,67 @@ description: Review changes and report issues.
|
|
|
188
198
|
claude:
|
|
189
199
|
model: sonnet
|
|
190
200
|
codex:
|
|
191
|
-
model: gpt-5
|
|
192
|
-
reasoningEffort:
|
|
201
|
+
model: gpt-5-codex
|
|
202
|
+
reasoningEffort: medium
|
|
193
203
|
webSearch: true
|
|
194
204
|
---
|
|
195
205
|
|
|
196
206
|
You are a strict reviewer...
|
|
197
207
|
```
|
|
198
208
|
|
|
209
|
+
## Command schema
|
|
210
|
+
|
|
211
|
+
Canonical commands are markdown files. Frontmatter is optional. When present,
|
|
212
|
+
provider-specific command config can be nested per provider:
|
|
213
|
+
|
|
214
|
+
```md
|
|
215
|
+
---
|
|
216
|
+
copilot:
|
|
217
|
+
description: Review current changes
|
|
218
|
+
agent: agent
|
|
219
|
+
tools:
|
|
220
|
+
- codebase
|
|
221
|
+
model: gpt-5
|
|
222
|
+
argument-hint: "<scope>"
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# /review
|
|
226
|
+
|
|
227
|
+
Review active changes with scope $ARGUMENTS.
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Notes:
|
|
231
|
+
|
|
232
|
+
- Provider configs follow the same pattern as agents:
|
|
233
|
+
- omit provider key for default behavior
|
|
234
|
+
- `provider: { ... }` to add provider-specific overrides
|
|
235
|
+
- `provider: false` to disable output for that provider
|
|
236
|
+
- Provider-specific frontmatter keys are passed through as-is to that provider output.
|
|
237
|
+
- Canonical command bodies can use `$ARGUMENTS`; provider-specific placeholder translation is applied during sync (for example Copilot receives `${input:args}` and Gemini receives `{{args}}`).
|
|
238
|
+
|
|
239
|
+
## Rule schema
|
|
240
|
+
|
|
241
|
+
Canonical rules are markdown files under `.agents/rules/*.md`.
|
|
242
|
+
Rules require `frontmatter.name`. Additional frontmatter keys are preserved.
|
|
243
|
+
|
|
244
|
+
```md
|
|
245
|
+
---
|
|
246
|
+
name: Always run tests
|
|
247
|
+
description: Require tests before merge
|
|
248
|
+
globs:
|
|
249
|
+
- "**/*.ts"
|
|
250
|
+
alwaysApply: true
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
Before finishing any change, run the project checks and include the result.
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Notes:
|
|
257
|
+
|
|
258
|
+
- Rule ID is the canonical filename stem (for example `.agents/rules/always-test.md` -> `always-test`).
|
|
259
|
+
- Managed instruction-block rendering uses only `frontmatter.name` + body.
|
|
260
|
+
- Extra frontmatter keys are used only for Cursor `.cursor/rules/*.mdc` rendering.
|
|
261
|
+
|
|
199
262
|
## MCP schema
|
|
200
263
|
|
|
201
264
|
Canonical MCP file format:
|
|
@@ -234,6 +297,29 @@ For canonical commands (`.agents/commands`), Codex output is always written to
|
|
|
234
297
|
global prompts under `~/.codex/prompts` (Codex prompts are global-only), even
|
|
235
298
|
when syncing local scope.
|
|
236
299
|
|
|
300
|
+
## Rule sync behavior
|
|
301
|
+
|
|
302
|
+
Rule sync is additive on top of agent/command/MCP/skill sync:
|
|
303
|
+
|
|
304
|
+
- Local scope always updates managed rule blocks in `AGENTS.md`.
|
|
305
|
+
- Local + Cursor writes `.cursor/rules/<rule-id>.mdc`.
|
|
306
|
+
- Local + Claude writes managed blocks to `CLAUDE.md`.
|
|
307
|
+
- Local + Gemini writes managed blocks to `GEMINI.md`.
|
|
308
|
+
- Local + Copilot writes managed blocks to `.github/copilot-instructions.md`.
|
|
309
|
+
- Local + OpenCode/Codex/Pi use the same managed `AGENTS.md` baseline blocks.
|
|
310
|
+
- Global + Claude writes `~/.claude/CLAUDE.md`.
|
|
311
|
+
- Global + Gemini writes `~/.gemini/GEMINI.md`.
|
|
312
|
+
- Global + Copilot writes `~/.copilot/copilot-instructions.md` and adds that location to VS Code `chat.instructionsFilesLocations`.
|
|
313
|
+
- Global + OpenCode writes `~/.config/opencode/AGENTS.md`.
|
|
314
|
+
- Global Cursor/Codex/Pi rule settings are no-op unless a stable provider settings key exists.
|
|
315
|
+
|
|
316
|
+
## Provider contract notes
|
|
317
|
+
|
|
318
|
+
- Cursor provider output uses Cursor subagents (`.cursor/agents/*.md`).
|
|
319
|
+
- Gemini command sync writes TOML files (`.gemini/commands/*.toml`).
|
|
320
|
+
- Copilot global output uses `~/.copilot/{agents,prompts,skills}` and `~/.copilot/copilot-instructions.md` (workspace output remains `.github/*`).
|
|
321
|
+
- Claude global user-scope MCP is not file-synced by Agentloom. Project MCP remains synced via `.mcp.json`.
|
|
322
|
+
|
|
237
323
|
## Development
|
|
238
324
|
|
|
239
325
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runDeleteCommand } from "./commands/delete.js";
|
|
|
7
7
|
import { runFindCommand } from "./commands/find.js";
|
|
8
8
|
import { runInitCommand } from "./commands/init.js";
|
|
9
9
|
import { runMcpCommand } from "./commands/mcp.js";
|
|
10
|
+
import { runRuleCommand } from "./commands/rule.js";
|
|
10
11
|
import { runSkillCommand } from "./commands/skills.js";
|
|
11
12
|
import { runSyncCommand } from "./commands/sync.js";
|
|
12
13
|
import { runUpgradeCommand } from "./commands/upgrade.js";
|
|
@@ -54,9 +55,11 @@ export async function runCli(argv) {
|
|
|
54
55
|
yes: Boolean(parsed.yes),
|
|
55
56
|
cwd,
|
|
56
57
|
});
|
|
58
|
+
const bootstrapArgs = shouldBootstrapManageAgents
|
|
59
|
+
? buildManageAgentsBootstrapArgs(parsed, cwd)
|
|
60
|
+
: undefined;
|
|
57
61
|
await runRoutedCommand(route, parsed, cwd, command, version);
|
|
58
|
-
if (
|
|
59
|
-
const bootstrapArgs = buildManageAgentsBootstrapArgs(parsed, cwd);
|
|
62
|
+
if (bootstrapArgs) {
|
|
60
63
|
await runSkillCommand(parseArgs(bootstrapArgs), cwd);
|
|
61
64
|
}
|
|
62
65
|
}
|
|
@@ -111,13 +114,17 @@ async function runRoutedCommand(route, parsed, cwd, command, version) {
|
|
|
111
114
|
case "skill":
|
|
112
115
|
await runSkillCommand(parsed, cwd);
|
|
113
116
|
return;
|
|
117
|
+
case "rule":
|
|
118
|
+
await runRuleCommand(parsed, cwd);
|
|
119
|
+
return;
|
|
114
120
|
default:
|
|
115
121
|
throw new Error(formatUnknownCommandError(command));
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
function buildManageAgentsBootstrapArgs(parsed, cwd) {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
125
|
+
const inferredScope = resolveBootstrapScope(parsed);
|
|
126
|
+
const scope = inferredScope ?? "global";
|
|
127
|
+
const providers = resolveBootstrapProviders(parsed, cwd, inferredScope);
|
|
121
128
|
const args = [
|
|
122
129
|
"skill",
|
|
123
130
|
"add",
|
package/dist/commands/add.js
CHANGED
|
@@ -40,10 +40,12 @@ async function runEntityAwareAdd(options) {
|
|
|
40
40
|
const importAgents = options.target === "all" || options.target === "agent";
|
|
41
41
|
const importCommands = options.target === "all" || options.target === "command";
|
|
42
42
|
const importMcp = options.target === "all" || options.target === "mcp";
|
|
43
|
+
const importRules = options.target === "all" || options.target === "rule";
|
|
43
44
|
const importSkills = options.target === "all" || options.target === "skill";
|
|
44
45
|
const agentSelectors = getEntitySelectors(options.argv, "agent");
|
|
45
46
|
const commandSelectors = getEntitySelectors(options.argv, "command");
|
|
46
47
|
const mcpSelectors = getEntitySelectors(options.argv, "mcp");
|
|
48
|
+
const ruleSelectors = getEntitySelectors(options.argv, "rule");
|
|
47
49
|
const skillSelectors = getEntitySelectors(options.argv, "skill");
|
|
48
50
|
let resolvedSkillProviders;
|
|
49
51
|
const resolveProvidersForSkills = async () => {
|
|
@@ -82,6 +84,10 @@ async function runEntityAwareAdd(options) {
|
|
|
82
84
|
requireMcp: options.target === "mcp",
|
|
83
85
|
mcpSelectors,
|
|
84
86
|
promptForMcp: mcpSelectors.length === 0,
|
|
87
|
+
importRules,
|
|
88
|
+
requireRules: options.target === "rule",
|
|
89
|
+
ruleSelectors,
|
|
90
|
+
promptForRules: ruleSelectors.length === 0,
|
|
85
91
|
importSkills,
|
|
86
92
|
requireSkills: options.target === "skill",
|
|
87
93
|
skillSelectors,
|
|
@@ -95,12 +101,14 @@ async function runEntityAwareAdd(options) {
|
|
|
95
101
|
promptForAgentSelection: agentSelectors.length === 0,
|
|
96
102
|
selectionMode,
|
|
97
103
|
});
|
|
104
|
+
const importedRules = summary.importedRules ?? [];
|
|
98
105
|
console.log(`Imported source: ${summary.source}`);
|
|
99
106
|
console.log(`Source type: ${summary.sourceType}`);
|
|
100
107
|
console.log(`Resolved commit: ${summary.resolvedCommit}`);
|
|
101
108
|
console.log(`Imported agents: ${summary.importedAgents.length}`);
|
|
102
109
|
console.log(`Imported commands: ${summary.importedCommands.length}`);
|
|
103
110
|
console.log(`Imported MCP servers: ${summary.importedMcpServers.length}`);
|
|
111
|
+
console.log(`Imported rules: ${importedRules.length}`);
|
|
104
112
|
console.log(`Imported skills: ${summary.importedSkills.length}`);
|
|
105
113
|
await sendAddTelemetryEvent({
|
|
106
114
|
rawSource: source,
|
|
@@ -131,6 +139,9 @@ function buildAddUsage(target) {
|
|
|
131
139
|
if (target === "mcp") {
|
|
132
140
|
return "agentloom mcp add <source> [--ref <ref>] [--subdir <path>] [--mcps <name>] [options]";
|
|
133
141
|
}
|
|
142
|
+
if (target === "rule") {
|
|
143
|
+
return "agentloom rule add <source> [--ref <ref>] [--subdir <path>] [--rules <name>] [options]";
|
|
144
|
+
}
|
|
134
145
|
if (target === "skill") {
|
|
135
146
|
return "agentloom skill add <source> [--ref <ref>] [--subdir <path>] [--skills <name>] [options]";
|
|
136
147
|
}
|
|
@@ -146,6 +157,9 @@ function buildAddExample(target) {
|
|
|
146
157
|
if (target === "mcp") {
|
|
147
158
|
return "agentloom mcp add farnoodma/agents --mcps browser";
|
|
148
159
|
}
|
|
160
|
+
if (target === "rule") {
|
|
161
|
+
return "agentloom rule add farnoodma/agents --rules always-test";
|
|
162
|
+
}
|
|
149
163
|
if (target === "skill") {
|
|
150
164
|
return "agentloom skill add farnoodma/agents --skills code-review";
|
|
151
165
|
}
|
package/dist/commands/delete.js
CHANGED
|
@@ -6,10 +6,11 @@ import { parseCommandsDir } from "../core/commands.js";
|
|
|
6
6
|
import { formatUsageError } from "../core/copy.js";
|
|
7
7
|
import { readLockfile, writeLockfile } from "../core/lockfile.js";
|
|
8
8
|
import { readCanonicalMcp, writeCanonicalMcp } from "../core/mcp.js";
|
|
9
|
+
import { normalizeRuleSelector, parseRulesDir, stripRuleFileExtension, } from "../core/rules.js";
|
|
9
10
|
import { parseSourceSpec } from "../core/sources.js";
|
|
10
11
|
import { parseSkillsDir } from "../core/skills.js";
|
|
11
12
|
import { getNonInteractiveMode, resolvePathsForCommand, runPostMutationSync, } from "./entity-utils.js";
|
|
12
|
-
const ALL_ENTITIES = ["agent", "command", "mcp", "skill"];
|
|
13
|
+
const ALL_ENTITIES = ["agent", "command", "mcp", "rule", "skill"];
|
|
13
14
|
const MULTISELECT_HELP_TEXT = "↑↓ move, space select, enter confirm";
|
|
14
15
|
function withMultiselectHelp(message) {
|
|
15
16
|
return `${message}\n${MULTISELECT_HELP_TEXT}`;
|
|
@@ -165,6 +166,11 @@ async function deleteBySource(options) {
|
|
|
165
166
|
delete mcp.mcpServers[server];
|
|
166
167
|
}
|
|
167
168
|
}
|
|
169
|
+
if (options.entities.includes("rule")) {
|
|
170
|
+
for (const imported of entry.importedRules) {
|
|
171
|
+
removeIfExists(path.join(options.paths.agentsRoot, imported));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
168
174
|
if (options.entities.includes("skill")) {
|
|
169
175
|
for (const skill of entry.importedSkills) {
|
|
170
176
|
removeIfExists(path.join(options.paths.skillsDir, skill));
|
|
@@ -185,6 +191,7 @@ async function deleteBySource(options) {
|
|
|
185
191
|
}
|
|
186
192
|
async function deleteByName(options) {
|
|
187
193
|
let selectedEntities = options.entities;
|
|
194
|
+
let deletedRuleIdentifiers;
|
|
188
195
|
if (selectedEntities.length > 1) {
|
|
189
196
|
const matches = detectNameMatches(options.paths, options.candidate);
|
|
190
197
|
const matchingEntities = selectedEntities.filter((entity) => matches.includes(entity));
|
|
@@ -227,6 +234,14 @@ async function deleteByName(options) {
|
|
|
227
234
|
}
|
|
228
235
|
delete mcp.mcpServers[existing];
|
|
229
236
|
}
|
|
237
|
+
else if (entity === "rule") {
|
|
238
|
+
const deletedRule = deleteRuleByName(options.paths, options.candidate);
|
|
239
|
+
deletedRuleIdentifiers = [
|
|
240
|
+
deletedRule.name,
|
|
241
|
+
deletedRule.fileName,
|
|
242
|
+
stripRuleFileExtension(deletedRule.fileName),
|
|
243
|
+
];
|
|
244
|
+
}
|
|
230
245
|
else if (entity === "skill") {
|
|
231
246
|
deleteSkillByName(options.paths, options.candidate);
|
|
232
247
|
}
|
|
@@ -236,7 +251,7 @@ async function deleteByName(options) {
|
|
|
236
251
|
}
|
|
237
252
|
const lockfile = readLockfile(options.paths);
|
|
238
253
|
lockfile.entries = lockfile.entries
|
|
239
|
-
.map((entry) => removeNameFromEntry(entry, selectedEntities, options.candidate))
|
|
254
|
+
.map((entry) => removeNameFromEntry(entry, selectedEntities, options.candidate, deletedRuleIdentifiers))
|
|
240
255
|
.filter((entry) => Boolean(entry));
|
|
241
256
|
writeLockfile(options.paths, lockfile);
|
|
242
257
|
}
|
|
@@ -269,6 +284,15 @@ function deleteSkillByName(paths, name) {
|
|
|
269
284
|
}
|
|
270
285
|
removeIfExists(target.sourcePath);
|
|
271
286
|
}
|
|
287
|
+
function deleteRuleByName(paths, name) {
|
|
288
|
+
const rules = parseRulesDir(paths.rulesDir);
|
|
289
|
+
const target = rules.find((rule) => ruleMatchesCandidate(rule, name));
|
|
290
|
+
if (!target) {
|
|
291
|
+
throw new Error(`Rule "${name}" was not found in canonical rules.`);
|
|
292
|
+
}
|
|
293
|
+
removeIfExists(target.sourcePath);
|
|
294
|
+
return target;
|
|
295
|
+
}
|
|
272
296
|
function removeIfExists(filePath) {
|
|
273
297
|
if (!fs.existsSync(filePath))
|
|
274
298
|
return;
|
|
@@ -293,6 +317,9 @@ function detectNameMatches(paths, candidate) {
|
|
|
293
317
|
const hasMcp = Object.keys(mcp.mcpServers).some((name) => normalizeName(name) === normalized);
|
|
294
318
|
if (hasMcp)
|
|
295
319
|
matches.push("mcp");
|
|
320
|
+
const hasRule = parseRulesDir(paths.rulesDir).some((rule) => ruleMatchesCandidate(rule, candidate));
|
|
321
|
+
if (hasRule)
|
|
322
|
+
matches.push("rule");
|
|
296
323
|
const hasSkill = parseSkillsDir(paths.skillsDir).some((skill) => normalizeName(skill.name) === normalized);
|
|
297
324
|
if (hasSkill)
|
|
298
325
|
matches.push("skill");
|
|
@@ -322,6 +349,14 @@ function removeEntityDataFromEntry(entry, entities) {
|
|
|
322
349
|
selectedSourceMcpServers: undefined,
|
|
323
350
|
};
|
|
324
351
|
}
|
|
352
|
+
if (entities.includes("rule")) {
|
|
353
|
+
next = {
|
|
354
|
+
...next,
|
|
355
|
+
importedRules: [],
|
|
356
|
+
selectedSourceRules: undefined,
|
|
357
|
+
ruleRenameMap: undefined,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
325
360
|
if (entities.includes("skill")) {
|
|
326
361
|
next = {
|
|
327
362
|
...next,
|
|
@@ -333,7 +368,7 @@ function removeEntityDataFromEntry(entry, entities) {
|
|
|
333
368
|
}
|
|
334
369
|
return finalizeEntry(next);
|
|
335
370
|
}
|
|
336
|
-
function removeNameFromEntry(entry, entities, candidate) {
|
|
371
|
+
function removeNameFromEntry(entry, entities, candidate, deletedRuleIdentifiers) {
|
|
337
372
|
let next = { ...entry };
|
|
338
373
|
const normalized = normalizeName(candidate);
|
|
339
374
|
if (entities.includes("agent")) {
|
|
@@ -373,6 +408,37 @@ function removeNameFromEntry(entry, entities, candidate) {
|
|
|
373
408
|
next.selectedSourceMcpServers = [...next.importedMcpServers];
|
|
374
409
|
}
|
|
375
410
|
}
|
|
411
|
+
if (entities.includes("rule")) {
|
|
412
|
+
const before = [...next.importedRules];
|
|
413
|
+
const ruleMatchKeys = new Set([candidate, ...(deletedRuleIdentifiers ?? [])].flatMap(getRuleMatchKeys));
|
|
414
|
+
const matchesDeletedRule = (value) => getRuleMatchKeys(value).some((key) => ruleMatchKeys.has(key));
|
|
415
|
+
next.importedRules = next.importedRules.filter((item) => {
|
|
416
|
+
const base = path.basename(item).replace(/\.(md|mdc)$/i, "");
|
|
417
|
+
return !matchesDeletedRule(base);
|
|
418
|
+
});
|
|
419
|
+
if (next.importedRules.length !== before.length) {
|
|
420
|
+
const remainingImportedNames = new Set(next.importedRules.map((item) => path.basename(item)));
|
|
421
|
+
if (next.ruleRenameMap) {
|
|
422
|
+
const filteredRenameEntries = Object.entries(next.ruleRenameMap).filter(([, importedName]) => remainingImportedNames.has(path.basename(importedName)));
|
|
423
|
+
next.ruleRenameMap =
|
|
424
|
+
filteredRenameEntries.length > 0
|
|
425
|
+
? Object.fromEntries(filteredRenameEntries)
|
|
426
|
+
: undefined;
|
|
427
|
+
}
|
|
428
|
+
const selectorsFromRenameMap = next.ruleRenameMap
|
|
429
|
+
? Object.keys(next.ruleRenameMap)
|
|
430
|
+
: [];
|
|
431
|
+
const renamedRuleTargets = new Set(Object.values(next.ruleRenameMap ?? {}).map((importedName) => normalizeName(path.basename(importedName))));
|
|
432
|
+
const selectorsFromUnrenamedRules = next.importedRules
|
|
433
|
+
.map((item) => path.basename(item))
|
|
434
|
+
.filter((item) => !renamedRuleTargets.has(normalizeName(path.basename(item))));
|
|
435
|
+
next.selectedSourceRules =
|
|
436
|
+
selectorsFromRenameMap.length > 0 ||
|
|
437
|
+
selectorsFromUnrenamedRules.length > 0
|
|
438
|
+
? [...selectorsFromRenameMap, ...selectorsFromUnrenamedRules]
|
|
439
|
+
: [];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
376
442
|
if (entities.includes("skill")) {
|
|
377
443
|
const renameEntriesBeforeRemoval = Object.entries(next.skillRenameMap ?? {});
|
|
378
444
|
const removedSkillTargets = next.importedSkills.filter((name) => normalizeName(name) === normalized);
|
|
@@ -400,9 +466,19 @@ function removeNameFromEntry(entry, entities, candidate) {
|
|
|
400
466
|
}
|
|
401
467
|
return finalizeEntry(next);
|
|
402
468
|
}
|
|
469
|
+
function ruleMatchesCandidate(rule, candidate) {
|
|
470
|
+
const candidateKeys = new Set(getRuleMatchKeys(candidate));
|
|
471
|
+
return [rule.name, rule.fileName, stripRuleFileExtension(rule.fileName)].some((value) => getRuleMatchKeys(value).some((key) => candidateKeys.has(key)));
|
|
472
|
+
}
|
|
473
|
+
function getRuleMatchKeys(value) {
|
|
474
|
+
const normalized = normalizeName(value);
|
|
475
|
+
const selector = normalizeRuleSelector(value);
|
|
476
|
+
return [...new Set([normalized, selector].filter(Boolean))];
|
|
477
|
+
}
|
|
403
478
|
function finalizeEntry(entry) {
|
|
404
479
|
const hasOtherEntities = entry.importedAgents.length > 0 ||
|
|
405
480
|
entry.importedMcpServers.length > 0 ||
|
|
481
|
+
entry.importedRules.length > 0 ||
|
|
406
482
|
entry.importedSkills.length > 0;
|
|
407
483
|
if (entry.importedCommands.length === 0) {
|
|
408
484
|
entry.commandRenameMap = undefined;
|
|
@@ -412,6 +488,10 @@ function finalizeEntry(entry) {
|
|
|
412
488
|
entry.skillRenameMap = undefined;
|
|
413
489
|
entry.skillsProviders = undefined;
|
|
414
490
|
}
|
|
491
|
+
if (entry.importedRules.length === 0) {
|
|
492
|
+
entry.ruleRenameMap = undefined;
|
|
493
|
+
entry.selectedSourceRules = hasOtherEntities ? [] : undefined;
|
|
494
|
+
}
|
|
415
495
|
const trackedEntities = (entry.trackedEntities ?? []).filter((entity) => {
|
|
416
496
|
if (entity === "agent") {
|
|
417
497
|
return entry.importedAgents.length > 0 || Boolean(entry.requestedAgents);
|
|
@@ -425,6 +505,11 @@ function finalizeEntry(entry) {
|
|
|
425
505
|
return (entry.importedMcpServers.length > 0 ||
|
|
426
506
|
Boolean(entry.selectedSourceMcpServers));
|
|
427
507
|
}
|
|
508
|
+
if (entity === "rule") {
|
|
509
|
+
return (entry.importedRules.length > 0 ||
|
|
510
|
+
Boolean(entry.selectedSourceRules) ||
|
|
511
|
+
Boolean(entry.ruleRenameMap));
|
|
512
|
+
}
|
|
428
513
|
return (entry.importedSkills.length > 0 ||
|
|
429
514
|
Boolean(entry.selectedSourceSkills) ||
|
|
430
515
|
Boolean(entry.skillsProviders) ||
|
|
@@ -437,6 +522,7 @@ function finalizeEntry(entry) {
|
|
|
437
522
|
if (next.importedAgents.length === 0 &&
|
|
438
523
|
next.importedCommands.length === 0 &&
|
|
439
524
|
next.importedMcpServers.length === 0 &&
|
|
525
|
+
next.importedRules.length === 0 &&
|
|
440
526
|
next.importedSkills.length === 0) {
|
|
441
527
|
return null;
|
|
442
528
|
}
|
|
@@ -32,6 +32,9 @@ export function getEntitySelectors(argv, entity) {
|
|
|
32
32
|
if (entity === "mcp") {
|
|
33
33
|
return getStringArrayFlag(argsRecord.mcps, getStringArrayFlag(argsRecord.mcp));
|
|
34
34
|
}
|
|
35
|
+
if (entity === "rule") {
|
|
36
|
+
return getStringArrayFlag(argsRecord.rules, getStringArrayFlag(argsRecord.rule));
|
|
37
|
+
}
|
|
35
38
|
return getStringArrayFlag(argsRecord.skills, getStringArrayFlag(argsRecord.skill));
|
|
36
39
|
}
|
|
37
40
|
export async function runPostMutationSync(options) {
|