@agentrules/core 0.1.0 → 0.2.0

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.
Files changed (4) hide show
  1. package/README.md +80 -54
  2. package/dist/index.d.ts +600 -786
  3. package/dist/index.js +302 -313
  4. package/package.json +15 -10
package/dist/index.js CHANGED
@@ -11,172 +11,174 @@ const PLATFORM_ID_TUPLE = [
11
11
  "claude",
12
12
  "cursor"
13
13
  ];
14
+ /** Tuple of all rule types for schema validation */
15
+ const RULE_TYPE_TUPLE = [
16
+ "instruction",
17
+ "rule",
18
+ "command",
19
+ "skill",
20
+ "agent",
21
+ "tool"
22
+ ];
14
23
 
15
24
  //#endregion
16
25
  //#region src/platform/config.ts
17
26
  const PLATFORM_IDS = PLATFORM_ID_TUPLE;
18
27
  /**
19
- * Platform configuration including supported rule types and install paths.
28
+ * Platform configuration including supported types and install paths.
20
29
  */
21
30
  const PLATFORMS = {
22
31
  opencode: {
23
32
  label: "OpenCode",
24
- projectDir: ".opencode",
33
+ platformDir: ".opencode",
25
34
  globalDir: "~/.config/opencode",
26
35
  types: {
27
36
  instruction: {
28
37
  description: "Project instructions",
29
- format: "markdown",
30
- extension: "md",
31
- projectPath: "AGENTS.md",
32
- globalPath: "~/.config/opencode/AGENTS.md"
38
+ project: "AGENTS.md",
39
+ global: "{platformDir}/AGENTS.md"
33
40
  },
34
41
  agent: {
35
42
  description: "Specialized AI agent",
36
- format: "markdown",
37
- extension: "md",
38
- projectPath: ".opencode/agent/{name}.md",
39
- globalPath: "~/.config/opencode/agent/{name}.md"
43
+ project: "{platformDir}/agent/{name}.md",
44
+ global: "{platformDir}/agent/{name}.md"
40
45
  },
41
46
  command: {
42
47
  description: "Custom slash command",
43
- format: "markdown",
44
- extension: "md",
45
- projectPath: ".opencode/command/{name}.md",
46
- globalPath: "~/.config/opencode/command/{name}.md"
48
+ project: "{platformDir}/command/{name}.md",
49
+ global: "{platformDir}/command/{name}.md"
47
50
  },
48
51
  tool: {
49
52
  description: "Custom tool",
50
- format: "typescript",
51
- extension: "ts",
52
- projectPath: ".opencode/tool/{name}.ts",
53
- globalPath: "~/.config/opencode/tool/{name}.ts"
53
+ project: "{platformDir}/tool/{name}.ts",
54
+ global: "{platformDir}/tool/{name}.ts"
54
55
  }
55
56
  }
56
57
  },
57
58
  claude: {
58
59
  label: "Claude Code",
59
- projectDir: ".claude",
60
+ platformDir: ".claude",
60
61
  globalDir: "~/.claude",
61
62
  types: {
62
63
  instruction: {
63
64
  description: "Project instructions",
64
- format: "markdown",
65
- extension: "md",
66
- projectPath: "CLAUDE.md",
67
- globalPath: "~/.claude/CLAUDE.md"
65
+ project: "CLAUDE.md",
66
+ global: "{platformDir}/CLAUDE.md"
67
+ },
68
+ rule: {
69
+ description: "Project rule",
70
+ project: "{platformDir}/rules/{name}.md",
71
+ global: "{platformDir}/rules/{name}.md"
68
72
  },
69
73
  command: {
70
74
  description: "Custom slash command",
71
- format: "markdown",
72
- extension: "md",
73
- projectPath: ".claude/commands/{name}.md",
74
- globalPath: "~/.claude/commands/{name}.md"
75
+ project: "{platformDir}/commands/{name}.md",
76
+ global: "{platformDir}/commands/{name}.md"
75
77
  },
76
78
  skill: {
77
79
  description: "Custom skill",
78
- format: "markdown",
79
- extension: "md",
80
- projectPath: ".claude/skills/{name}/SKILL.md",
81
- globalPath: "~/.claude/skills/{name}/SKILL.md"
80
+ project: "{platformDir}/skills/{name}/SKILL.md",
81
+ global: "{platformDir}/skills/{name}/SKILL.md"
82
82
  }
83
83
  }
84
84
  },
85
85
  cursor: {
86
86
  label: "Cursor",
87
- projectDir: ".cursor",
87
+ platformDir: ".cursor",
88
88
  globalDir: "~/.cursor",
89
89
  types: {
90
90
  instruction: {
91
91
  description: "Project instructions",
92
- format: "markdown",
93
- extension: "md",
94
- projectPath: "AGENTS.md",
95
- globalPath: null
92
+ project: "AGENTS.md",
93
+ global: null
96
94
  },
97
95
  rule: {
98
96
  description: "Custom rule",
99
- format: "mdc",
100
- extension: "mdc",
101
- projectPath: ".cursor/rules/{name}.mdc",
102
- globalPath: null
97
+ project: "{platformDir}/rules/{name}.mdc",
98
+ global: null
103
99
  },
104
100
  command: {
105
101
  description: "Custom slash command",
106
- format: "markdown",
107
- extension: "md",
108
- projectPath: ".cursor/commands/{name}.md",
109
- globalPath: "~/.cursor/commands/{name}.md"
102
+ project: "{platformDir}/commands/{name}.md",
103
+ global: "{platformDir}/commands/{name}.md"
110
104
  }
111
105
  }
112
106
  },
113
107
  codex: {
114
108
  label: "Codex",
115
- projectDir: ".codex",
109
+ platformDir: ".codex",
116
110
  globalDir: "~/.codex",
117
111
  types: {
118
112
  instruction: {
119
113
  description: "Project instructions",
120
- format: "markdown",
121
- extension: "md",
122
- projectPath: "AGENTS.md",
123
- globalPath: "~/.codex/AGENTS.md"
114
+ project: "AGENTS.md",
115
+ global: "{platformDir}/AGENTS.md"
124
116
  },
125
117
  command: {
126
118
  description: "Custom prompt",
127
- format: "markdown",
128
- extension: "md",
129
- projectPath: null,
130
- globalPath: "~/.codex/prompts/{name}.md"
119
+ project: null,
120
+ global: "{platformDir}/prompts/{name}.md"
131
121
  }
132
122
  }
133
123
  }
134
124
  };
