@agentrules/core 0.0.11 → 0.1.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.
package/dist/index.js CHANGED
@@ -1,49 +1,6 @@
1
1
  import { ZodError, z } from "zod";
2
2
  import { createTwoFilesPatch } from "diff";
3
3
 
4
- //#region src/constants.ts
5
- /**
6
- * Shared constants for agentrules presets and registry.
7
- */
8
- /** Filename for preset configuration */
9
- const PRESET_CONFIG_FILENAME = "agentrules.json";
10
- /** Directory name for preset metadata (README, LICENSE, etc.) */
11
- const AGENT_RULES_DIR = ".agentrules";
12
- /** JSON Schema URL for preset configuration */
13
- const PRESET_SCHEMA_URL = "https://agentrules.directory/schema/agentrules.json";
14
- /** API root path segment */
15
- const API_PATH = "api";
16
- /** Default version identifier for latest preset version */
17
- const LATEST_VERSION = "latest";
18
- /**
19
- * API endpoint paths (relative to registry base URL).
20
- *
21
- * Note: Path parameters (slug, platform, version) are NOT URI-encoded.
22
- * - Slugs may contain slashes (e.g., "username/my-preset") which should flow
23
- * through as path segments for static registry compatibility
24
- * - Platform and version are constrained values (enums, validated formats)
25
- * that only contain URL-safe characters
26
- *
27
- * The client is responsible for validating these values before making requests.
28
- */
29
- const API_ENDPOINTS = {
30
- presets: {
31
- base: `${API_PATH}/preset`,
32
- get: (slug, platform, version = LATEST_VERSION) => `${API_PATH}/preset/${slug}/${platform}/${version}`,
33
- unpublish: (slug, platform, version) => `${API_PATH}/preset/${slug}/${platform}/${version}`
34
- },
35
- auth: {
36
- session: `${API_PATH}/auth/get-session`,
37
- deviceCode: `${API_PATH}/auth/device/code`,
38
- deviceToken: `${API_PATH}/auth/device/token`
39
- },
40
- rule: {
41
- base: `${API_PATH}/rule`,
42
- get: (slug) => `${API_PATH}/rule/${slug}`
43
- }
44
- };
45
-
46
- //#endregion
47
4
  //#region src/platform/types.ts
48
5
  /**
49
6
  * Platform and rule type definitions.
@@ -68,14 +25,14 @@ const PLATFORMS = {
68
25
  globalDir: "~/.config/opencode",
69
26
  types: {
70
27
  instruction: {
71
- description: "Project instructions (AGENTS.md)",
28
+ description: "Project instructions",
72
29
  format: "markdown",
73
30
  extension: "md",
74
31
  projectPath: "AGENTS.md",
75
32
  globalPath: "~/.config/opencode/AGENTS.md"
76
33
  },
77
34
  agent: {
78
- description: "Specialized AI agent definition",
35
+ description: "Specialized AI agent",
79
36
  format: "markdown",
80
37
  extension: "md",
81
38
  projectPath: ".opencode/agent/{name}.md",
@@ -89,7 +46,7 @@ const PLATFORMS = {
89
46
  globalPath: "~/.config/opencode/command/{name}.md"
90
47
  },
91
48
  tool: {
92
- description: "Custom tool definition",
49
+ description: "Custom tool",
93
50
  format: "typescript",
94
51
  extension: "ts",
95
52
  projectPath: ".opencode/tool/{name}.ts",
@@ -103,7 +60,7 @@ const PLATFORMS = {
103
60
  globalDir: "~/.claude",
104
61
  types: {
105
62
  instruction: {
106
- description: "Project instructions (CLAUDE.md)",
63
+ description: "Project instructions",
107
64
  format: "markdown",
108
65
  extension: "md",
109
66
  projectPath: "CLAUDE.md",
@@ -117,7 +74,7 @@ const PLATFORMS = {
117
74
  globalPath: "~/.claude/commands/{name}.md"
118
75
  },
119
76
  skill: {
120
- description: "Custom skill definition",
77
+ description: "Custom skill",
121
78
  format: "markdown",
122
79
  extension: "md",
123
80
  projectPath: ".claude/skills/{name}/SKILL.md",
@@ -128,29 +85,45 @@ const PLATFORMS = {
128
85
  cursor: {
129
86
  label: "Cursor",
130
87
  projectDir: ".cursor",
131
- globalDir: null,
132
- types: { rule: {
133
- description: "Project rule (MDC format)",
134
- format: "mdc",
135
- extension: "mdc",
136
- projectPath: ".cursor/rules/{name}.mdc",
137
- globalPath: null
138
- } }
88
+ globalDir: "~/.cursor",
89
+ types: {
90
+ instruction: {
91
+ description: "Project instructions",
92
+ format: "markdown",
93
+ extension: "md",
94
+ projectPath: "AGENTS.md",
95
+ globalPath: null
96
+ },
97
+ rule: {
98
+ description: "Custom rule",
99
+ format: "mdc",
100
+ extension: "mdc",
101
+ projectPath: ".cursor/rules/{name}.mdc",
102
+ globalPath: null
103
+ },
104
+ command: {
105
+ description: "Custom slash command",
106
+ format: "markdown",
107
+ extension: "md",
108
+ projectPath: ".cursor/commands/{name}.md",
109
+ globalPath: "~/.cursor/commands/{name}.md"
110
+ }
111
+ }
139
112
  },
140
113
  codex: {
141
114
  label: "Codex",
142
- projectDir: "",
115
+ projectDir: ".codex",
143
116
  globalDir: "~/.codex",
144
117
  types: {
145
118
  instruction: {
146
- description: "Project instructions (AGENTS.md)",
119
+ description: "Project instructions",
147
120
  format: "markdown",
148
121
  extension: "md",
149
122
  projectPath: "AGENTS.md",
150
123
  globalPath: "~/.codex/AGENTS.md"
151
124
  },
152
125
  command: {
153
- description: "Custom prompt (global only)",
126
+ description: "Custom prompt",
154
127
  format: "markdown",
155
128
  extension: "md",
156
129
  projectPath: null,
@@ -163,8 +136,8 @@ const PLATFORMS = {
163
136
  const PLATFORM_RULE_TYPES = {
164
137
  opencode: [
165
138
  "instruction",
166
- "agent",
167
139
  "command",
140
+ "agent",
168
141
  "tool"
169
142
  ],
170
143
  claude: [
@@ -172,8 +145,12 @@ const PLATFORM_RULE_TYPES = {
172
145
  "command",
173
146
  "skill"
174
147
  ],
175
- cursor: ["rule"],
176
- codex: ["instruction", "command"]
148
+ codex: ["instruction", "command"],
149
+ cursor: [
150
+ "instruction",
151
+ "command",
152
+ "rule"
153
+ ]
177
154
  };
178
155
  /** Get valid rule types for a specific platform */