135
- /** Valid rule types for each platform. Must be kept in sync with PLATFORMS. */
125
+ /** Get valid types for a specific platform */
126
+ function getValidTypes(platform) {
127
+ return Object.keys(PLATFORMS[platform].types);
128
+ }
129
+ /** Check if a type is valid for a given platform */
130
+ function isValidType(platform, type) {
131
+ return type in PLATFORMS[platform].types;
132
+ }
133
+ /** Get the configuration for a specific platform + type combination */
134
+ function getTypeConfig(platform, type) {
135
+ const platformConfig = PLATFORMS[platform];
136
+ return platformConfig.types[type];
137
+ }
138
+ function supportsInstallPath({ platform, type, scope = "project" }) {
139
+ const typeConfig = getTypeConfig(platform, type);
140
+ if (!typeConfig) return false;
141
+ const template = scope === "project" ? typeConfig.project : typeConfig.global;
142
+ return template !== null;
143
+ }
144
+ function getInstallPath({ platform, type, name, scope = "project" }) {
145
+ const platformConfig = PLATFORMS[platform];
146
+ const typeConfig = getTypeConfig(platform, type);
147
+ if (!typeConfig) return null;
148
+ const template = scope === "project" ? typeConfig.project : typeConfig.global;
149
+ if (!template) return null;
150
+ const rootDir = scope === "project" ? platformConfig.platformDir : platformConfig.globalDir;
151
+ if (template.includes("{name}") && !name) throw new Error(`Missing name for install path: platform="${platform}" type="${type}" scope="${scope}"`);
152
+ return template.replace("{platformDir}", rootDir).replace("{name}", name ?? "");
153
+ }
154
+ /** Get platform configuration */
155
+ function getPlatformConfig(platform) {
156
+ return PLATFORMS[platform];
157
+ }
158
+ /**
159
+ * Platform-specific type tuples for zod schema validation.
160
+ * Must be kept in sync with PLATFORMS types above.
161
+ */
136
162
  const PLATFORM_RULE_TYPES = {
137
163
  opencode: [
138
164
  "instruction",
139
- "command",
140
165
  "agent",
166
+ "command",
141
167
  "tool"
142
168
  ],
143
169
  claude: [
144
170
  "instruction",
171
+ "rule",
145
172
  "command",
146
173
  "skill"
147
174
  ],
148
- codex: ["instruction", "command"],
149
175
  cursor: [
150
176
  "instruction",
151
- "command",
152
- "rule"
153
- ]
177
+ "rule",
178
+ "command"
179
+ ],
180
+ codex: ["instruction", "command"]
154
181
  };
155
- /** Get valid rule types for a specific platform */
156
- function getValidRuleTypes(platform) {
157
- return PLATFORM_RULE_TYPES[platform];
158
- }
159
- /** Check if a type is valid for a given platform */
160
- function isValidRuleType(platform, type) {
161
- return PLATFORM_RULE_TYPES[platform].includes(type);
162
- }
163
- /** Get the configuration for a specific platform + type combination */
164
- function getRuleTypeConfig(platform, type) {
165
- const platformConfig = PLATFORMS[platform];
166
- return platformConfig.types[type];
167
- }
168
- /** Get the install path for a rule, replacing {name} placeholder */
169
- function getInstallPath(platform, type, name, location = "project") {
170
- const config = getRuleTypeConfig(platform, type);
171
- if (!config) return null;
172
- const pathTemplate = location === "project" ? config.projectPath : config.globalPath;
173
- if (!pathTemplate) return null;
174
- return pathTemplate.replace("{name}", name);
175
- }
176
- /** Get platform configuration */
177
- function getPlatformConfig(platform) {
178
- return PLATFORMS[platform];
179
- }
180
182
 
181
183
  //#endregion
182
184
  //#region src/platform/utils.ts
@@ -189,19 +191,89 @@ function normalizePlatformInput(value) {
189
191
  throw new Error(`Unknown platform "${value}". Supported platforms: ${PLATFORM_IDS.join(", ")}.`);
190
192
  }
191
193
  /**
192
- * Check if a directory name matches a platform's projectDir.
193
- * Used to detect if a preset config is inside a platform directory (in-project mode).
194
+ * Check if a directory name matches a platform's platformDir.
195
+ * Used to detect if a rule config is inside a platform directory (in-project mode).
194
196
  */
195
197
  function isPlatformDir(dirName) {
196
- return PLATFORM_IDS.some((id) => PLATFORMS[id].projectDir === dirName);
198
+ return PLATFORM_IDS.some((id) => PLATFORMS[id].platformDir === dirName);
197
199
  }
198
200
  /**
199
- * Get the platform ID from a directory name, if it matches a platform's projectDir.
201
+ * Get the platform ID from a directory name, if it matches a platform's platformDir.
200
202
  */
201
203
  function getPlatformFromDir(dirName) {
202
- for (const id of PLATFORM_IDS) if (PLATFORMS[id].projectDir === dirName) return id;
204
+ for (const id of PLATFORM_IDS) if (PLATFORMS[id].platformDir === dirName) return id;
205
+ return;
206
+ }
207
+ function normalizePathForInference(value) {
208
+ return value.replace(/\\/g, "/");
209
+ }
210
+ function getBasename(value) {
211
+ const normalized = normalizePathForInference(value);
212
+ const segments = normalized.split("/").filter(Boolean);
213
+ return segments.at(-1) ?? "";
214
+ }
215
+ /**
216
+ * Infer the platform from a file path by searching for platformDir segments.
217
+ *
218
+ * Example: "/repo/.claude/commands/foo.md" -> "claude"
219
+ */
220
+ function inferPlatformFromPath(value) {
221
+ const normalized = normalizePathForInference(value);
222
+ const segments = normalized.split("/").filter(Boolean);
223
+ const matches = PLATFORM_IDS.filter((id) => segments.includes(PLATFORMS[id].platformDir));
224
+ if (matches.length === 1) return matches[0];
203
225
  return;
204
226
  }