179
156
  function getValidRuleTypes(platform) {
@@ -247,25 +224,56 @@ function toUint8Array(payload) {
247
224
  }
248
225
 
249
226
  //#endregion
250
- //#region src/preset/schema.ts
227
+ //#region src/schemas/common.ts
251
228
  const PLATFORM_ID_SET = new Set(PLATFORM_IDS);
252
- const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
253
- const platformIdSchema = z.enum(PLATFORM_IDS);
254
- const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title must be 80 characters or less");
255
- const descriptionSchema = z.string().trim().min(1, "Description is required").max(500, "Description must be 500 characters or less");
256
- const versionSchema = z.string().trim().regex(VERSION_REGEX, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
257
- const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
258
229
  const TAG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
259
230
  const TAG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-tag)";
231
+ /**
232
+ * Schema for a single tag.
233
+ * - Max 35 characters
234
+ * - Lowercase alphanumeric with hyphens
235
+ * - Platform names blocked (redundant with platform field)
236
+ */
260
237
  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
+ /**
239
+ * Schema for tags array.
240
+ * - 1-10 tags required
241
+ */
261
242
  const tagsSchema = z.array(tagSchema).min(1, "At least one tag is required").max(10, "Maximum 10 tags allowed");
243
+ const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
244
+ const NAME_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
245
+ /**
246
+ * Schema for preset/rule name.
247
+ * - Max 64 characters
248
+ * - Lowercase alphanumeric with hyphens
249
+ */
250
+ const nameSchema = z.string().trim().min(1, "Name is required").max(64, "Name must be 64 characters or less").regex(NAME_REGEX, NAME_ERROR);
251
+ /**
252
+ * Schema for display title.
253
+ * - Max 80 characters
254
+ */
255
+ const titleSchema = z.string().trim().min(1, "Title is required").max(80, "Title must be 80 characters or less");
256
+ /**
257
+ * Schema for description.
258
+ * - Max 500 characters
259
+ */
260
+ const descriptionSchema = z.string().trim().max(500, "Description must be 500 characters or less");
261
+
262
+ //#endregion
263
+ //#region src/preset/schema.ts
264
+ const VERSION_REGEX$1 = /^[1-9]\d*\.\d+$/;
265
+ 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");
271
+ const versionSchema = z.string().trim().regex(VERSION_REGEX$1, "Version must be in MAJOR.MINOR format (e.g., 1.3)");
272
+ const majorVersionSchema = z.number().int().positive("Major version must be a positive integer");
262
273
  const featureSchema = z.string().trim().min(1, "Feature cannot be empty").max(100, "Feature must be 100 characters or less");
263
274
  const featuresSchema = z.array(featureSchema).max(5, "Maximum 5 features allowed");
264
275
  const installMessageSchema = z.string().trim().max(2e3, "Install message must be 2000 characters or less");
265
276
  const contentSchema = z.string();
266
- const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
267
- const SLUG_ERROR = "Must be lowercase alphanumeric with hyphens (e.g., my-preset)";
268
- const slugSchema = z.string().trim().min(1, "Name is required").max(64, "Name must be 64 characters or less").regex(SLUG_REGEX, SLUG_ERROR);
269
277
  const COMMON_LICENSES = [
270
278
  "MIT",
271
279
  "Apache-2.0",
@@ -278,44 +286,78 @@ const licenseSchema = z.string().trim().min(1, "License is required").max(128, "
278
286
  const pathSchema = z.string().trim().min(1);
279
287
  const ignorePatternSchema = z.string().trim().min(1, "Ignore pattern cannot be empty");
280
288
  const ignoreSchema = z.array(ignorePatternSchema).max(50, "Maximum 50 ignore patterns allowed");
289
+ /**
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
+ * Platform entry - either a platform ID string or an object with optional path.
297
+ *
298
+ * Examples:
299
+ * - "opencode" (shorthand, uses default directory)
300
+ * - { platform: "opencode", path: "rules" } (custom path)
301
+ */
302
+ const platformEntryObjectSchema = z.object({
303
+ platform: platformIdSchema,
304
+ path: pathSchema.optional()
305
+ }).strict();
306
+ const platformEntrySchema = z.union([platformIdSchema, platformEntryObjectSchema]);
307
+ /**
308
+ * Preset config schema.
309
+ *
310
+ * Uses a unified `platforms` array that accepts either:
311
+ * - Platform ID strings: `["opencode", "claude"]`
312
+ * - Objects with optional path: `[{ platform: "opencode", path: "rules" }]`
313
+ * - Mixed: `["opencode", { platform: "claude", path: "my-claude" }]`
314
+ */
281
315
  const presetConfigSchema = z.object({
282
316
  $schema: z.string().optional(),
283
- name: slugSchema,
317
+ name: nameSchema,
284
318
  title: titleSchema,
285
319
  version: majorVersionSchema.optional(),
286
- description: descriptionSchema,
320
+ description: requiredDescriptionSchema,
287
321
  tags: tagsSchema,
288
322
  features: featuresSchema.optional(),
289
323
  license: licenseSchema,
290
- platform: platformIdSchema,
291
- path: pathSchema.optional(),
292
- ignore: ignoreSchema.optional()
324
+ ignore: ignoreSchema.optional(),
325
+ agentrulesDir: agentrulesPathSchema.optional(),
326
+ platforms: z.array(platformEntrySchema).min(1, "At least one platform is required")
293
327
  }).strict();
294
328
  const bundledFileSchema = z.object({
295
329
  path: z.string().min(1),
296
330
  size: z.number().int().nonnegative(),
297
331
  checksum: z.string().length(64),
298
- contents: z.string()
332
+ content: z.string()
333
+ });
334
+ /**
335
+ * Schema for per-platform variant in publish input.
336
+ */
337
+ const publishVariantInputSchema = z.object({
338
+ platform: platformIdSchema,
339
+ files: z.array(bundledFileSchema).min(1),
340
+ readmeContent: contentSchema.optional(),
341
+ licenseContent: contentSchema.optional(),
342
+ installMessage: installMessageSchema.optional()
299
343
  });
300
344
  /**
301
- * Schema for what clients send to publish a preset.
345
+ * Schema for what clients send to publish a preset (multi-platform).
346
+ *
347
+ * One publish call creates ONE version with ALL platform variants.
302
348
  * Version is optional major version. Registry assigns full MAJOR.MINOR.
303
349
  *
304
350
  * Note: Clients send `name` (e.g., "my-preset"), and the registry defines the format of the slug.
305
351
  * For example, a namespaced slug could be returned as "username/my-preset"
306
352
  */
307
353
  const presetPublishInputSchema = z.object({
308
- name: slugSchema,
309
- platform: platformIdSchema,
354
+ name: nameSchema,
310
355
  title: titleSchema,
311
- description: descriptionSchema,
356
+ description: requiredDescriptionSchema,
312
357
  tags: tagsSchema,
313
358
  license: licenseSchema,
314
- licenseContent: contentSchema.optional(),
315
- readmeContent: contentSchema.optional(),
316
359
  features: featuresSchema.optional(),
317
- installMessage: installMessageSchema.optional(),
318
- files: z.array(bundledFileSchema).min(1),
360
+ variants: z.array(publishVariantInputSchema).min(1, "At least one platform variant is required"),
319
361
  version: majorVersionSchema.optional()
320
362
  });
321
363
  /**
@@ -329,18 +371,16 @@ const presetBundleSchema = presetPublishInputSchema.omit({
329
371
  slug: z.string().trim().min(1),
330
372
  version: versionSchema
331
373
  });
332
- const presetSchema = presetBundleSchema.omit({
333
- files: true,
334
- readmeContent: true,
335
- licenseContent: true,
336
- installMessage: true
337
- }).extend({
338
- name: z.string().trim().min(1),
339
- bundleUrl: z.string().trim().min(1),
340
- fileCount: z.number().int().nonnegative(),
341
- totalSize: z.number().int().nonnegative()
342
- });
343
- const presetIndexSchema = z.record(z.string(), presetSchema);
374
+
375
+ //#endregion
376
+ //#region src/preset/types.ts
377
+ /**
378
+ * Normalize a raw platform entry to the object form.
379
+ */
380
+ function normalizePlatformEntry(entry) {
381
+ if (typeof entry === "string") return { platform: entry };
382
+ return entry;
383
+ }
344
384
 
345
385
  //#endregion
346
386
  //#region src/builder/utils.ts
@@ -349,9 +389,10 @@ function cleanInstallMessage(value) {
349
389
  const trimmed = value.trim();
350
390
  return trimmed.length > 0 ? trimmed : void 0;
351
391
  }
352
- function encodeItemName(slug, platform) {
353
- return `${slug}.${platform}`;
354
- }
392
+ /**
393
+ * Validate raw preset config from JSON.
394
+ * Returns the raw config shape (before normalization).
395
+ */
355
396
  function validatePresetConfig(config, slug) {
356
397
  try {
357
398
  return presetConfigSchema.parse(config);
@@ -385,122 +426,128 @@ async function sha256(data) {
385
426
  }
386
427
  /**
387
428
  * Builds a PresetPublishInput from preset input.
388
- * Used by CLI to prepare data for publishing to a registry.
429
+ *
430
+ * PresetInput always has platformFiles array (unified format).
389
431
  */
390
432
  async function buildPresetPublishInput(options) {
391
- const { preset: presetInput, version } = options;
392
- if (!NAME_PATTERN.test(presetInput.name)) throw new Error(`Invalid name "${presetInput.name}". Names must be lowercase kebab-case.`);
393
- const presetConfig = validatePresetConfig(presetInput.config, presetInput.name);
394
- const platform = presetConfig.platform;
395
- ensureKnownPlatform(platform, presetInput.name);
396
- if (presetInput.files.length === 0) throw new Error(`Preset ${presetInput.name} does not include any files.`);
397
- const files = await createBundledFilesFromInputs(presetInput.files);
398
- const installMessage = cleanInstallMessage(presetInput.installMessage);
399
- const features = presetConfig.features ?? [];
400
- const readmeContent = presetInput.readmeContent?.trim() || void 0;
401
- const licenseContent = presetInput.licenseContent?.trim() || void 0;
402
- const majorVersion = version ?? presetConfig.version;
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);
436
+ 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);
440
+ const variants = [];
441
+ 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}".`);
445
+ const files = await createBundledFilesFromInputs(platformData.files);
446
+ variants.push({
447
+ platform: entry.platform,
448
+ 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)
452
+ });
453
+ }
403
454
  return {
404
- name: presetInput.name,
405
- platform,
406
- title: presetConfig.title,
407
- description: presetConfig.description,
408
- tags: presetConfig.tags ?? [],
409
- license: presetConfig.license,
410
- licenseContent,
411
- readmeContent,
412
- features,
413
- installMessage,
414
- files,
455
+ name: preset.name,
456
+ title: config.title,
457
+ description: config.description,
458
+ tags: config.tags ?? [],
459
+ license: config.license,
460
+ features: config.features ?? [],
461
+ variants,
415
462
  ...majorVersion !== void 0 && { version: majorVersion }
416
463
  };
417
464
  }