227
+ /**
228
+ * Return all platforms whose instruction file matches this basename.
229
+ *
230
+ * Example: "CLAUDE.md" -> ["claude"], "AGENTS.md" -> ["opencode", "cursor", "codex"]
231
+ */
232
+ function inferInstructionPlatformsFromFileName(fileName) {
233
+ const matches = [];
234
+ for (const id of PLATFORM_IDS) {
235
+ const instructionPath = getInstallPath({
236
+ platform: id,
237
+ type: "instruction",
238
+ scope: "project"
239
+ });
240
+ if (instructionPath === fileName) matches.push(id);
241
+ }
242
+ return matches;
243
+ }
244
+ function getProjectTypeDirMap(platform) {
245
+ const map = new Map();
246
+ for (const [type, cfg] of Object.entries(PLATFORMS[platform].types)) {
247
+ const template = cfg.project;
248
+ if (!template) continue;
249
+ if (!template.startsWith("{platformDir}/")) continue;
250
+ const rest = template.slice(14);
251
+ const dir = rest.split("/")[0];
252
+ if (!dir || dir.includes("{")) continue;
253
+ map.set(dir, type);
254
+ }
255
+ return map;
256
+ }
257
+ /**
258
+ * Infer a rule type from a file path for a known platform.
259
+ * Uses PLATFORMS templates as source-of-truth.
260
+ */
261
+ function inferTypeFromPath(platform, filePath) {
262
+ const base = getBasename(filePath);
263
+ const instructionPath = getInstallPath({
264
+ platform,
265
+ type: "instruction",
266
+ scope: "project"
267
+ });
268
+ if (instructionPath === base) return "instruction";
269
+ const normalized = normalizePathForInference(filePath);
270
+ const segments = normalized.split("/").filter(Boolean);
271
+ const platformDirIndex = segments.lastIndexOf(PLATFORMS[platform].platformDir);
272
+ if (platformDirIndex < 0) return;
273
+ const nextDir = segments[platformDirIndex + 1];
274
+ if (!nextDir) return;
275
+ return getProjectTypeDirMap(platform).get(nextDir);
276
+ }
205
277
 
206
278
  //#endregion
207
279
  //#region src/utils/encoding.ts
@@ -215,7 +287,10 @@ function encodeUtf8(value) {
215
287
  function decodeUtf8(payload) {
216
288
  const bytes = toUint8Array(payload);
217
289
  if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("utf8");
218
- return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
290
+ return new TextDecoder("utf-8", {
291
+ fatal: false,
292
+ ignoreBOM: false
293
+ }).decode(bytes);
219
294
  }
220
295
  function toUint8Array(payload) {
221
296
  if (payload instanceof Uint8Array) return payload;
@@ -237,13 +312,13 @@ const TAG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-tag)";
237
312
  const tagSchema = z.string().trim().min(1, "Tag cannot be empty").max(35, "Tag must be 35 characters or less").regex(TAG_REGEX, TAG_ERROR).refine((tag) => !PLATFORM_ID_SET.has(tag), { message: "Platform names cannot be used as tags (redundant with platform field)" });
238
313
  /**
239
314
  * Schema for tags array.
240
- * - 1-10 tags required
315
+ * - 0-10 tags allowed
241
316
  */
242
- const tagsSchema = z.array(tagSchema).min(1, "At least one tag is required").max(10, "Maximum 10 tags allowed");
317
+ const tagsSchema = z.array(tagSchema).max(10, "Maximum 10 tags allowed");
243
318
  const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
244
- const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
319
+ const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-rule)";
245
320
  /**
246
- * Schema for preset/rule name.
321
+ * Schema for rule name.
247
322
  * - Max 64 characters
248
323
  * - Lowercase alphanumeric with hyphens
249
324
  */
@@ -260,14 +335,11 @@ const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title
260
335
  const descriptionSchema = z.string().trim().max(500, "Description must be 500 characters or less");
261
336
 
262
337
  //#endregion
263
- //#region src/preset/schema.ts
338
+ //#region src/rule/schema.ts
264
339
  const VERSION_REGEX$1 = /^[1-9]\d*\.\d+$/;
265
340
  const platformIdSchema = z.enum(PLATFORM_IDS);