418
465
  /**
419
- * Builds a static registry with entries, index, and bundles.
420
- * Used for building static registry files (e.g., community-presets).
421
- * Each preset uses its version from config (default: major 1, minor 0).
466
+ * Builds a static registry with items and bundles.
467
+ *
468
+ * Uses the same model as dynamic publishing:
469
+ * - Each PresetInput (single or multi-platform) becomes one item
470
+ * - Each platform variant becomes one bundle
422
471
  */
423
472
  async function buildPresetRegistry(options) {
424
473
  const bundleBase = normalizeBundleBase(options.bundleBase);
425
- const entries = [];
474
+ const items = [];
426
475
  const bundles = [];
427
476
  for (const presetInput of options.presets) {
428
- if (!NAME_PATTERN.test(presetInput.name)) throw new Error(`Invalid name "${presetInput.name}". Names must be lowercase kebab-case.`);
429
- const presetConfig = validatePresetConfig(presetInput.config, presetInput.name);
430
- const platform = presetConfig.platform;
431
- ensureKnownPlatform(platform, presetInput.name);
432
- if (presetInput.files.length === 0) throw new Error(`Preset ${presetInput.name} does not include any files.`);
433
- const files = await createBundledFilesFromInputs(presetInput.files);
434
- const totalSize = files.reduce((sum, file) => sum + file.size, 0);
435
- const installMessage = cleanInstallMessage(presetInput.installMessage);
436
- const features = presetConfig.features ?? [];
437
- const readmeContent = presetInput.readmeContent?.trim() || void 0;
438
- const licenseContent = presetInput.licenseContent?.trim() || void 0;
439
- const majorVersion = presetConfig.version ?? 1;
440
- const version = `${majorVersion}.0`;
441
- const slug = presetInput.name;
442
- const entry = {
443
- name: encodeItemName(slug, platform),
444
- slug,
445
- platform,
446
- title: presetConfig.title,
447
- version,
448
- description: presetConfig.description,
449
- tags: presetConfig.tags ?? [],
450
- license: presetConfig.license,
451
- features,
452
- bundleUrl: getBundlePath(bundleBase, slug, platform, version),
453
- fileCount: files.length,
454
- totalSize
455
- };
456
- entries.push(entry);
457
- const bundle = {
477
+ const publishInput = await buildPresetPublishInput({ preset: presetInput });
478
+ const slug = publishInput.name;
479
+ const version = `${publishInput.version ?? 1}.0`;
480
+ const presetVariants = publishInput.variants.map((v) => ({
481
+ platform: v.platform,
482
+ bundleUrl: getBundlePath(bundleBase, slug, v.platform, version),
483
+ fileCount: v.files.length,
484
+ totalSize: v.files.reduce((sum, f) => sum + f.content.length, 0)
485
+ }));
486
+ presetVariants.sort((a, b) => a.platform.localeCompare(b.platform));
487
+ const item = {
488
+ kind: "preset",
458
489
  slug,
459
- platform,
460
- title: presetConfig.title,
461
- version,
462
- description: presetConfig.description,
463
- tags: presetConfig.tags ?? [],
464
- license: presetConfig.license,
465
- licenseContent,
466
- readmeContent,
467
- features,
468
- installMessage,
469
- files
490
+ name: publishInput.title,
491
+ title: publishInput.title,
492
+ description: publishInput.description,
493
+ tags: publishInput.tags,
494
+ license: publishInput.license,
495
+ features: publishInput.features ?? [],
496
+ versions: [{
497
+ version,
498
+ isLatest: true,
499
+ variants: presetVariants
500
+ }]
470
501
  };
471
- bundles.push(bundle);
502
+ items.push(item);
503
+ for (const variant of publishInput.variants) {
504
+ const bundle = {
505
+ name: publishInput.name,
506
+ slug,
507
+ platform: variant.platform,
508
+ title: publishInput.title,
509
+ version,
510
+ description: publishInput.description,
511
+ tags: publishInput.tags,
512
+ license: publishInput.license,
513
+ features: publishInput.features,
514
+ files: variant.files,
515
+ readmeContent: variant.readmeContent,
516
+ licenseContent: variant.licenseContent,
517
+ installMessage: variant.installMessage
518
+ };
519
+ bundles.push(bundle);
520
+ }
472
521
  }
473
- sortBySlugAndPlatform(entries);
474
- sortBySlugAndPlatform(bundles);
475
- const index = entries.reduce((acc, entry) => {
476
- acc[entry.name] = entry;
477
- return acc;
478
- }, {});
522
+ items.sort((a, b) => a.slug.localeCompare(b.slug));
523
+ bundles.sort((a, b) => {
524
+ if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
525
+ return a.slug.localeCompare(b.slug);
526
+ });
479
527
  return {
480
- entries,
481
- index,
528
+ items,
482
529
  bundles
483
530
  };
484
531
  }
485
532
  async function createBundledFilesFromInputs(files) {
486
533
  const results = await Promise.all(files.map(async (file) => {
487
- const payload = normalizeFilePayload(file.contents);
488
- const contents = encodeFilePayload(payload, file.path);
534
+ const payload = normalizeFilePayload(file.content);
535
+ const content = encodeFilePayload(payload, file.path);
489
536
  const checksum = await sha256(payload);
490
537
  return {
491
538
  path: toPosixPath(file.path),
492
539
  size: payload.length,
493
540
  checksum,
494
- contents
541
+ content
495
542
  };
496
543
  }));
497
544
  return results.sort((a, b) => a.path.localeCompare(b.path));
498
545
  }
499
- function normalizeFilePayload(contents) {
500
- if (typeof contents === "string") return new TextEncoder().encode(contents);
501
- if (contents instanceof ArrayBuffer) return new Uint8Array(contents);
502
- if (ArrayBuffer.isView(contents)) return new Uint8Array(contents.buffer, contents.byteOffset, contents.byteLength);
503
- return new Uint8Array(contents);
546
+ function normalizeFilePayload(content) {
547
+ if (typeof content === "string") return new TextEncoder().encode(content);
548
+ if (content instanceof ArrayBuffer) return new Uint8Array(content);
549
+ if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
550
+ return new Uint8Array(content);
504
551
  }
505
552
  function encodeFilePayload(data, filePath) {
506
553
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -510,36 +557,22 @@ function encodeFilePayload(data, filePath) {
510
557
  throw new Error(`Binary files are not supported: "${filePath}". Only UTF-8 text files are allowed.`);
511
558
  }
512
559
  }
513
- /**
514
- * Normalize bundle base by removing trailing slashes.
515
- * Returns empty string if base is undefined/empty (use default relative path).
516
- */
517
560
  function normalizeBundleBase(base) {
518
561
  if (!base) return "";
519
562
  return base.replace(/\/+$/, "");
520
563
  }
521
- /**
522
- * Returns the bundle URL/path for a preset.
523
- * Format: {base}/{STATIC_BUNDLE_DIR}/{slug}/{platform}/{version}
524
- */
525
- function getBundlePath(base, slug, platform, version = LATEST_VERSION) {
564
+ function getBundlePath(base, slug, platform, version) {
526
565
  const prefix = base ? `${base}/` : "";
527
566
  return `${prefix}${STATIC_BUNDLE_DIR}/${slug}/${platform}/${version}`;
528
567
  }
529
568
  function ensureKnownPlatform(platform, slug) {
530
569
  if (!isSupportedPlatform(platform)) throw new Error(`Unknown platform "${platform}" in ${slug}. Supported: ${PLATFORM_IDS.join(", ")}`);
531
570
  }
532
- function sortBySlugAndPlatform(items) {
533
- items.sort((a, b) => {
534
- if (a.slug === b.slug) return a.platform.localeCompare(b.platform);
535
- return a.slug.localeCompare(b.slug);
536
- });
537
- }
538
571
 
539
572
  //#endregion
540
573
  //#region src/client/bundle.ts
541
574
  function decodeBundledFile(file) {
542
- return encodeUtf8(file.contents);
575
+ return encodeUtf8(file.content);
543
576
  }
544
577
  async function verifyBundledFileChecksum(file, payload) {
545
578
  const bytes = toUint8Array(payload);
@@ -567,39 +600,48 @@ async function sha256Hex(payload) {
567
600
  }
568
601
 
569
602
  //#endregion
570
- //#region src/client/registry.ts
603
+ //#region src/constants.ts
604
+ /**
605
+ * Shared constants for agentrules presets and registry.
606
+ */
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";
613
+ /** API root path segment */
614
+ const API_PATH = "api";
615
+ /** Default version identifier for latest preset version */
616
+ const LATEST_VERSION = "latest";
571
617
  /**
572
- * Resolves a preset from the registry via API endpoint.
573
- * Returns the entry metadata and the absolute bundle URL.
618
+ * API endpoint paths (relative to registry base URL).
574
619
  *
575
- * @param baseUrl - Registry base URL
576
- * @param slug - Preset slug
577
- * @param platform - Target platform
578
- * @param version - Version to resolve (defaults to "latest")
620
+ * Note on slug handling:
621
+ * - Slugs may contain slashes (e.g., "username/my-preset") which flow through as path segments
622
+ * - The client is responsible for validating values before making requests
579
623
  */
580
- async function resolvePreset(baseUrl, slug, platform, version = LATEST_VERSION) {
581
- const apiUrl = new URL(API_ENDPOINTS.presets.get(slug, platform, version), baseUrl);
582
- const response = await fetch(apiUrl);
583
- if (!response.ok) {
584
- if (response.status === 404) {
585
- const versionInfo = version === LATEST_VERSION ? "" : ` version "${version}"`;
586
- throw new Error(`Preset "${slug}"${versionInfo} for platform "${platform}" was not found in the registry.`);
587
- }
588
- throw new Error(`Failed to resolve preset (${response.status} ${response.statusText}).`);
589
- }
590
- try {
591
- const preset = await response.json();
592
- const resolvedBundleUrl = new URL(preset.bundleUrl, baseUrl).toString();
593
- return {
594
- preset,
595
- bundleUrl: resolvedBundleUrl
596
- };
597
- } catch (error) {
598
- throw new Error(`Unable to parse preset response: ${error.message}`);
599
- }
600
- }
624
+ const API_ENDPOINTS = {
625
+ presets: {
626
+ base: `${API_PATH}/presets`,
627
+ unpublish: (slug, version) => `${API_PATH}/presets/${slug}/${version}`
628
+ },
629
+ auth: {
630
+ session: `${API_PATH}/auth/get-session`,
631
+ deviceCode: `${API_PATH}/auth/device/code`,
632
+ 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}` }
639
+ };
640
+
641
+ //#endregion
642
+ //#region src/client/registry.ts
601
643
  /**
602
- * Fetches a bundle from an absolute URL or resolves it relative to the registry.
644
+ * Fetches a bundle from an absolute URL.
603
645
  */
604
646
  async function fetchBundle(bundleUrl) {
605
647
  const response = await fetch(bundleUrl);
@@ -610,22 +652,200 @@ async function fetchBundle(bundleUrl) {
610
652
  throw new Error(`Unable to parse bundle JSON: ${error.message}`);
611
653
  }
612
654
  }
655
+ /**
656
+ * Resolves a slug to get all versions and platform variants.
657
+ *
658
+ * @param baseUrl - Registry base URL
659
+ * @param slug - Content slug (may contain slashes, e.g., "username/my-preset")
660
+ * @param version - Optional version filter (server may ignore for static registries)
661
+ * @returns Resolved data, or null if not found
662
+ * @throws Error on network/server errors
663
+ */
664
+ async function resolveSlug(baseUrl, slug, version) {
665
+ const url = new URL(API_ENDPOINTS.items.get(slug), baseUrl);
666
+ if (version) url.searchParams.set("version", version);
667
+ let response;
668
+ try {
669
+ response = await fetch(url);
670
+ } catch (error) {
671
+ throw new Error(`Failed to connect to registry: ${error.message}`);
672
+ }
673
+ if (response.status === 404) return null;
674
+ if (!response.ok) {
675
+ let errorMessage = `Registry returned ${response.status} ${response.statusText}`;
676
+ try {
677
+ const body = await response.json();
678
+ if (body && typeof body === "object" && "error" in body && typeof body.error === "string") errorMessage = body.error;
679
+ } catch {}
680
+ throw new Error(errorMessage);
681
+ }
682
+ 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
+ }
686
+ return data;
687
+ }
688
+
689
+ //#endregion
690
+ //#region src/resolve/schema.ts
691
+ const VERSION_REGEX = /^[1-9]\d*\.\d+$/;
692
+ const presetVariantBundleSchema = z.object({
693
+ platform: platformIdSchema,
694
+ bundleUrl: z.string().min(1),
695
+ fileCount: z.number().int().nonnegative(),
696
+ totalSize: z.number().int().nonnegative()
697
+ });
698
+ const presetVariantInlineSchema = z.object({
699
+ platform: platformIdSchema,
700
+ content: z.string().min(1),
701
+ fileCount: z.number().int().nonnegative(),
702
+ totalSize: z.number().int().nonnegative()
703
+ });
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
+ });
716
+ const ruleVersionSchema = z.object({
717
+ version: z.string().regex(VERSION_REGEX, "Version must be MAJOR.MINOR format"),
718
+ isLatest: z.boolean(),
719
+ publishedAt: z.string().datetime().optional(),
720
+ variants: z.array(ruleVariantSchema).min(1)
721
+ });
722
+ const resolvedPresetSchema = z.object({
723
+ kind: z.literal("preset"),
724
+ slug: z.string().min(1),
725
+ name: z.string().min(1),
726
+ title: z.string().min(1),
727
+ description: z.string(),
728
+ tags: z.array(z.string()),
729
+ license: z.string(),
730
+ 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
+ versions: z.array(ruleVersionSchema).min(1)
741
+ });
742
+ const resolveResponseSchema = z.discriminatedUnion("kind", [resolvedPresetSchema, resolvedRuleSchema]);
743
+
744
+ //#endregion
745
+ //#region src/resolve/utils.ts
746
+ /**
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
760
+ */
761
+ function hasBundle(variant) {
762
+ return "bundleUrl" in variant;
763
+ }
764
+ /**
765
+ * Type guard for preset variant with inline content
766
+ */
767
+ function hasInlineContent(variant) {
768
+ return "content" in variant;
769
+ }
770
+ /**
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
+ * Get the latest version from a resolved rule
778
+ */
779
+ function getLatestRuleVersion(item) {
780
+ return item.versions.find((v) => v.isLatest);
781
+ }
782
+ /**
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
+ * Get a specific version from a resolved rule
790
+ */
791
+ function getRuleVersion(item, version) {
792
+ return item.versions.find((v) => v.version === version);
793
+ }
794
+ /**
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
+ * Get a specific platform variant from a rule version
802
+ */
803
+ function getRuleVariant(version, platform) {
804
+ return version.variants.find((v) => v.platform === platform);
805
+ }
806
+ /**
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
+ * Get all available platforms for a rule version
814
+ */
815
+ function getRulePlatforms(version) {
816
+ return version.variants.map((v) => v.platform);
817
+ }
818
+ /**
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
+ * Check if a platform is available in any version of a rule
826
+ */
827
+ function ruleHasPlatform(item, platform) {
828
+ return item.versions.some((v) => v.variants.some((variant) => variant.platform === platform));
829
+ }
613
830
 
614
831
  //#endregion
615
832
  //#region src/rule/schema.ts
616
- const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
617
833
  /**
618
- * Schema for the rule name.
619
- * This is what users provide when creating a rule.
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
620
840
  */
621
- const ruleNameSchema = z.string().trim().min(1, "Name is required").max(64, "Name must be 64 characters or less").regex(NAME_REGEX, "Must be lowercase alphanumeric with hyphens");
622
- const ruleTitleSchema = z.string().trim().min(1, "Title is required").max(80, "Title must be 80 characters or less");
623
- const ruleDescriptionSchema = z.string().trim().max(500, "Description must be 500 characters or less");
841
+ const ruleNameSchema = nameSchema;
842
+ const ruleTitleSchema = titleSchema;
843
+ const ruleDescriptionSchema = descriptionSchema;
844
+ const ruleTagSchema = tagSchema;
845
+ const ruleTagsSchema = tagsSchema;
624
846
  const rulePlatformSchema = z.enum(PLATFORM_IDS);
625
847
  const ruleTypeSchema = z.string().trim().min(1).max(32);
626
848
  const ruleContentSchema = z.string().min(1, "Content is required").max(1e5, "Content must be 100KB or less");
627
- const ruleTagSchema = z.string().trim().min(1).max(32).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Tags must be lowercase alphanumeric with single hyphens between words");
628
- const ruleTagsSchema = z.array(ruleTagSchema).min(1, "At least 1 tag is required").max(10, "Maximum 10 tags allowed");
629
849
  /** Common fields shared across all platform-type combinations */
630
850
  const ruleCommonFields = {
631
851
  name: ruleNameSchema,
@@ -658,13 +878,6 @@ const rulePlatformTypeSchema = z.discriminatedUnion("platform", [
658
878
  ]);
659
879
  /** Schema for rule creation with discriminated union for platform+type */
660
880
  const ruleCreateInputSchema = z.object(ruleCommonFields).and(rulePlatformTypeSchema);
661
- const ruleUpdateInputSchema = z.object({
662
- name: ruleNameSchema,
663
- title: ruleTitleSchema.optional(),
664
- description: ruleDescriptionSchema.optional(),
665
- content: ruleContentSchema.optional(),
666
- tags: ruleTagsSchema.optional()
667
- });
668
881
 
669
882
  //#endregion
670
883
  //#region src/utils/diff.ts
@@ -690,4 +903,4 @@ function normalizeBundlePath(value) {
690
903
  }
691
904
 
692
905
  //#endregion
693
- 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, encodeItemName, encodeUtf8, fetchBundle, getInstallPath, getPlatformConfig, getPlatformFromDir, getRuleTypeConfig, getValidRuleTypes, isLikelyText, isPlatformDir, isSupportedPlatform, isValidRuleType, licenseSchema, normalizeBundlePath, normalizePlatformInput, platformIdSchema, presetBundleSchema, presetConfigSchema, presetIndexSchema, presetPublishInputSchema, presetSchema, resolvePreset, ruleContentSchema, ruleCreateInputSchema, ruleDescriptionSchema, ruleNameSchema, rulePlatformSchema, rulePlatformTypeSchema, ruleTagSchema, ruleTagsSchema, ruleTitleSchema, ruleTypeSchema, ruleUpdateInputSchema, slugSchema, tagSchema, tagsSchema, titleSchema, toPosixPath, toUint8Array, toUtf8String, validatePresetConfig, verifyBundledFileChecksum };
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 };