266
- /**
267
- * Schema for required description (presets require a description).
268
- * Extends base descriptionSchema with min(1) constraint.
269
- */
270
- const requiredDescriptionSchema = descriptionSchema.min(1, "Description is required");
341
+ const normalizedDescriptionSchema = descriptionSchema.optional().default("");
342
+ const normalizedTagsSchema = tagsSchema.optional().default([]);
271
343
  const versionSchema = z.string().trim().regex(VERSION_REGEX$1, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
272
344
  const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
273
345
  const featureSchema = z.string().trim().min(1, "Feature cannot be empty").max(100, "Feature must be 100 characters or less");
@@ -287,12 +359,6 @@ const pathSchema = z.string().trim().min(1);
287
359
  const ignorePatternSchema = z.string().trim().min(1, "Ignore pattern cannot be empty");
288
360
  const ignoreSchema = z.array(ignorePatternSchema).max(50, "Maximum 50 ignore patterns allowed");
289
361
  /**
290
- * Schema for agentrulesDir - directory containing metadata files (README, LICENSE, INSTALL).
291
- * Defaults to ".agentrules" if not specified.
292
- * Use "." to read metadata from the project root.
293
- */
294
- const agentrulesPathSchema = z.string().trim().min(1, "agentrulesDir cannot be empty");
295
- /**
296
362
  * Platform entry - either a platform ID string or an object with optional path.
297
363
  *
298
364
  * Examples:
@@ -305,24 +371,32 @@ const platformEntryObjectSchema = z.object({
305
371
  }).strict();
306
372
  const platformEntrySchema = z.union([platformIdSchema, platformEntryObjectSchema]);
307
373
  /**
308
- * Preset config schema.
374
+ * Schema for rule type.
375
+ * Valid types: instruction, rule, command, skill, agent, tool, multi.
376
+ * Optional - defaults to "multi" (freeform file structure).
377
+ * Platform-specific validation happens at publish time.
378
+ */
379
+ const ruleTypeSchema = z.enum(RULE_TYPE_TUPLE);
380
+ const typeSchema = ruleTypeSchema.optional();
381
+ /**
382
+ * Rule config schema.
309
383
  *
310
384
  * Uses a unified `platforms` array that accepts either:
311
385
  * - Platform ID strings: `["opencode", "claude"]`
312
386
  * - Objects with optional path: `[{ platform: "opencode", path: "rules" }]`
313
387
  * - Mixed: `["opencode", { platform: "claude", path: "my-claude" }]`
314
388
  */
315
- const presetConfigSchema = z.object({
389
+ const ruleConfigSchema = z.object({
316
390
  $schema: z.string().optional(),
317
391
  name: nameSchema,
392
+ type: typeSchema,
318
393
  title: titleSchema,
319
394
  version: majorVersionSchema.optional(),
320
- description: requiredDescriptionSchema,
321
- tags: tagsSchema,
395
+ description: normalizedDescriptionSchema,
396
+ tags: normalizedTagsSchema,
322
397
  features: featuresSchema.optional(),
323
398
  license: licenseSchema,
324
399
  ignore: ignoreSchema.optional(),
325
- agentrulesDir: agentrulesPathSchema.optional(),
326
400
  platforms: z.array(platformEntrySchema).min(1, "At least one platform is required")
327
401
  }).strict();
328
402
  const bundledFileSchema = z.object({
@@ -342,46 +416,74 @@ const publishVariantInputSchema = z.object({
342
416
  installMessage: installMessageSchema.optional()
343
417
  });
344
418
  /**
345
- * Schema for what clients send to publish a preset (multi-platform).
419
+ * Schema for what clients send to publish a rule.
346
420
  *
347
421
  * One publish call creates ONE version with ALL platform variants.
348
422
  * Version is optional major version. Registry assigns full MAJOR.MINOR.
349
423
  *
350
- * Note: Clients send `name` (e.g., "my-preset"), and the registry defines the format of the slug.
351
- * For example, a namespaced slug could be returned as "username/my-preset"
424
+ * Note: Clients send `name` (e.g., "my-rule"), and the registry defines the format of the slug.
425
+ * For example, a namespaced slug could be returned as "username/my-rule"
352
426
  */
353
- const presetPublishInputSchema = z.object({
427
+ const rulePublishInputSchema = z.object({
354
428
  name: nameSchema,
429
+ type: typeSchema,
355
430
  title: titleSchema,
356
- description: requiredDescriptionSchema,
357
- tags: tagsSchema,
431
+ description: normalizedDescriptionSchema,
432
+ tags: normalizedTagsSchema,
358
433
  license: licenseSchema,
359
434
  features: featuresSchema.optional(),
360
435
  variants: z.array(publishVariantInputSchema).min(1, "At least one platform variant is required"),
361
436
  version: majorVersionSchema.optional()
362
437
  });
363
438
  /**
364
- * Schema for what registries store and return.
365
- * Includes full namespaced slug and version assigned by registry.
439
+ * Schema for what registries store and return for a single platform bundle.
440
+ * This is per-platform (flat structure), stored in R2 and fetched via bundleUrl.
366
441
  */
367
- const presetBundleSchema = presetPublishInputSchema.omit({
368
- name: true,
369
- version: true
370
- }).extend({
442
+ const ruleBundleSchema = z.object({
443
+ name: nameSchema,
444
+ type: typeSchema,
445
+ platform: platformIdSchema,
446
+ title: titleSchema,
447
+ description: normalizedDescriptionSchema,
448
+ tags: normalizedTagsSchema,
449
+ license: licenseSchema,
450
+ features: featuresSchema.optional(),
451
+ files: z.array(bundledFileSchema).min(1, "At least one file is required"),
452
+ readmeContent: contentSchema.optional(),
453
+ licenseContent: contentSchema.optional(),
454
+ installMessage: installMessageSchema.optional(),
371
455
  slug: z.string().trim().min(1),
372
456
  version: versionSchema
373
457
  });
374
458
 
375
459
  //#endregion
376
- //#region src/preset/types.ts
377
- /**
378
- * Normalize a raw platform entry to the object form.
379
- */
460
+ //#region src/rule/types.ts
461
+ /** Normalize a raw platform entry to the object form */
380
462
  function normalizePlatformEntry(entry) {
381
463
  if (typeof entry === "string") return { platform: entry };
382
464
  return entry;
383
465
  }
384
466
 
467
+ //#endregion
468
+ //#region src/rule/validate.ts
469
+ function validateRule(config) {
470
+ const errors = [];
471
+ const warnings = [];
472
+ if (!config.platforms || config.platforms.length === 0) errors.push("At least one platform is required.");
473
+ for (const entry of config.platforms) if (!isSupportedPlatform(entry.platform)) errors.push(`Unknown platform "${entry.platform}". Supported: ${PLATFORM_IDS.join(", ")}`);
474
+ if (config.type) for (const entry of config.platforms) {
475
+ if (!isSupportedPlatform(entry.platform)) continue;
476
+ if (!isValidType(entry.platform, config.type)) errors.push(`Platform "${entry.platform}" does not support type "${config.type}". Rule "${config.name}" cannot target this platform with type "${config.type}".`);
477
+ }
478
+ const hasPlaceholderFeatures = config.features?.some((feature) => feature.trim().startsWith("//"));
479
+ if (hasPlaceholderFeatures) errors.push("Replace placeholder comments in features before publishing.");
480
+ return {
481
+ valid: errors.length === 0,
482
+ errors,
483
+ warnings
484
+ };
485
+ }
486
+
385
487
  //#endregion
386
488
  //#region src/builder/utils.ts
387
489
  function cleanInstallMessage(value) {
@@ -390,19 +492,19 @@ function cleanInstallMessage(value) {
390
492
  return trimmed.length > 0 ? trimmed : void 0;
391
493
  }
392
494
  /**
393
- * Validate raw preset config from JSON.
495
+ * Validate raw rule config from JSON.
394
496
  * Returns the raw config shape (before normalization).
395
497
  */
396
- function validatePresetConfig(config, slug) {
498
+ function validateConfig(config, slug) {
397
499
  try {
398
- return presetConfigSchema.parse(config);
500
+ return ruleConfigSchema.parse(config);
399
501
  } catch (e) {
400
502
  if (e instanceof ZodError) {
401
503
  const messages = e.issues.map((issue) => {
402
504
  const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
403
505
  return `${path}${issue.message}`;
404
506
  });
405
- throw new Error(`Invalid preset config for ${slug}:\n - ${messages.join("\n - ")}`);
507
+ throw new Error(`Invalid rule config for ${slug}:\n - ${messages.join("\n - ")}`);
406
508
  }
407
509
  throw e;
408
510
  }
@@ -425,34 +527,39 @@ async function sha256(data) {
425
527
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
426
528
  }
427
529
  /**
428
- * Builds a PresetPublishInput from preset input.
530
+ * Builds a RulePublishInput from rule input.
429
531
  *
430
- * PresetInput always has platformFiles array (unified format).
532
+ * RuleInput always has platformFiles array (unified format).
431
533
  */
432
- async function buildPresetPublishInput(options) {
433
- const { preset, version } = options;
434
- if (!NAME_PATTERN.test(preset.name)) throw new Error(`Invalid name "${preset.name}". Names must be lowercase kebab-case.`);
435
- const config = validatePresetConfig(preset.config, preset.name);
534
+ async function buildPublishInput(options) {
535
+ const { rule, version } = options;
536
+ if (!NAME_PATTERN.test(rule.name)) throw new Error(`Invalid name "${rule.name}". Names must be lowercase kebab-case.`);
537
+ const config = validateConfig(rule.config, rule.name);
436
538
  const majorVersion = version ?? config.version;
437
- const platforms = preset.config.platforms;
438
- if (platforms.length === 0) throw new Error(`Preset ${preset.name} must specify at least one platform.`);
439
- for (const entry of platforms) ensureKnownPlatform(entry.platform, preset.name);
539
+ const platforms = rule.config.platforms;
540
+ if (platforms.length === 0) throw new Error(`Rule ${rule.name} must specify at least one platform.`);
541
+ for (const entry of platforms) ensureKnownPlatform(entry.platform, rule.name);
542
+ const ruleType = config.type;
543
+ if (ruleType) {
544
+ for (const entry of platforms) if (!isValidType(entry.platform, ruleType)) throw new Error(`Platform "${entry.platform}" does not support type "${ruleType}". Rule "${rule.name}" cannot target this platform with type "${ruleType}".`);
545
+ }
440
546
  const variants = [];
441
547
  for (const entry of platforms) {
442
- const platformData = preset.platformFiles.find((pf) => pf.platform === entry.platform);
443
- if (!platformData) throw new Error(`Preset ${preset.name} is missing files for platform "${entry.platform}".`);
444
- if (platformData.files.length === 0) throw new Error(`Preset ${preset.name} has no files for platform "${entry.platform}".`);
548
+ const platformData = rule.platformFiles.find((pf) => pf.platform === entry.platform);
549
+ if (!platformData) throw new Error(`Rule ${rule.name} is missing files for platform "${entry.platform}".`);
550
+ if (platformData.files.length === 0) throw new Error(`Rule ${rule.name} has no files for platform "${entry.platform}".`);
445
551
  const files = await createBundledFilesFromInputs(platformData.files);
446
552
  variants.push({
447
553
  platform: entry.platform,
448
554
  files,
449
- readmeContent: platformData.readmeContent?.trim() || preset.readmeContent?.trim() || void 0,
450
- licenseContent: platformData.licenseContent?.trim() || preset.licenseContent?.trim() || void 0,
451
- installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(preset.installMessage)
555
+ readmeContent: platformData.readmeContent?.trim() || rule.readmeContent?.trim() || void 0,
556
+ licenseContent: platformData.licenseContent?.trim() || rule.licenseContent?.trim() || void 0,
557
+ installMessage: cleanInstallMessage(platformData.installMessage) || cleanInstallMessage(rule.installMessage)
452
558
  });
453
559
  }
454
560
  return {
455
- name: preset.name,
561
+ name: rule.name,
562
+ ...ruleType && { type: ruleType },
456
563
  title: config.title,
457
564
  description: config.description,
458
565
  tags: config.tags ?? [],
@@ -466,28 +573,28 @@ async function buildPresetPublishInput(options) {
466
573
  * Builds a static registry with items and bundles.
467
574
  *
468
575
  * Uses the same model as dynamic publishing:
469
- * - Each PresetInput (single or multi-platform) becomes one item
576
+ * - Each RuleInput (single or multi-platform) becomes one item
470
577
  * - Each platform variant becomes one bundle
471
578
  */
472
- async function buildPresetRegistry(options) {
579
+ async function buildRegistry(options) {
473
580
  const bundleBase = normalizeBundleBase(options.bundleBase);
474
- const items = [];
581
+ const rules = [];
475
582
  const bundles = [];
476
- for (const presetInput of options.presets) {
477
- const publishInput = await buildPresetPublishInput({ preset: presetInput });
583
+ for (const ruleInput of options.rules) {
584
+ const publishInput = await buildPublishInput({ rule: ruleInput });
478
585
  const slug = publishInput.name;
479
586
  const version = `${publishInput.version ?? 1}.0`;
480
- const presetVariants = publishInput.variants.map((v) => ({
587
+ const ruleVariants = publishInput.variants.map((v) => ({
481
588
  platform: v.platform,
482
589
  bundleUrl: getBundlePath(bundleBase, slug, v.platform, version),
483
590
  fileCount: v.files.length,
484
- totalSize: v.files.reduce((sum, f) => sum + f.content.length, 0)
591
+ totalSize: v.files.reduce((sum, f) => sum + f.size, 0)
485
592
  }));
486
- presetVariants.sort((a, b) => a.platform.localeCompare(b.platform));
487
- const item = {
488
- kind: "preset",
593
+ ruleVariants.sort((a, b) => a.platform.localeCompare(b.platform));
594
+ const rule = {
489
595
  slug,
490
- name: publishInput.title,
596
+ name: publishInput.name,
597
+ ...publishInput.type && { type: publishInput.type },
491
598
  title: publishInput.title,
492
599
  description: publishInput.description,
493
600
  tags: publishInput.tags,
@@ -496,13 +603,14 @@ async function buildPresetRegistry(options) {
496
603
  versions: [{
497
604
  version,
498
605
  isLatest: true,
499
- variants: presetVariants
606
+ variants: ruleVariants
500
607
  }]
501
608
  };
502
- items.push(item);
609
+ rules.push(rule);
503
610
  for (const variant of publishInput.variants) {
504
611
  const bundle = {
505
612
  name: publishInput.name,
613
+ ...publishInput.type && { type: publishInput.type },
506
614
  slug,
507
615
  platform: variant.platform,
508
616
  title: publishInput.title,
@@ -519,13 +627,13 @@ async function buildPresetRegistry(options) {
519
627
  bundles.push(bundle);
520
628
  }
521
629
  }
522
- items.sort((a, b) => a.slug.localeCompare(b.slug));
630
+ rules.sort((a, b) => a.slug.localeCompare(b.slug));
523
631
  bundles.sort((a, b) => {
524
632
  if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
525
633
  return a.slug.localeCompare(b.slug);
526
634
  });
527
635
  return {
528
- items,
636
+ rules,
529
637
  bundles
530
638
  };
531
639
  }
@@ -550,7 +658,10 @@ function normalizeFilePayload(content) {
550
658
  return new Uint8Array(content);
551
659
  }
552
660
  function encodeFilePayload(data, filePath) {
553
- const decoder = new TextDecoder("utf-8", { fatal: true });
661
+ const decoder = new TextDecoder("utf-8", {
662
+ fatal: true,
663
+ ignoreBOM: false
664
+ });
554
665
  try {
555
666
  return decoder.decode(data);
556
667
  } catch {
@@ -602,40 +713,34 @@ async function sha256Hex(payload) {
602
713
  //#endregion
603
714
  //#region src/constants.ts
604
715
  /**
605
- * Shared constants for agentrules presets and registry.
716
+ * Shared constants for agentrules.
606
717
  */
607
- /** Filename for preset configuration */
608
- const PRESET_CONFIG_FILENAME = "agentrules.json";
609
- /** Directory name for preset metadata (README, LICENSE, etc.) */
610
- const AGENT_RULES_DIR = ".agentrules";
611
- /** JSON Schema URL for preset configuration */
612
- const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
718
+ /** Filename for rule configuration */
719
+ const RULE_CONFIG_FILENAME = "agentrules.json";
720
+ /** JSON Schema URL for rule configuration */
721
+ const RULE_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
613
722
  /** API root path segment */
614
723
  const API_PATH = "api";
615
- /** Default version identifier for latest preset version */
724
+ /** Default version identifier for latest rule version */
616
725
  const LATEST_VERSION = "latest";
617
726
  /**
618
727
  * API endpoint paths (relative to registry base URL).
619
728
  *
620
729
  * Note on slug handling:
621
- * - Slugs may contain slashes (e.g., "username/my-preset") which flow through as path segments
730
+ * - Slugs may contain slashes (e.g., "username/my-rule") which flow through as path segments
622
731
  * - The client is responsible for validating values before making requests
623
732
  */
624
733
  const API_ENDPOINTS = {
625
- presets: {
626
- base: `${API_PATH}/presets`,
627
- unpublish: (slug, version) => `${API_PATH}/presets/${slug}/${version}`
734
+ rules: {
735
+ base: `${API_PATH}/rules`,
736
+ get: (slug) => `${API_PATH}/rules/${slug}`,
737
+ unpublish: (slug, version) => `${API_PATH}/rules/${slug}/${version}`
628
738
  },
629
739
  auth: {
630
740
  session: `${API_PATH}/auth/get-session`,
631
741
  deviceCode: `${API_PATH}/auth/device/code`,
632
742
  deviceToken: `${API_PATH}/auth/device/token`
633
- },
634
- rules: {
635
- base: `${API_PATH}/rules`,
636
- bySlug: (slug) => `${API_PATH}/rules/${slug}`
637
- },
638
- items: { get: (slug) => `${API_PATH}/items/${slug}` }
743
+ }
639
744
  };
640
745
 
641
746
  //#endregion
@@ -656,13 +761,13 @@ async function fetchBundle(bundleUrl) {
656
761
  * Resolves a slug to get all versions and platform variants.
657
762
  *
658
763
  * @param baseUrl - Registry base URL
659
- * @param slug - Content slug (may contain slashes, e.g., "username/my-preset")
764
+ * @param slug - Content slug (may contain slashes, e.g., "username/my-rule")
660
765
  * @param version - Optional version filter (server may ignore for static registries)
661
766
  * @returns Resolved data, or null if not found
662
767
  * @throws Error on network/server errors
663
768
  */
664
769
  async function resolveSlug(baseUrl, slug, version) {
665
- const url = new URL(API_ENDPOINTS.items.get(slug), baseUrl);
770
+ const url = new URL(API_ENDPOINTS.rules.get(slug), baseUrl);
666
771
  if (version) url.searchParams.set("version", version);
667
772
  let response;
668
773
  try {
@@ -680,47 +785,33 @@ async function resolveSlug(baseUrl, slug, version) {
680
785
  throw new Error(errorMessage);
681
786
  }
682
787
  const data = await response.json();
683
- if (data.kind === "preset") {
684
- for (const ver of data.versions) for (const variant of ver.variants) if ("bundleUrl" in variant) variant.bundleUrl = new URL(variant.bundleUrl, baseUrl).toString();
685
- }
788
+ for (const ver of data.versions) for (const variant of ver.variants) if ("bundleUrl" in variant) variant.bundleUrl = new URL(variant.bundleUrl, baseUrl).toString();
686
789
  return data;
687
790
  }
688
791
 
689
792
  //#endregion
690
793
  //#region src/resolve/schema.ts
691
794
  const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
692
- const presetVariantBundleSchema = z.object({
795
+ const ruleVariantBundleSchema = z.object({
693
796
  platform: platformIdSchema,
694
797
  bundleUrl: z.string().min(1),
695
798
  fileCount: z.number().int().nonnegative(),
696
799
  totalSize: z.number().int().nonnegative()
697
800
  });
698
- const presetVariantInlineSchema = z.object({
801
+ const ruleVariantInlineSchema = z.object({
699
802
  platform: platformIdSchema,
700
803
  content: z.string().min(1),
701
804
  fileCount: z.number().int().nonnegative(),
702
805
  totalSize: z.number().int().nonnegative()
703
806
  });
704
- const presetVariantSchema = z.union([presetVariantBundleSchema, presetVariantInlineSchema]);
705
- const ruleVariantSchema = z.object({
706
- platform: platformIdSchema,
707
- type: z.string().min(1),
708
- content: z.string().min(1)
709
- });
710
- const presetVersionSchema = z.object({
711
- version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
712
- isLatest: z.boolean(),
713
- publishedAt: z.string().datetime().optional(),
714
- variants: z.array(presetVariantSchema).min(1)
715
- });
807
+ const ruleVariantSchema = z.union([ruleVariantBundleSchema, ruleVariantInlineSchema]);
716
808
  const ruleVersionSchema = z.object({
717
809
  version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
718
810
  isLatest: z.boolean(),
719
811
  publishedAt: z.string().datetime().optional(),
720
812
  variants: z.array(ruleVariantSchema).min(1)
721
813
  });
722
- const resolvedPresetSchema = z.object({
723
- kind: z.literal("preset"),
814
+ const resolvedRuleSchema = z.object({
724
815
  slug: z.string().min(1),
725
816
  name: z.string().min(1),
726
817
  title: z.string().min(1),
@@ -728,157 +819,55 @@ const resolvedPresetSchema = z.object({
728
819
  tags: z.array(z.string()),
729
820
  license: z.string(),
730
821
  features: z.array(z.string()),
731
- versions: z.array(presetVersionSchema).min(1)
732
- });
733
- const resolvedRuleSchema = z.object({
734
- kind: z.literal("rule"),
735
- slug: z.string().min(1),
736
- name: z.string().min(1),
737
- title: z.string().min(1),
738
- description: z.string(),
739
- tags: z.array(z.string()),
740
822
  versions: z.array(ruleVersionSchema).min(1)
741
823
  });
742
- const resolveResponseSchema = z.discriminatedUnion("kind", [resolvedPresetSchema, resolvedRuleSchema]);
824
+ const resolveResponseSchema = resolvedRuleSchema;
743
825
 
744
826
  //#endregion
745
827
  //#region src/resolve/utils.ts
746
828
  /**
747
- * Type guard for preset
748
- */
749
- function isPreset(item) {
750
- return item.kind === "preset";
751
- }
752
- /**
753
- * Type guard for rule
754
- */
755
- function isRule(item) {
756
- return item.kind === "rule";
757
- }
758
- /**
759
- * Type guard for preset variant with bundleUrl
829
+ * Type guard for rule variant with bundleUrl
760
830
  */
761
831
  function hasBundle(variant) {
762
832
  return "bundleUrl" in variant;
763
833
  }
764
834
  /**
765
- * Type guard for preset variant with inline content
835
+ * Type guard for rule variant with inline content
766
836
  */
767
837
  function hasInlineContent(variant) {
768
838
  return "content" in variant;
769
839
  }
770
840
  /**
771
- * Get the latest version from a resolved preset
772
- */
773
- function getLatestPresetVersion(item) {
774
- return item.versions.find((v) => v.isLatest);
775
- }
776
- /**
777
841
  * Get the latest version from a resolved rule
778
842
  */
779
- function getLatestRuleVersion(item) {
843
+ function getLatestVersion(item) {
780
844
  return item.versions.find((v) => v.isLatest);
781
845
  }
782
846
  /**
783
- * Get a specific version from a resolved preset
784
- */
785
- function getPresetVersion(item, version) {
786
- return item.versions.find((v) => v.version === version);
787
- }
788
- /**
789
847
  * Get a specific version from a resolved rule
790
848
  */
791
- function getRuleVersion(item, version) {
849
+ function getVersion(item, version) {
792
850
  return item.versions.find((v) => v.version === version);
793
851
  }
794
852
  /**
795
- * Get a specific platform variant from a preset version
796
- */
797
- function getPresetVariant(version, platform) {
798
- return version.variants.find((v) => v.platform === platform);
799
- }
800
- /**
801
853
  * Get a specific platform variant from a rule version
802
854
  */
803
- function getRuleVariant(version, platform) {
855
+ function getVariant(version, platform) {
804
856
  return version.variants.find((v) => v.platform === platform);
805
857
  }
806
858
  /**
807
- * Get all available platforms for a preset version
808
- */
809
- function getPresetPlatforms(version) {
810
- return version.variants.map((v) => v.platform);
811
- }
812
- /**
813
859
  * Get all available platforms for a rule version
814
860
  */
815
- function getRulePlatforms(version) {
861
+ function getPlatforms(version) {
816
862
  return version.variants.map((v) => v.platform);
817
863
  }
818
864
  /**
819
- * Check if a platform is available in any version of a preset
820
- */
821
- function presetHasPlatform(item, platform) {
822
- return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
823
- }
824
- /**
825
865
  * Check if a platform is available in any version of a rule
826
866
  */
827
- function ruleHasPlatform(item, platform) {
867
+ function hasPlatform(item, platform) {
828
868
  return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
829
869
  }
830
870
 
831
- //#endregion
832
- //#region src/rule/schema.ts
833
- /**
834
- * Rule-specific schema aliases.
835
- * All use shared schemas for consistency with presets:
836
- * - name: max 64 chars, lowercase kebab-case
837
- * - title: max 80 chars
838
- * - description: max 500 chars (optional for rules)
839
- * - tags: max 35 chars each, 1-10 required, platform names blocked
840
- */
841
- const ruleNameSchema = nameSchema;
842
- const ruleTitleSchema = titleSchema;
843
- const ruleDescriptionSchema = descriptionSchema;
844
- const ruleTagSchema = tagSchema;
845
- const ruleTagsSchema = tagsSchema;
846
- const rulePlatformSchema = z.enum(PLATFORM_IDS);
847
- const ruleTypeSchema = z.string().trim().min(1).max(32);
848
- const ruleContentSchema = z.string().min(1, "Content is required").max(1e5, "Content must be 100KB or less");
849
- /** Common fields shared across all platform-type combinations */
850
- const ruleCommonFields = {
851
- name: ruleNameSchema,
852
- title: ruleTitleSchema,
853
- description: ruleDescriptionSchema.optional(),
854
- content: ruleContentSchema,
855
- tags: ruleTagsSchema
856
- };
857
- /**
858
- * Discriminated union schema for platform + type combinations.
859
- * Each platform has its own set of valid types.
860
- */
861
- const rulePlatformTypeSchema = z.discriminatedUnion("platform", [
862
- z.object({
863
- platform: z.literal("opencode"),
864
- type: z.enum(PLATFORM_RULE_TYPES.opencode)
865
- }),
866
- z.object({
867
- platform: z.literal("claude"),
868
- type: z.enum(PLATFORM_RULE_TYPES.claude)
869
- }),
870
- z.object({
871
- platform: z.literal("cursor"),
872
- type: z.enum(PLATFORM_RULE_TYPES.cursor)
873
- }),
874
- z.object({
875
- platform: z.literal("codex"),
876
- type: z.enum(PLATFORM_RULE_TYPES.codex)
877
- })
878
- ]);
879
- /** Schema for rule creation with discriminated union for platform+type */
880
- const ruleCreateInputSchema = z.object(ruleCommonFields).and(rulePlatformTypeSchema);
881
-
882
871
  //#endregion
883
872
  //#region src/utils/diff.ts
884
873
  const DEFAULT_CONTEXT = 2;
@@ -903,4 +892,4 @@ function normalizeBundlePath(value) {
903
892
  }
904
893
 
905
894
  //#endregion
906
- export { AGENT_RULES_DIR, API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PLATFORM_ID_TUPLE, PLATFORM_RULE_TYPES, PRESET_CONFIG_FILENAME, PRESET_SCHEMA_URL, STATIC_BUNDLE_DIR, buildPresetPublishInput, buildPresetRegistry, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeUtf8, fetchBundle, getInstallPath, getLatestPresetVersion, getLatestRuleVersion, getPlatformConfig, getPlatformFromDir, getPresetPlatforms, getPresetVariant, getPresetVersion, getRulePlatforms, getRuleTypeConfig, getRuleVariant, getRuleVersion, getValidRuleTypes, hasBundle, hasInlineContent, isLikelyText, isPlatformDir, isPreset, isRule, isSupportedPlatform, isValidRuleType, licenseSchema, nameSchema, normalizeBundlePath, normalizePlatformEntry, normalizePlatformInput, platformIdSchema, presetBundleSchema, presetConfigSchema, presetHasPlatform, presetPublishInputSchema, presetVariantSchema, presetVersionSchema, publishVariantInputSchema, requiredDescriptionSchema, resolveResponseSchema, resolveSlug, resolvedPresetSchema, resolvedRuleSchema, ruleContentSchema, ruleCreateInputSchema, ruleDescriptionSchema, ruleHasPlatform, ruleNameSchema, rulePlatformSchema, rulePlatformTypeSchema, ruleTagSchema, ruleTagsSchema, ruleTitleSchema, ruleTypeSchema, ruleVariantSchema, ruleVersionSchema, tagSchema, tagsSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
895
+ export { API_ENDPOINTS, COMMON_LICENSES, LATEST_VERSION, PLATFORMS, PLATFORM_IDS, PLATFORM_ID_TUPLE, PLATFORM_RULE_TYPES, RULE_CONFIG_FILENAME, RULE_SCHEMA_URL, RULE_TYPE_TUPLE, STATIC_BUNDLE_DIR, buildPublishInput, buildRegistry, bundledFileSchema, cleanInstallMessage, createDiffPreview, decodeBundledFile, decodeUtf8, descriptionSchema, encodeUtf8, fetchBundle, getInstallPath, getLatestVersion, getPlatformConfig, getPlatformFromDir, getPlatforms, getTypeConfig, getValidTypes, getVariant, getVersion, hasBundle, hasInlineContent, hasPlatform, inferInstructionPlatformsFromFileName, inferPlatformFromPath, inferTypeFromPath, isLikelyText, isPlatformDir, isSupportedPlatform, isValidType, licenseSchema, nameSchema, normalizeBundlePath, normalizePlatformEntry, normalizePlatformInput, platformIdSchema, publishVariantInputSchema, resolveResponseSchema, resolveSlug, resolvedRuleSchema, ruleBundleSchema, ruleConfigSchema, rulePublishInputSchema, ruleTypeSchema, ruleVariantSchema, ruleVersionSchema, supportsInstallPath, tagSchema, tagsSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validateConfig, validateRule, verifyBundledFileChecksum };