@hasna/skills 0.1.63 → 0.1.64

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 (38) hide show
  1. package/README.md +28 -6
  2. package/bin/index.js +1593 -228
  3. package/bin/mcp.js +290 -24
  4. package/bin/migrate.js +229 -33
  5. package/bin/server.js +774 -116
  6. package/bin/worker.js +558 -79
  7. package/dist/cli/commands/registry-reconcile.d.ts +2 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +559 -251
  10. package/dist/lib/agent-sync.d.ts +26 -6
  11. package/dist/lib/auth-store.d.ts +37 -0
  12. package/dist/lib/config.d.ts +51 -0
  13. package/dist/lib/home-census.d.ts +4 -0
  14. package/dist/lib/home-migration.d.ts +8 -9
  15. package/dist/lib/native-storage.d.ts +1 -1
  16. package/dist/lib/portable-skills.d.ts +35 -6
  17. package/dist/lib/pull.d.ts +31 -0
  18. package/dist/lib/registry-reconcile.d.ts +114 -0
  19. package/dist/lib/registry.d.ts +7 -4
  20. package/dist/lib/remote-client.d.ts +118 -1
  21. package/dist/lib/remote-registry.d.ts +26 -0
  22. package/dist/lib/revision.d.ts +29 -0
  23. package/dist/sdk/index.js +1351 -353
  24. package/dist/server/app.d.ts +1 -1
  25. package/dist/server/config.d.ts +12 -2
  26. package/dist/server/rows.d.ts +2 -1
  27. package/dist/server/skills-api.d.ts +108 -6
  28. package/dist/server/sqlite-store.d.ts +20 -3
  29. package/dist/server/store.d.ts +31 -5
  30. package/dist/server/types.d.ts +98 -3
  31. package/dist/storage.js +7 -2
  32. package/migrations/postgres/0004_hosted_pins.sql +28 -0
  33. package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
  34. package/migrations/postgres/0005_tag_projection.sql +36 -0
  35. package/migrations/sqlite/0004_hosted_pins.sql +21 -0
  36. package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
  37. package/migrations/sqlite/0005_tag_projection.sql +25 -0
  38. package/package.json +3 -2
package/bin/index.js CHANGED
@@ -36860,7 +36860,7 @@ var package_default;
36860
36860
  var init_package = __esm(() => {
36861
36861
  package_default = {
36862
36862
  name: "@hasna/skills",
36863
- version: "0.1.63",
36863
+ version: "0.1.64",
36864
36864
  description: "Skills library for AI coding agents",
36865
36865
  type: "module",
36866
36866
  bin: {
@@ -36934,6 +36934,7 @@ var init_package = __esm(() => {
36934
36934
  license: "Apache-2.0",
36935
36935
  devDependencies: {
36936
36936
  "@types/bun": "latest",
36937
+ "@types/node": "25.2.3",
36937
36938
  "@types/react": "^18.2.0",
36938
36939
  "bun-types": "1.3.14",
36939
36940
  "react-devtools-core": "^7.0.1",
@@ -36942,7 +36943,7 @@ var init_package = __esm(() => {
36942
36943
  dependencies: {
36943
36944
  "@aws-sdk/client-ecs": "^3.1079.0",
36944
36945
  "@aws-sdk/client-s3": "^3.1079.0",
36945
- "@hasna/events": "0.1.7",
36946
+ "@hasna/events": "0.1.16",
36946
36947
  "@modelcontextprotocol/sdk": "^1.26.0",
36947
36948
  chalk: "^5.3.0",
36948
36949
  commander: "^12.1.0",
@@ -37956,6 +37957,9 @@ function normalizeConfigValue(key, value) {
37956
37957
  return value.trim() ? value : undefined;
37957
37958
  return;
37958
37959
  }
37960
+ function isOwnerLayoutMigrated(appDir) {
37961
+ return existsSync3(join3(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
37962
+ }
37959
37963
  function getDataDir() {
37960
37964
  const override = process.env[DATA_DIR_ENV];
37961
37965
  if (override) {
@@ -37979,6 +37983,33 @@ function getDataDir() {
37979
37983
  }
37980
37984
  return newDir;
37981
37985
  }
37986
+ function getDataDirReadOnly() {
37987
+ const override = process.env[DATA_DIR_ENV];
37988
+ if (override)
37989
+ return override;
37990
+ return join3(process.env["HOME"] || process.env["USERPROFILE"] || homedir2(), ".hasna", "skills");
37991
+ }
37992
+ function getConfigPathReadOnly(scope) {
37993
+ if (scope === "global")
37994
+ return join3(getDataDirReadOnly(), "config.json");
37995
+ return join3(process.cwd(), "skills.config.json");
37996
+ }
37997
+ function loadConfigReadOnly() {
37998
+ const canonicalConfigPath = getConfigPathReadOnly("global");
37999
+ let globalConfig;
38000
+ if (existsSync3(canonicalConfigPath)) {
38001
+ globalConfig = readConfigFile(canonicalConfigPath);
38002
+ } else if (process.env[DATA_DIR_ENV] !== undefined) {
38003
+ globalConfig = {};
38004
+ } else {
38005
+ globalConfig = readConfigFile(legacyConfigFilePath());
38006
+ }
38007
+ const projectConfig = readConfigFile(getConfigPathReadOnly("project"));
38008
+ return { ...globalConfig, ...projectConfig };
38009
+ }
38010
+ function legacyConfigFilePath() {
38011
+ return join3(process.env["HOME"] || process.env["USERPROFILE"] || homedir2(), ".skillsrc");
38012
+ }
37982
38013
  function getConfigPath(scope) {
37983
38014
  if (scope === "global") {
37984
38015
  return join3(getDataDir(), "config.json");
@@ -38065,7 +38096,7 @@ function unsetConfig(key, scope = "project") {
38065
38096
  `);
38066
38097
  return true;
38067
38098
  }
38068
- var ENUM_KEYS, STRING_KEYS, DATA_DIR_ENV = "HASNA_SKILLS_DIR", INSTALLED_SKILLS_DIRNAME = "installed";
38099
+ var ENUM_KEYS, STRING_KEYS, DATA_DIR_ENV = "HASNA_SKILLS_DIR", INSTALLED_SKILLS_DIRNAME = "installed", SKILLS_CACHE_DIRNAME = "skills", LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
38069
38100
  var init_config = __esm(() => {
38070
38101
  init_retired_settings();
38071
38102
  ENUM_KEYS = {
@@ -38236,7 +38267,7 @@ var init_development_tools = __esm(() => {
38236
38267
  {
38237
38268
  name: "monitor",
38238
38269
  displayName: "Monitor",
38239
- description: "Operate the open-monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
38270
+ description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
38240
38271
  category: "Development Tools",
38241
38272
  tags: ["monitoring", "mcp", "processes", "operations"]
38242
38273
  },
@@ -38282,6 +38313,14 @@ var init_development_tools = __esm(() => {
38282
38313
  description: "Validate configuration files for syntax and schema compliance",
38283
38314
  category: "Development Tools",
38284
38315
  tags: ["config", "validation", "schema", "linting"]
38316
+ },
38317
+ {
38318
+ name: "session-inject-monitor",
38319
+ displayName: "Session Inject Monitor",
38320
+ description: "Set up a declarative monitor that injects a prompt into a live coding-agent session when a watched source (conversations, email, todos, knowledge, command output) has new content",
38321
+ category: "Development Tools",
38322
+ tags: ["monitor", "session", "injection", "automation", "wake"],
38323
+ kind: "instruction"
38285
38324
  }
38286
38325
  ];
38287
38326
  });
@@ -38693,7 +38732,7 @@ var init_design_branding = __esm(() => {
38693
38732
  displayName: "Site Analyze",
38694
38733
  description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
38695
38734
  category: "Design & Branding",
38696
- tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"]
38735
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
38697
38736
  }
38698
38737
  ];
38699
38738
  });
@@ -40114,6 +40153,9 @@ function getPortableSkillsRoot(options = {}) {
40114
40153
  if (options.rootDir)
40115
40154
  return options.rootDir;
40116
40155
  const appDir = options.homeDir ? join7(options.homeDir, ".hasna", "skills") : getDataDir();
40156
+ const cache3 = join7(appDir, SKILLS_CACHE_DIRNAME);
40157
+ if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
40158
+ return cache3;
40117
40159
  const installed = join7(appDir, INSTALLED_SKILLS_DIRNAME);
40118
40160
  migrateLegacySkillLayout(appDir, installed);
40119
40161
  return installed;
@@ -40333,10 +40375,7 @@ function writeCorpusSkill(input, options = {}) {
40333
40375
  const skillPath = join7(root, name);
40334
40376
  const created = !existsSync7(skillPath);
40335
40377
  mkdirSync3(skillPath, { recursive: true });
40336
- const skillMd = input.skillMd.endsWith(`
40337
- `) ? input.skillMd : `${input.skillMd}
40338
- `;
40339
- writeFileSync3(join7(skillPath, "SKILL.md"), skillMd);
40378
+ writeFileSync3(join7(skillPath, "SKILL.md"), input.skillMd);
40340
40379
  const frontmatter = parseSkillFrontmatter(input.skillMd) ?? undefined;
40341
40380
  const kind = input.meta?.kind ?? parseSkillKind(frontmatter?.kind) ?? "instruction";
40342
40381
  const manifest = {
@@ -42516,16 +42555,7 @@ import { join as join9 } from "path";
42516
42555
  function layoutMigrationRecordPath(appDir) {
42517
42556
  return join9(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD);
42518
42557
  }
42519
- function isOwnerLayoutMigrated(appDir) {
42520
- return existsSync9(layoutMigrationRecordPath(appDir));
42521
- }
42522
42558
  function resolveCorpusRoot(options = {}) {
42523
- if (options.rootDir)
42524
- return options.rootDir;
42525
- const appDir = options.homeDir ? join9(options.homeDir, ".hasna", "skills") : getDataDir();
42526
- const cache3 = join9(appDir, SKILLS_CACHE_DIRNAME);
42527
- if (isOwnerLayoutMigrated(appDir) && existsSync9(cache3))
42528
- return cache3;
42529
42559
  return getPortableSkillsRoot(options);
42530
42560
  }
42531
42561
  function migrationAppDir(homeDir) {
@@ -42647,10 +42677,11 @@ function migrateOwnerLayout(options = {}) {
42647
42677
  `);
42648
42678
  return { status: "migrated", moved, created, record };
42649
42679
  }
42650
- var SKILLS_CACHE_DIRNAME = "skills", LOGS_DIRNAME = "logs", OUTPUTS_DIRNAME = "outputs", LAYOUT_MIGRATION_RECORD = ".layout-migration.json", LEGACY_CUSTOM_DIRNAME2 = "custom";
42680
+ var LOGS_DIRNAME = "logs", OUTPUTS_DIRNAME = "outputs", LEGACY_CUSTOM_DIRNAME2 = "custom";
42651
42681
  var init_home_migration = __esm(() => {
42652
42682
  init_config();
42653
42683
  init_portable_skills();
42684
+ init_config();
42654
42685
  });
42655
42686
 
42656
42687
  // src/lib/agent-sync.ts
@@ -42705,6 +42736,9 @@ ${lines.join(`
42705
42736
  ---
42706
42737
  ${body}`;
42707
42738
  }
42739
+ function isPointerSkillMd(markdown) {
42740
+ return markdown.includes(POINTER_MARKER_PHRASE) && /^kind:\s*executable\b/m.test(markdown);
42741
+ }
42708
42742
  function pointerSkillMd(name, description) {
42709
42743
  const display = name.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
42710
42744
  return [
@@ -42730,7 +42764,7 @@ function resolveSyncCorpus(options = {}) {
42730
42764
  if (explicit) {
42731
42765
  const roots = packageSourceRoots(explicit);
42732
42766
  if (roots.length === 0) {
42733
- throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills/ and/or agent-skills/`);
42767
+ throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills/`);
42734
42768
  }
42735
42769
  return { roots, source: "source" };
42736
42770
  }
@@ -42738,7 +42772,7 @@ function resolveSyncCorpus(options = {}) {
42738
42772
  }
42739
42773
  function packageSourceRoots(source) {
42740
42774
  const roots = [];
42741
- for (const sub of ["skills", "agent-skills"]) {
42775
+ for (const sub of ["skills"]) {
42742
42776
  const candidate = join10(source, sub);
42743
42777
  if (existsSync10(candidate) && isDirectory(candidate))
42744
42778
  roots.push(candidate);
@@ -42863,6 +42897,21 @@ function writeManagedSkillDir(dir, skillMd, options) {
42863
42897
  reason: "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
42864
42898
  };
42865
42899
  }
42900
+ if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
42901
+ let existingIsStub = false;
42902
+ try {
42903
+ existingIsStub = isPointerSkillMd(readFileSync7(skillMdPath, "utf-8"));
42904
+ } catch {
42905
+ existingIsStub = false;
42906
+ }
42907
+ if (!existingIsStub) {
42908
+ return {
42909
+ action: "skip",
42910
+ path: skillMdPath,
42911
+ reason: "refusing to replace a content-bearing managed home with an executable pointer stub (the corpus entry lacks kind: instruction); pass --force to overwrite"
42912
+ };
42913
+ }
42914
+ }
42866
42915
  const action = dirExists ? "update" : "create";
42867
42916
  if (options.dryRun)
42868
42917
  return { action, path: skillMdPath };
@@ -42947,7 +42996,7 @@ function normalizeRequested(names) {
42947
42996
  const normalized = names.map((name) => name.trim()).filter(Boolean).map((name) => normalizePortableSkillName(name));
42948
42997
  return [...new Set(normalized)];
42949
42998
  }
42950
- var SYNC_AGENTS, SKILLS_SOURCE_ENV = "SKILLS_SOURCE", SYNC_MARKER_FILE = ".hasna-skills.json", SYNC_MARKER_MANAGED_BY = "@hasna/skills";
42999
+ var SYNC_AGENTS, SKILLS_SOURCE_ENV = "SKILLS_SOURCE", SYNC_MARKER_FILE = ".hasna-skills.json", SYNC_MARKER_MANAGED_BY = "@hasna/skills", POINTER_MARKER_PHRASE = "This is an executable skill from the @hasna/skills catalog";
42951
43000
  var init_agent_sync = __esm(() => {
42952
43001
  init_portable_skills();
42953
43002
  init_home_migration();
@@ -47539,19 +47588,33 @@ var exports_auth_store = {};
47539
47588
  __export(exports_auth_store, {
47540
47589
  saveAuthConfig: () => saveAuthConfig,
47541
47590
  normalizeSkillsApiOrigin: () => normalizeSkillsApiOrigin,
47591
+ getAuthFilePathReadOnly: () => getAuthFilePathReadOnly,
47592
+ getAuthFilePath: () => getAuthFilePath,
47593
+ getAuthConfigReadOnly: () => getAuthConfigReadOnly,
47542
47594
  getAuthConfig: () => getAuthConfig,
47543
47595
  getApiUrl: () => getApiUrl,
47596
+ getApiKeyReadOnly: () => getApiKeyReadOnly,
47544
47597
  getApiKey: () => getApiKey,
47545
47598
  clearAuthConfig: () => clearAuthConfig
47546
47599
  });
47547
47600
  import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, unlinkSync } from "fs";
47548
- import { join as join13 } from "path";
47601
+ import { dirname as dirname6, join as join13 } from "path";
47549
47602
  import { homedir as homedir5 } from "os";
47603
+ function getAuthFilePath() {
47604
+ return join13(getDataDir(), "auth.json");
47605
+ }
47606
+ function getAuthFilePathReadOnly() {
47607
+ return join13(getDataDirReadOnly(), "auth.json");
47608
+ }
47609
+ function legacyAuthFilePath() {
47610
+ return join13(process.env["HOME"] || process.env["USERPROFILE"] || homedir5(), ".skills", "auth.json");
47611
+ }
47550
47612
  function getAuthConfig() {
47551
47613
  if (cachedConfig !== undefined)
47552
47614
  return cachedConfig;
47553
47615
  try {
47554
- const raw = readFileSync10(existsSync13(AUTH_FILE) ? AUTH_FILE : LEGACY_AUTH_FILE, "utf-8");
47616
+ const file = existsSync13(getAuthFilePath()) ? getAuthFilePath() : legacyAuthFilePath();
47617
+ const raw = readFileSync10(file, "utf-8");
47555
47618
  const config = JSON.parse(raw);
47556
47619
  if (!config.apiKey) {
47557
47620
  cachedConfig = null;
@@ -47565,19 +47628,20 @@ function getAuthConfig() {
47565
47628
  }
47566
47629
  }
47567
47630
  function saveAuthConfig(config) {
47568
- mkdirSync7(AUTH_DIR, { recursive: true, mode: 448 });
47569
- writeFileSync7(AUTH_FILE, JSON.stringify(config, null, 2) + `
47631
+ const file = getAuthFilePath();
47632
+ mkdirSync7(dirname6(file), { recursive: true, mode: 448 });
47633
+ writeFileSync7(file, JSON.stringify(config, null, 2) + `
47570
47634
  `, { mode: 384 });
47571
47635
  cachedConfig = config;
47572
47636
  }
47573
47637
  function clearAuthConfig() {
47574
47638
  try {
47575
- unlinkSync(AUTH_FILE);
47639
+ unlinkSync(getAuthFilePath());
47576
47640
  } catch {}
47577
47641
  try {
47578
- unlinkSync(LEGACY_AUTH_FILE);
47642
+ unlinkSync(legacyAuthFilePath());
47579
47643
  } catch {}
47580
- cachedConfig = null;
47644
+ cachedConfig = undefined;
47581
47645
  }
47582
47646
  function getApiKey() {
47583
47647
  if (process.env.SKILLS_API_KEY)
@@ -47586,6 +47650,25 @@ function getApiKey() {
47586
47650
  return process.env.SKILL_API_KEY;
47587
47651
  return getAuthConfig()?.apiKey || null;
47588
47652
  }
47653
+ function getAuthConfigReadOnly() {
47654
+ try {
47655
+ const file = existsSync13(getAuthFilePathReadOnly()) ? getAuthFilePathReadOnly() : legacyAuthFilePath();
47656
+ const raw = readFileSync10(file, "utf-8");
47657
+ const config = JSON.parse(raw);
47658
+ if (!config.apiKey)
47659
+ return null;
47660
+ return config;
47661
+ } catch {
47662
+ return null;
47663
+ }
47664
+ }
47665
+ function getApiKeyReadOnly() {
47666
+ if (process.env.SKILLS_API_KEY)
47667
+ return process.env.SKILLS_API_KEY;
47668
+ if (process.env.SKILL_API_KEY)
47669
+ return process.env.SKILL_API_KEY;
47670
+ return getAuthConfigReadOnly()?.apiKey || null;
47671
+ }
47589
47672
  function normalizeSkillsApiOrigin(apiUrl) {
47590
47673
  const url = new URL(apiUrl);
47591
47674
  const pathname = url.pathname.replace(/\/+$/, "");
@@ -47601,12 +47684,10 @@ function normalizeSkillsApiOrigin(apiUrl) {
47601
47684
  function getApiUrl(action) {
47602
47685
  return normalizeSkillsApiOrigin(requireApiUrl(action));
47603
47686
  }
47604
- var AUTH_DIR, AUTH_FILE, LEGACY_AUTH_FILE, cachedConfig;
47687
+ var cachedConfig;
47605
47688
  var init_auth_store = __esm(() => {
47606
47689
  init_api_url();
47607
- AUTH_DIR = join13(homedir5(), ".hasna", "skills");
47608
- AUTH_FILE = join13(AUTH_DIR, "auth.json");
47609
- LEGACY_AUTH_FILE = join13(homedir5(), ".skills", "auth.json");
47690
+ init_config();
47610
47691
  });
47611
47692
 
47612
47693
  // src/lib/remote-registry.ts
@@ -47723,6 +47804,16 @@ async function loadRemoteRegistry(options = {}) {
47723
47804
  const url = buildSkillsApiUrl(apiUrl, options.endpoint);
47724
47805
  return parseRemoteRegistryPayload(await fetchRemoteJson(url, options));
47725
47806
  }
47807
+ async function mergeRemoteRegistry(local, options = {}) {
47808
+ const apiUrl = options.apiUrl || getConfiguredApiUrl();
47809
+ if (!apiUrl)
47810
+ return local;
47811
+ const token = options.authToken !== undefined ? options.authToken : getApiKey();
47812
+ if (!token?.trim())
47813
+ return local;
47814
+ const remote = await loadRemoteRegistry({ ...options, apiUrl, authToken: token });
47815
+ return mergeSkillRegistryLists(local, remote);
47816
+ }
47726
47817
  async function loadRemoteSkill(name, options = {}) {
47727
47818
  const apiUrl = options.apiUrl || getConfiguredApiUrl();
47728
47819
  if (!apiUrl) {
@@ -47739,6 +47830,7 @@ var init_remote_registry = __esm(() => {
47739
47830
  init_auth_store();
47740
47831
  init_config();
47741
47832
  init_discovery();
47833
+ init_registry_merge();
47742
47834
  remoteAvailabilitySchema = exports_external.object({
47743
47835
  status: exports_external.enum(["available", "unavailable"]),
47744
47836
  code: exports_external.string().optional(),
@@ -48077,7 +48169,7 @@ __export(exports_list, {
48077
48169
  registerBrowse: () => registerBrowse
48078
48170
  });
48079
48171
  function registerBrowse(parent) {
48080
- parent.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-p, --pinned", "Show only pinned skills", false).option("-t, --tags <tags>", "Filter by comma-separated tags (OR logic, case-insensitive)").option("--all", "Show the full skill registry instead of the default basic set", false).option("--remote", "Use remote registry from SKILLS_API_URL or config apiUrl", false).option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,price,description)").option("--limit <n>", "Maximum rows to print for human output (default: 30, use 0 or all for every row)").option("--cursor <n>", "Numeric offset for human-output pagination", "0").option("--verbose", "Show longer descriptions and tags in human output", false).action((options) => {
48172
+ parent.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-p, --pinned", "Show only pinned skills", false).option("-t, --tags <tags>", "Filter by comma-separated tags (OR logic, case-insensitive)").option("--tag <tags>", "Filter by comma-separated tags (alias for --tags)").option("--all", "Show the full skill registry instead of the default basic set", false).option("--remote", "Use remote registry from SKILLS_API_URL or config apiUrl", false).option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,price,description)").option("--limit <n>", "Maximum rows to print for human output (default: 30, use 0 or all for every row)").option("--cursor <n>", "Numeric offset for human-output pagination", "0").option("--verbose", "Show longer descriptions and tags in human output", false).action((options) => {
48081
48173
  return handleList(options).catch(handleBrowseError);
48082
48174
  });
48083
48175
  parent.command("search").alias("s").argument("<query>", "Search term").option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,price,description)").option("-c, --category <category>", "Filter results by category").option("-t, --tags <tags>", "Filter results by comma-separated tags (OR logic, case-insensitive)").option("--all", "Search the full skill registry instead of the default basic set", false).option("--remote", "Use remote registry from SKILLS_API_URL or config apiUrl", false).option("--limit <n>", "Maximum rows to print for human output (default: 20, use 0 or all for every row)").option("--cursor <n>", "Numeric offset for human-output pagination", "0").option("--verbose", "Show longer descriptions and tags in human output", false).description("Search for skills").action((query, options) => {
@@ -48129,10 +48221,11 @@ async function writeJson(value, space) {
48129
48221
  async function getBrowseRegistry(options) {
48130
48222
  const profile = options.all ? "all" : "basic";
48131
48223
  const local = loadRegistryProfile(profile);
48132
- if (!options.remote)
48133
- return local;
48134
- const remote = await loadRemoteRegistry();
48135
- return mergeSkillRegistryLists(local, remote);
48224
+ if (options.remote) {
48225
+ const remote = await loadRemoteRegistry();
48226
+ return mergeSkillRegistryLists(local, remote);
48227
+ }
48228
+ return mergeRemoteRegistry(local);
48136
48229
  }
48137
48230
  function registryCategories(registry) {
48138
48231
  const known = CATEGORIES.filter((category) => registry.some((skill) => skill.category === category));
@@ -48140,7 +48233,13 @@ function registryCategories(registry) {
48140
48233
  return [...known, ...extra];
48141
48234
  }
48142
48235
  function availableCategories(options, registry) {
48143
- return options.remote ? registryCategories(registry) : [...CATEGORIES];
48236
+ if (options.remote)
48237
+ return registryCategories(registry);
48238
+ const hasRemoteRows = registry.some((skill) => skill.source === "remote");
48239
+ if (!hasRemoteRows)
48240
+ return [...CATEGORIES];
48241
+ const extras = Array.from(new Set(registry.map((skill) => skill.category))).filter((category) => !CATEGORIES.includes(category)).sort();
48242
+ return [...CATEGORIES, ...extras];
48144
48243
  }
48145
48244
  async function handleList(options) {
48146
48245
  const brief = options.brief && !options.json;
@@ -48185,7 +48284,8 @@ Pinned skills (${showingLabel(installed.length, page2.items.length, page2.offset
48185
48284
  printPageHint(page2, "skills list --pinned");
48186
48285
  return;
48187
48286
  }
48188
- const tagFilter = options.tags ? options.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean) : null;
48287
+ const tagsOption = options.tags ?? options.tag;
48288
+ const tagFilter = tagsOption ? tagsOption.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean) : null;
48189
48289
  if (options.category) {
48190
48290
  const categories = availableCategories(options, registry);
48191
48291
  const category = categories.find((c) => c.toLowerCase() === options.category.toLowerCase());
@@ -48396,6 +48496,8 @@ function listCommand(options) {
48396
48496
  parts.push("--category", quoteArg(options.category));
48397
48497
  if (options.tags)
48398
48498
  parts.push("--tags", quoteArg(options.tags));
48499
+ else if (options.tag)
48500
+ parts.push("--tag", quoteArg(options.tag));
48399
48501
  if (options.pinned)
48400
48502
  parts.push("--pinned");
48401
48503
  return parts.join(" ");
@@ -49100,7 +49202,8 @@ function handleDiff(name, options) {
49100
49202
  const canonical = {
49101
49203
  present: existsSync15(canonicalSkillMd),
49102
49204
  path: canonicalDir,
49103
- ...existsSync15(canonicalSkillMd) ? { hash: hashSkillMarkdownFile(canonicalSkillMd) } : {}
49205
+ ...existsSync15(canonicalSkillMd) ? { hash: hashSkillMarkdownFile(canonicalSkillMd) } : {},
49206
+ ...existsSync15(canonicalSkillMd) ? { stub: isPointerSkillMd(readFileSync12(canonicalSkillMd, "utf-8")) } : {}
49104
49207
  };
49105
49208
  const pinned = getInstalledSkills().includes(bare);
49106
49209
  const installMeta = getInstallMeta();
@@ -49120,6 +49223,14 @@ function handleDiff(name, options) {
49120
49223
  const managed = existsSync15(join15(dir, SYNC_MARKER_FILE));
49121
49224
  const skillMdPath = join15(dir, "SKILL.md");
49122
49225
  const hash = present && existsSync15(skillMdPath) ? hashSkillMarkdownFile(skillMdPath) : undefined;
49226
+ let stub;
49227
+ if (present && existsSync15(skillMdPath)) {
49228
+ try {
49229
+ stub = isPointerSkillMd(readFileSync12(skillMdPath, "utf-8"));
49230
+ } catch {
49231
+ stub = undefined;
49232
+ }
49233
+ }
49123
49234
  const diverged = present && canonical.present && managed && hash !== canonical.hash;
49124
49235
  homes.push({
49125
49236
  agent,
@@ -49127,6 +49238,7 @@ function handleDiff(name, options) {
49127
49238
  present,
49128
49239
  managed,
49129
49240
  ...hash ? { hash } : {},
49241
+ ...stub !== undefined ? { stub } : {},
49130
49242
  ...diverged !== undefined ? { diverged } : {}
49131
49243
  });
49132
49244
  }
@@ -49156,7 +49268,8 @@ function handleDiff(name, options) {
49156
49268
  } else if (!home.managed) {
49157
49269
  console.log(`${source_default.dim("\u2022")} ${home.agent}: ${source_default.yellow("unmarked (adoption candidate)")}`);
49158
49270
  } else if (home.diverged) {
49159
- console.log(`${source_default.red("\u2717")} ${home.agent}: ${source_default.red("diverged")} ${source_default.dim(home.hash?.slice(0, 12))} \u2260 ${source_default.dim(canonical.hash?.slice(0, 12))}`);
49271
+ const stubNote = home.stub === true ? " (home is a pointer stub; canonical holds content)" : canonical.stub === true ? " (home holds content; canonical renders a pointer stub \u2014 sync refuses to replace it)" : "";
49272
+ console.log(`${source_default.red("\u2717")} ${home.agent}: ${source_default.red("diverged")} ${source_default.dim(home.hash?.slice(0, 12))} \u2260 ${source_default.dim(canonical.hash?.slice(0, 12))}${stubNote}`);
49160
49273
  } else {
49161
49274
  console.log(`${source_default.green("\u2713")} ${home.agent}: ${source_default.green("matches canonical")}`);
49162
49275
  }
@@ -50085,7 +50198,7 @@ var init_home_adoption = __esm(() => {
50085
50198
  });
50086
50199
 
50087
50200
  // src/lib/home-census.ts
50088
- import { existsSync as existsSync18, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
50201
+ import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
50089
50202
  import { homedir as homedir7 } from "os";
50090
50203
  import { join as join18 } from "path";
50091
50204
  function sortEntries(entries) {
@@ -50145,7 +50258,20 @@ function censusHomeDrift(options = {}) {
50145
50258
  }
50146
50259
  const homeHash = hashSkillMarkdownFile(skillMdPath);
50147
50260
  if (homeHash !== canonicalHash) {
50148
- entries.push({ agent, skill, kind: "diverged", path: dir, homeHash, canonicalHash });
50261
+ let homeStub;
50262
+ try {
50263
+ homeStub = isPointerSkillMd(readFileSync15(skillMdPath, "utf-8"));
50264
+ } catch {
50265
+ homeStub = undefined;
50266
+ }
50267
+ let canonicalStub;
50268
+ const canonicalSkillMd = join18(corpusRoot, skill, "SKILL.md");
50269
+ try {
50270
+ canonicalStub = isPointerSkillMd(readFileSync15(canonicalSkillMd, "utf-8"));
50271
+ } catch {
50272
+ canonicalStub = undefined;
50273
+ }
50274
+ entries.push({ agent, skill, kind: "diverged", path: dir, homeHash, canonicalHash, homeStub, canonicalStub });
50149
50275
  }
50150
50276
  }
50151
50277
  for (const [name, canonicalHash] of index) {
@@ -50180,7 +50306,7 @@ var exports_diagnostic = {};
50180
50306
  __export(exports_diagnostic, {
50181
50307
  registerDiagnostic: () => registerDiagnostic
50182
50308
  });
50183
- import { existsSync as existsSync19, readFileSync as readFileSync15, readdirSync as readdirSync10, statSync as statSync10, writeFileSync as writeFileSync10 } from "fs";
50309
+ import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync10, writeFileSync as writeFileSync10 } from "fs";
50184
50310
  import { join as join19 } from "path";
50185
50311
  import { execSync as execSync2 } from "child_process";
50186
50312
  function registerDiagnostic(parent) {
@@ -50319,7 +50445,7 @@ function handleAuth(name, options) {
50319
50445
  process.exitCode = 1;
50320
50446
  return;
50321
50447
  }
50322
- let existing = existsSync19(envFilePath) ? readFileSync15(envFilePath, "utf-8") : "";
50448
+ let existing = existsSync19(envFilePath) ? readFileSync16(envFilePath, "utf-8") : "";
50323
50449
  const keyPattern = new RegExp(`^${key}=.*$`, "m");
50324
50450
  const updated = keyPattern.test(existing) ? existing.replace(keyPattern, `${key}=${value}`) : existing.endsWith(`
50325
50451
  `) || existing === "" ? existing + `${key}=${value}
@@ -50427,7 +50553,7 @@ function handleOutdated(options) {
50427
50553
  let registryVersion = "unknown";
50428
50554
  if (existsSync19(registryPkgPath))
50429
50555
  try {
50430
- registryVersion = JSON.parse(readFileSync15(registryPkgPath, "utf-8")).version || "unknown";
50556
+ registryVersion = JSON.parse(readFileSync16(registryPkgPath, "utf-8")).version || "unknown";
50431
50557
  } catch {}
50432
50558
  if (installedVersion !== registryVersion)
50433
50559
  pins.push({ skill: name, installedVersion, registryVersion });
@@ -50691,7 +50817,7 @@ var init_runs = __esm(() => {
50691
50817
 
50692
50818
  // src/lib/run-state.ts
50693
50819
  import { createHash as createHash2, randomBytes } from "crypto";
50694
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync16, readdirSync as readdirSync11, statSync as statSync11, writeFileSync as writeFileSync11 } from "fs";
50820
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync11, writeFileSync as writeFileSync11 } from "fs";
50695
50821
  import { extname, join as join20, relative as relative2 } from "path";
50696
50822
  function createSkillRun(params, targetDir = process.cwd()) {
50697
50823
  const now3 = new Date;
@@ -50762,7 +50888,7 @@ function appendRunEvent(context, event, data = {}) {
50762
50888
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
50763
50889
  `;
50764
50890
  const path = join20(context.runDir, "events.ndjson");
50765
- const previous = existsSync20(path) ? readFileSync16(path, "utf-8") : "";
50891
+ const previous = existsSync20(path) ? readFileSync17(path, "utf-8") : "";
50766
50892
  writeFileSync11(path, previous + line);
50767
50893
  }
50768
50894
  function listSkillRuns(targetDir = process.cwd(), limit = 50) {
@@ -50822,7 +50948,7 @@ function collectRunArtifacts(context) {
50822
50948
  const artifacts = [];
50823
50949
  for (const path of walkFiles(context.exportDir)) {
50824
50950
  const stat = statSync11(path);
50825
- const bytes = readFileSync16(path);
50951
+ const bytes = readFileSync17(path);
50826
50952
  artifacts.push({
50827
50953
  path: toProjectRelative(context.targetDir, path),
50828
50954
  mime: mimeForPath(path),
@@ -50837,7 +50963,7 @@ function readRunRecord(runDir) {
50837
50963
  if (!existsSync20(path))
50838
50964
  return null;
50839
50965
  try {
50840
- return JSON.parse(readFileSync16(path, "utf-8"));
50966
+ return JSON.parse(readFileSync17(path, "utf-8"));
50841
50967
  } catch {
50842
50968
  return null;
50843
50969
  }
@@ -68063,8 +68189,11 @@ var init_discovery_tools = __esm(() => {
68063
68189
  // src/lib/remote-client.ts
68064
68190
  var exports_remote_client = {};
68065
68191
  __export(exports_remote_client, {
68192
+ createRemoteSkillsClientReadOnly: () => createRemoteSkillsClientReadOnly,
68066
68193
  createRemoteSkillsClient: () => createRemoteSkillsClient,
68067
- RemoteSkillsClient: () => RemoteSkillsClient
68194
+ RemoteSkillsClient: () => RemoteSkillsClient,
68195
+ RemoteRouteUnsupportedError: () => RemoteRouteUnsupportedError,
68196
+ RemoteRequestError: () => RemoteRequestError
68068
68197
  });
68069
68198
 
68070
68199
  class RemoteSkillsClient {
@@ -68084,6 +68213,20 @@ class RemoteSkillsClient {
68084
68213
  }
68085
68214
  });
68086
68215
  }
68216
+ async requestNewRoute(path, options, opts = {}) {
68217
+ const response = await this.request(path, options);
68218
+ const routePath = path.split("?")[0];
68219
+ if (response.status === 404 || response.status === 405) {
68220
+ if (response.status === 404 && opts.domainNotFoundCodes?.length && await responseBodyCarriesCode(response, opts.domainNotFoundCodes)) {
68221
+ return response;
68222
+ }
68223
+ throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
68224
+ }
68225
+ if (!response.ok) {
68226
+ throw new RemoteRequestError(routePath, response.status, response.statusText);
68227
+ }
68228
+ return response;
68229
+ }
68087
68230
  async listSkills() {
68088
68231
  const res = await this.request("/api/v1/skills");
68089
68232
  return res.json();
@@ -68100,6 +68243,14 @@ class RemoteSkillsClient {
68100
68243
  return null;
68101
68244
  return res.json();
68102
68245
  }
68246
+ async getSkillStatus(slug) {
68247
+ const res = await this.request(`/api/v1/skills/${encodeURIComponent(slug)}`, { method: "GET" });
68248
+ let body = null;
68249
+ try {
68250
+ body = await res.json();
68251
+ } catch {}
68252
+ return { status: res.status, body };
68253
+ }
68103
68254
  async submitRun(slug, input, args) {
68104
68255
  const res = await this.request(`/api/v1/runs/${slug}`, {
68105
68256
  method: "POST",
@@ -68133,15 +68284,18 @@ class RemoteSkillsClient {
68133
68284
  method: "GET"
68134
68285
  });
68135
68286
  }
68136
- async publishSkill(manifest, bundle) {
68287
+ async publishSkill(manifest, bundle, ifMatch) {
68137
68288
  const form = new FormData;
68138
68289
  form.set("manifest", JSON.stringify(manifest));
68139
68290
  if (bundle) {
68140
68291
  form.set("bundle", new Blob([bundle], { type: "application/gzip" }), `${String(manifest.slug ?? "skill")}.tar.gz`);
68141
68292
  }
68293
+ const headers = { Authorization: `Bearer ${this.apiKey}` };
68294
+ if (ifMatch)
68295
+ headers["If-Match"] = ifMatch;
68142
68296
  return fetch(`${this.apiUrl}/api/v1/skills`, {
68143
68297
  method: "POST",
68144
- headers: { Authorization: `Bearer ${this.apiKey}` },
68298
+ headers,
68145
68299
  body: form
68146
68300
  });
68147
68301
  }
@@ -68157,6 +68311,135 @@ class RemoteSkillsClient {
68157
68311
  return null;
68158
68312
  return response;
68159
68313
  }
68314
+ async listPins() {
68315
+ const response = await this.requestNewRoute("/api/v1/pins");
68316
+ return normalizePinList(await response.json());
68317
+ }
68318
+ async pin(slug, metadata) {
68319
+ const path = `/api/v1/pins/${encodeURIComponent(slug)}`;
68320
+ const response = await this.requestNewRoute(path, {
68321
+ method: "PUT",
68322
+ body: JSON.stringify({ ...metadata ? { metadata } : {} })
68323
+ });
68324
+ return normalizePin(await response.json());
68325
+ }
68326
+ async unpin(slug) {
68327
+ const path = `/api/v1/pins/${encodeURIComponent(slug)}`;
68328
+ const response = await this.requestNewRoute(path, { method: "DELETE" }, { domainNotFoundCodes: ["PIN_NOT_FOUND"] });
68329
+ return response.status !== 404;
68330
+ }
68331
+ async listTags() {
68332
+ const response = await this.requestNewRoute("/api/v1/tags");
68333
+ const payload = await response.json();
68334
+ if (!Array.isArray(payload)) {
68335
+ throw new Error("Remote tags payload did not match the expected contract (expected an array of tag names)");
68336
+ }
68337
+ for (const tag of payload) {
68338
+ if (typeof tag !== "string" || tag.trim().length === 0) {
68339
+ throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name)");
68340
+ }
68341
+ }
68342
+ return payload;
68343
+ }
68344
+ async skillsByTag(tag) {
68345
+ const path = `/api/v1/tags/${encodeURIComponent(tag)}/skills`;
68346
+ const response = await this.requestNewRoute(path);
68347
+ return normalizeSkillSummaryList(await response.json());
68348
+ }
68349
+ async listUpdatedSince(since, options = {}) {
68350
+ const params = new URLSearchParams({ since });
68351
+ if (options.cursor)
68352
+ params.set("cursor", options.cursor);
68353
+ if (options.limit !== undefined)
68354
+ params.set("limit", String(options.limit));
68355
+ const response = await this.requestNewRoute(`/api/v1/skills/updated?${params.toString()}`);
68356
+ return normalizeUpdatedSincePage(await response.json());
68357
+ }
68358
+ }
68359
+ function requireOptionalString(record3, field) {
68360
+ if (record3[field] === undefined)
68361
+ return;
68362
+ if (typeof record3[field] !== "string") {
68363
+ throw new Error(`Remote payload did not match the expected contract (${field} must be a string when present)`);
68364
+ }
68365
+ return record3[field];
68366
+ }
68367
+ function normalizePin(entry) {
68368
+ if (!entry || typeof entry !== "object") {
68369
+ throw new Error("Remote pin payload did not match the expected contract (expected an object)");
68370
+ }
68371
+ const record3 = entry;
68372
+ const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
68373
+ if (!slug) {
68374
+ throw new Error("Remote pin payload did not match the expected contract (missing slug)");
68375
+ }
68376
+ let metadata;
68377
+ if (record3.metadata !== undefined) {
68378
+ if (!record3.metadata || typeof record3.metadata !== "object" || Array.isArray(record3.metadata)) {
68379
+ throw new Error("Remote pin payload did not match the expected contract (metadata must be a JSON object when present)");
68380
+ }
68381
+ metadata = record3.metadata;
68382
+ }
68383
+ const pinnedAt = requireOptionalString(record3, "pinnedAt");
68384
+ return {
68385
+ slug,
68386
+ ...pinnedAt !== undefined ? { pinnedAt } : {},
68387
+ ...metadata ? { metadata } : {}
68388
+ };
68389
+ }
68390
+ function normalizePinList(payload) {
68391
+ if (!Array.isArray(payload)) {
68392
+ throw new Error("Remote pins payload did not match the expected contract (expected an array of pins)");
68393
+ }
68394
+ return payload.map(normalizePin);
68395
+ }
68396
+ function normalizeSkillSummary(entry) {
68397
+ if (!entry || typeof entry !== "object") {
68398
+ throw new Error("Remote skill payload did not match the expected contract (expected an object)");
68399
+ }
68400
+ const record3 = entry;
68401
+ const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
68402
+ if (!slug) {
68403
+ throw new Error("Remote skill payload did not match the expected contract (missing slug)");
68404
+ }
68405
+ return {
68406
+ slug,
68407
+ ...requireOptionalString(record3, "name") !== undefined ? { name: requireOptionalString(record3, "name") } : {},
68408
+ ...requireOptionalString(record3, "version") !== undefined ? { version: requireOptionalString(record3, "version") } : {},
68409
+ ...requireOptionalString(record3, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record3, "updatedAt") } : {}
68410
+ };
68411
+ }
68412
+ function normalizeSkillSummaryList(payload) {
68413
+ if (!Array.isArray(payload)) {
68414
+ throw new Error("Remote skills payload did not match the expected contract (expected an array of skills)");
68415
+ }
68416
+ return payload.map(normalizeSkillSummary);
68417
+ }
68418
+ async function responseBodyCarriesCode(response, codes) {
68419
+ try {
68420
+ const payload = await response.clone().json();
68421
+ if (!payload || typeof payload !== "object")
68422
+ return false;
68423
+ const code = payload.code;
68424
+ return typeof code === "string" && codes.includes(code);
68425
+ } catch {
68426
+ return false;
68427
+ }
68428
+ }
68429
+ function normalizeUpdatedSincePage(payload) {
68430
+ if (!payload || typeof payload !== "object") {
68431
+ throw new Error("Updated-since payload did not match the expected contract (expected an object)");
68432
+ }
68433
+ const record3 = payload;
68434
+ if (!Array.isArray(record3.skills)) {
68435
+ throw new Error("Updated-since payload did not match the expected contract (missing skills array)");
68436
+ }
68437
+ const skills = record3.skills.map(normalizeSkillSummary);
68438
+ const nextCursor = record3.nextCursor === undefined || record3.nextCursor === null ? null : record3.nextCursor;
68439
+ if (nextCursor !== null && typeof nextCursor !== "string") {
68440
+ throw new Error("Updated-since payload did not match the expected contract (nextCursor must be a string or absent)");
68441
+ }
68442
+ return { skills, nextCursor };
68160
68443
  }
68161
68444
  function createRemoteSkillsClient() {
68162
68445
  const apiKey = getApiKey();
@@ -68164,8 +68447,42 @@ function createRemoteSkillsClient() {
68164
68447
  return null;
68165
68448
  return new RemoteSkillsClient(apiKey);
68166
68449
  }
68450
+ function createRemoteSkillsClientReadOnly() {
68451
+ const apiKey = getApiKeyReadOnly();
68452
+ if (!apiKey)
68453
+ return null;
68454
+ const apiUrl = resolveApiUrl(loadConfigReadOnly(), process.env);
68455
+ if (!apiUrl)
68456
+ throw new MissingApiUrlError("the cloud group's sync verb (--dry-run)");
68457
+ return new RemoteSkillsClient(apiKey, apiUrl);
68458
+ }
68459
+ var RemoteRouteUnsupportedError, RemoteRequestError;
68167
68460
  var init_remote_client = __esm(() => {
68168
68461
  init_auth_store();
68462
+ init_api_url();
68463
+ init_config();
68464
+ RemoteRouteUnsupportedError = class RemoteRouteUnsupportedError extends Error {
68465
+ path;
68466
+ status;
68467
+ instance;
68468
+ constructor(path, status, instance) {
68469
+ super(`The configured Skills instance does not support ${path} (HTTP ${status}). ` + `The instance at ${instance} predates this client feature \u2014 upgrade the server, or ` + `use a client version that matches it.`);
68470
+ this.path = path;
68471
+ this.status = status;
68472
+ this.instance = instance;
68473
+ this.name = "RemoteRouteUnsupportedError";
68474
+ }
68475
+ };
68476
+ RemoteRequestError = class RemoteRequestError extends Error {
68477
+ path;
68478
+ status;
68479
+ constructor(path, status, statusText) {
68480
+ super(`Remote request to ${path} failed: HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
68481
+ this.path = path;
68482
+ this.status = status;
68483
+ this.name = "RemoteRequestError";
68484
+ }
68485
+ };
68169
68486
  });
68170
68487
 
68171
68488
  // src/mcp/operation-tools.ts
@@ -68630,14 +68947,14 @@ var init_operation_tools = __esm(() => {
68630
68947
 
68631
68948
  // src/lib/feedback.ts
68632
68949
  import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
68633
- import { dirname as dirname6, join as join22 } from "path";
68950
+ import { dirname as dirname7, join as join22 } from "path";
68634
68951
  import { Database } from "bun:sqlite";
68635
68952
  function getFeedbackDbPath() {
68636
68953
  return join22(getDataDir(), "skills.db");
68637
68954
  }
68638
68955
  function getFeedbackDb() {
68639
68956
  const dbPath = getFeedbackDbPath();
68640
- const dir = dirname6(dbPath);
68957
+ const dir = dirname7(dbPath);
68641
68958
  if (!existsSync22(dir))
68642
68959
  mkdirSync10(dir, { recursive: true });
68643
68960
  const db = new Database(dbPath);
@@ -68812,7 +69129,7 @@ var init_resource_meta_tools = __esm(() => {
68812
69129
  });
68813
69130
 
68814
69131
  // src/lib/scheduler.ts
68815
- import { existsSync as existsSync23, readFileSync as readFileSync17, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
69132
+ import { existsSync as existsSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
68816
69133
  import { join as join23 } from "path";
68817
69134
  function getSchedulesPath(targetDir = process.cwd()) {
68818
69135
  return join23(targetDir, ".skills", "schedules.json");
@@ -68821,7 +69138,7 @@ function loadSchedules(targetDir = process.cwd()) {
68821
69138
  const path = getSchedulesPath(targetDir);
68822
69139
  if (existsSync23(path)) {
68823
69140
  try {
68824
- return JSON.parse(readFileSync17(path, "utf-8"));
69141
+ return JSON.parse(readFileSync18(path, "utf-8"));
68825
69142
  } catch {}
68826
69143
  }
68827
69144
  return { version: 1, schedules: [] };
@@ -69131,12 +69448,12 @@ import { createHash as createHash3, createHmac as createHmac2 } from "crypto";
69131
69448
  import {
69132
69449
  existsSync as existsSync24,
69133
69450
  mkdirSync as mkdirSync12,
69134
- readFileSync as readFileSync18,
69451
+ readFileSync as readFileSync19,
69135
69452
  readdirSync as readdirSync13,
69136
69453
  statSync as statSync13,
69137
69454
  writeFileSync as writeFileSync13
69138
69455
  } from "fs";
69139
- import { dirname as dirname7, join as join24, normalize as normalize3, relative as relative3, sep as sep2 } from "path";
69456
+ import { dirname as dirname8, join as join24, normalize as normalize3, relative as relative3, sep as sep2 } from "path";
69140
69457
  function resolveSkillsNativeStorageConfig(env3 = process.env) {
69141
69458
  assertNoRetiredModeEnvVars(env3, {
69142
69459
  app: SKILLS_ENV_NAMESPACE,
@@ -69168,7 +69485,7 @@ function getSkillsNativeStorageStatus(options = {}) {
69168
69485
  const s3BucketEnv = readStorageEnv(env3, "s3Bucket");
69169
69486
  const targetDir = options.targetDir ?? process.cwd();
69170
69487
  return {
69171
- package: "open-skills",
69488
+ package: "skills",
69172
69489
  tables: [...SKILLS_STORAGE_TABLES],
69173
69490
  env: {
69174
69491
  databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
@@ -69199,7 +69516,7 @@ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
69199
69516
  const files = [];
69200
69517
  if (existsSync24(projectStateDir)) {
69201
69518
  for (const filePath of walkFiles2(projectStateDir)) {
69202
- const bytes = readFileSync18(filePath);
69519
+ const bytes = readFileSync19(filePath);
69203
69520
  const relativePath = toPosix(relative3(targetDir, filePath));
69204
69521
  files.push({
69205
69522
  path: relativePath,
@@ -69361,7 +69678,7 @@ function registerStorageTools(server) {
69361
69678
  const snapshot = exportSkillsLocalSnapshot(targetDir, { includeFileContents: false });
69362
69679
  const s3Plan = config2.s3Bucket ? planSkillsS3SnapshotUpload(snapshot, { prefix: config2.s3Prefix }) : [];
69363
69680
  return mcpJson({
69364
- package: "open-skills",
69681
+ package: "skills",
69365
69682
  noNetwork: true,
69366
69683
  databaseConfigured: Boolean(config2.databaseUrl),
69367
69684
  s3Configured: Boolean(config2.s3Bucket),
@@ -69802,9 +70119,9 @@ var init_mcp2 = __esm(() => {
69802
70119
  });
69803
70120
 
69804
70121
  // src/cli/commands/runtime-mcp.ts
69805
- import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
70122
+ import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
69806
70123
  import { homedir as homedir8 } from "os";
69807
- import { dirname as dirname8, join as join25 } from "path";
70124
+ import { dirname as dirname9, join as join25 } from "path";
69808
70125
  async function handleMcp(options) {
69809
70126
  if (options.register) {
69810
70127
  let agents;
@@ -69912,7 +70229,7 @@ function registerCodexMcp(command) {
69912
70229
  const config2 = `[mcp_servers.${MCP_SERVER_NAME}]
69913
70230
  command = ${JSON.stringify(command)}`;
69914
70231
  try {
69915
- const current = existsSync25(path) ? readFileSync19(path, "utf-8") : "";
70232
+ const current = existsSync25(path) ? readFileSync20(path, "utf-8") : "";
69916
70233
  writeTextFile(path, upsertTomlSection(current, `[mcp_servers.${MCP_SERVER_NAME}]`, `command = ${JSON.stringify(command)}`));
69917
70234
  return { agent: "codex", success: true, path, config: config2 };
69918
70235
  } catch (err) {
@@ -69964,7 +70281,7 @@ function registerJsonMcpServer(agent, path, containerKey, server2) {
69964
70281
  function readJsonObject2(path) {
69965
70282
  if (!existsSync25(path))
69966
70283
  return {};
69967
- const raw = readFileSync19(path, "utf-8").trim();
70284
+ const raw = readFileSync20(path, "utf-8").trim();
69968
70285
  if (!raw)
69969
70286
  return {};
69970
70287
  const parsed = JSON.parse(raw);
@@ -69977,7 +70294,7 @@ function writeJsonObject(path, data) {
69977
70294
  `);
69978
70295
  }
69979
70296
  function writeTextFile(path, content) {
69980
- mkdirSync13(dirname8(path), { recursive: true });
70297
+ mkdirSync13(dirname9(path), { recursive: true });
69981
70298
  writeFileSync14(path, content.endsWith(`
69982
70299
  `) ? content : `${content}
69983
70300
  `);
@@ -70029,7 +70346,7 @@ __export(exports_runtime, {
70029
70346
  registerRuntime: () => registerRuntime
70030
70347
  });
70031
70348
  import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync15 } from "fs";
70032
- import { dirname as dirname9, join as join26 } from "path";
70349
+ import { dirname as dirname10, join as join26 } from "path";
70033
70350
  import { createInterface } from "readline";
70034
70351
  function registerRuntime(parent) {
70035
70352
  parent.command("run").argument("<skill>", "Skill name").argument("[args...]", "Arguments to pass to the skill").allowUnknownOption(true).passThroughOptions(true).option("--json", "Output result as JSON", false).option("--wait", "Poll remote runs until a terminal status", false).option("--poll-interval-ms <ms>", "Remote polling interval in milliseconds", "1000").option("--poll-timeout-ms <ms>", "Maximum time to wait for a remote run", "300000").description("Run a skill directly").action(async (name, args2, options) => handleRun(name, args2, options));
@@ -70514,7 +70831,7 @@ async function handleExportsDownload(runId, options) {
70514
70831
  throw new Error(`download failed for artifact ${artifactId}: ${response.status}`);
70515
70832
  const relativePath = safeArtifactRelativePath(typeof artifact.relativePath === "string" ? artifact.relativePath : artifact.fileName, String(artifact.fileName || artifactId));
70516
70833
  const outputPath = join26(exportDir, relativePath);
70517
- mkdirSync14(dirname9(outputPath), { recursive: true });
70834
+ mkdirSync14(dirname10(outputPath), { recursive: true });
70518
70835
  const bytes = new Uint8Array(await response.arrayBuffer());
70519
70836
  writeFileSync15(outputPath, bytes);
70520
70837
  downloaded.push({ id: artifactId, path: outputPath, byteSize: bytes.byteLength });
@@ -70954,7 +71271,7 @@ function registerCreateSync(parent) {
70954
71271
  console.log(`${source_default.cyan("project")}: ${pp}${existsSync26(pp) ? source_default.green(" (exists)") : source_default.dim(" (not found)")}`);
70955
71272
  });
70956
71273
  parent.command("create").argument("<name>", "Skill name (e.g. my-tool)").option("--category <category>", "Skill category", "Development Tools").option("--description <description>", "Short description of what the skill does").option("--tags <tags>", "Comma-separated tags (e.g. api,testing,automation)").option("--global", "Deprecated; custom skills are always global", false).option("--json", "Output result as JSON", false).description("Scaffold a new custom skill directory").action((name, options) => handleCreate(name, options));
70957
- parent.command("sync").alias("render").argument("[names...]", "Skills to sync (default: every skill in this machine's corpus)").option("--for <agent>", `Target one agent (${SYNC_AGENTS.join(", ")}, or all)`, "all").option("--all", "Sync every corpus skill (the default)", false).option("--source <path>", "Canonical corpus source: a directory of skill folders, or a package root with skills/ and agent-skills/ (overrides $SKILLS_SOURCE)").option("--dry-run", "Show what would be written without touching any agent folder", false).option("--force", "Adopt an unmanaged skill that already has SKILL.md; other unmarked directories are never overwritten", false).option("--check", "Home drift census (missing-from-home / stray-in-home / diverged); exits non-zero on drift, writes nothing", false).option("--adopt", "Unmarked-home adoption mode: hash unmarked home skills against the corpus; exact matches are marked (dry-run by default)", false).option("--prune", "Prune mode: list (or with --apply, remove) marked home skill dirs that have no canonical corpus entry", false).option("--apply", "Write adoption markers / conflicts ledger, or perform prune removals", false).option("--json", "Output as JSON", false).description("Write corpus skills into each coding agent's global skills folder, per-tool adapted").action((names, options) => handleSync(names, options));
71274
+ parent.command("sync").alias("render").argument("[names...]", "Skills to sync (default: every skill in this machine's corpus)").option("--for <agent>", `Target one agent (${SYNC_AGENTS.join(", ")}, or all)`, "all").option("--all", "Sync every corpus skill (the default)", false).option("--source <path>", "Canonical corpus source: a directory of skill folders, or a package root with skills/ (overrides $SKILLS_SOURCE)").option("--dry-run", "Show what would be written without touching any agent folder", false).option("--force", "Adopt an unmanaged skill that already has SKILL.md; other unmarked directories are never overwritten", false).option("--check", "Home drift census (missing-from-home / stray-in-home / diverged); exits non-zero on drift, writes nothing", false).option("--adopt", "Unmarked-home adoption mode: hash unmarked home skills against the corpus; exact matches are marked (dry-run by default)", false).option("--prune", "Prune mode: list (or with --apply, remove) marked home skill dirs that have no canonical corpus entry", false).option("--apply", "Write adoption markers / conflicts ledger, or perform prune removals", false).option("--json", "Output as JSON", false).description("Write corpus skills into each coding agent's global skills folder, per-tool adapted").action((names, options) => handleSync(names, options));
70958
71275
  }
70959
71276
  function handleCreate(name, options) {
70960
71277
  const bare = name.trim();
@@ -71139,7 +71456,12 @@ Home drift census: ${census.entries.length} drift entr${census.entries.length ==
71139
71456
  `));
71140
71457
  for (const entry of census.entries) {
71141
71458
  const kind = entry.kind === "missing-from-home" ? source_default.red("missing-from-home") : entry.kind === "stray-in-home" ? source_default.yellow("stray-in-home") : source_default.yellow("diverged");
71142
- console.log(` ${kind} ${source_default.bold(entry.skill)} \u2192 ${entry.agent} ${source_default.dim(entry.path)}`);
71459
+ let note = "";
71460
+ if (entry.homeStub === true)
71461
+ note = " (home is a pointer stub; canonical holds content)";
71462
+ else if (entry.canonicalStub === true)
71463
+ note = " (home holds content; canonical renders a pointer stub \u2014 sync refuses to replace it)";
71464
+ console.log(` ${kind} ${source_default.bold(entry.skill)} \u2192 ${entry.agent} ${source_default.dim(entry.path)}${note}`);
71143
71465
  }
71144
71466
  console.log(source_default.dim(`
71145
71467
  ${census.managed} managed, ${census.unmarked} unmarked (adoption candidates). Exit code is non-zero while drift exists.`));
@@ -71497,7 +71819,7 @@ var init_schedule = __esm(() => {
71497
71819
 
71498
71820
  // src/lib/registry-sync.ts
71499
71821
  import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
71500
- import { dirname as dirname10, relative as relative4 } from "path";
71822
+ import { dirname as dirname11, relative as relative4 } from "path";
71501
71823
  function createRegistrySyncArtifact(options = {}) {
71502
71824
  const profile = options.profile ?? "all";
71503
71825
  const includeDocs = options.includeDocs ?? true;
@@ -71554,7 +71876,7 @@ function createRegistrySyncArtifact(options = {}) {
71554
71876
  };
71555
71877
  }
71556
71878
  function writeRegistrySyncArtifact(path, artifact) {
71557
- mkdirSync16(dirname10(path), { recursive: true });
71879
+ mkdirSync16(dirname11(path), { recursive: true });
71558
71880
  writeFileSync17(path, `${JSON.stringify(artifact, null, 2)}
71559
71881
  `);
71560
71882
  }
@@ -71573,9 +71895,32 @@ var init_registry_sync = __esm(() => {
71573
71895
  init_skill_validation();
71574
71896
  });
71575
71897
 
71576
- // src/lib/skill-bundle.ts
71898
+ // src/lib/revision.ts
71577
71899
  import { createHash as createHash4 } from "crypto";
71578
- import { readFileSync as readFileSync20, readdirSync as readdirSync14, statSync as statSync14 } from "fs";
71900
+ function revisionIdOf(content) {
71901
+ const canonical = JSON.stringify({
71902
+ slug: content.slug,
71903
+ displayName: content.displayName,
71904
+ description: content.description,
71905
+ category: content.category,
71906
+ tags: content.tags,
71907
+ source: content.source,
71908
+ kind: content.kind,
71909
+ version: content.version ?? null,
71910
+ skillMd: content.skillMd ?? null,
71911
+ bundleSha256: content.bundleSha256 ?? null,
71912
+ bundleByteSize: content.bundleByteSize ?? null
71913
+ });
71914
+ return createHash4("sha256").update(canonical).digest("hex");
71915
+ }
71916
+ var REVISION_ID_PATTERN;
71917
+ var init_revision = __esm(() => {
71918
+ REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
71919
+ });
71920
+
71921
+ // src/lib/skill-bundle.ts
71922
+ import { createHash as createHash5 } from "crypto";
71923
+ import { readFileSync as readFileSync21, readdirSync as readdirSync14, statSync as statSync14 } from "fs";
71579
71924
  import { join as join28, relative as relative5 } from "path";
71580
71925
  function isDotenvFile(lower) {
71581
71926
  if (lower === ".env" || lower.startsWith(".env."))
@@ -71605,7 +71950,7 @@ function ownBytes(view) {
71605
71950
  return out;
71606
71951
  }
71607
71952
  function sha256Hex(bytes) {
71608
- return createHash4("sha256").update(bytes).digest("hex");
71953
+ return createHash5("sha256").update(bytes).digest("hex");
71609
71954
  }
71610
71955
  function collectSkillBundleEntries(dir) {
71611
71956
  const entries = [];
@@ -71621,6 +71966,8 @@ function walk(root, current, out) {
71621
71966
  continue;
71622
71967
  if (isRootLevel && ROOT_EXCLUDES.has(entry.name.toLowerCase()))
71623
71968
  continue;
71969
+ if (isRootLevel && TOOL_SIDECAR_FILENAMES.has(entry.name.toLowerCase()))
71970
+ continue;
71624
71971
  if (entry.name.startsWith("._"))
71625
71972
  continue;
71626
71973
  if (entry.isSymbolicLink())
@@ -71636,7 +71983,7 @@ function walk(root, current, out) {
71636
71983
  const stats = statSync14(absolute);
71637
71984
  out.push({
71638
71985
  path: rel,
71639
- bytes: ownBytes(readFileSync20(absolute)),
71986
+ bytes: ownBytes(readFileSync21(absolute)),
71640
71987
  mode: stats.mode & 64 ? 493 : 420
71641
71988
  });
71642
71989
  }
@@ -71774,7 +72121,7 @@ function concat2(chunks) {
71774
72121
  }
71775
72122
  return merged;
71776
72123
  }
71777
- var BLOCK = 512, ANY_SEGMENT_EXCLUDES, ROOT_EXCLUDES, CREDENTIAL_FILENAMES, CREDENTIAL_EXTENSIONS, ENV_TEMPLATE_NAMES, NON_DOTENV_EXTENSIONS;
72124
+ var BLOCK = 512, ANY_SEGMENT_EXCLUDES, ROOT_EXCLUDES, TOOL_SIDECAR_FILENAMES, CREDENTIAL_FILENAMES, CREDENTIAL_EXTENSIONS, ENV_TEMPLATE_NAMES, NON_DOTENV_EXTENSIONS;
71778
72125
  var init_skill_bundle = __esm(() => {
71779
72126
  ANY_SEGMENT_EXCLUDES = new Set([
71780
72127
  ".git",
@@ -71787,6 +72134,7 @@ var init_skill_bundle = __esm(() => {
71787
72134
  ".docker"
71788
72135
  ]);
71789
72136
  ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
72137
+ TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
71790
72138
  CREDENTIAL_FILENAMES = new Set([
71791
72139
  ".npmrc",
71792
72140
  ".pypirc",
@@ -71921,8 +72269,8 @@ var init_skill_bundles = __esm(() => {
71921
72269
  });
71922
72270
 
71923
72271
  // src/lib/pull.ts
71924
- import { existsSync as existsSync27, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
71925
- import { dirname as dirname11, join as join29 } from "path";
72272
+ import { existsSync as existsSync27, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
72273
+ import { dirname as dirname12, join as join29 } from "path";
71926
72274
  async function pullSkills(options = {}) {
71927
72275
  const client = options.client !== undefined ? options.client : createRemoteSkillsClient();
71928
72276
  if (!client) {
@@ -71963,6 +72311,15 @@ async function pullOne(client, rawName, corpusOptions, verify) {
71963
72311
  return { name: slug, success: false, error: `Failed to fetch '${slug}': ${error2.message}` };
71964
72312
  }
71965
72313
  if (bundleResponse && !bundleResponse.ok) {
72314
+ if (bundleResponse.status === 410) {
72315
+ return reconcileTombstone(slug, corpusOptions);
72316
+ }
72317
+ if (bundleResponse.status === 404) {
72318
+ const marker = readPullMarker(join29(getPortableSkillsRoot(corpusOptions), slug));
72319
+ if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
72320
+ return { name: slug, success: true, purged: true, removed: false };
72321
+ }
72322
+ }
71966
72323
  return {
71967
72324
  name: slug,
71968
72325
  success: false,
@@ -71988,17 +72345,78 @@ async function pullOne(client, rawName, corpusOptions, verify) {
71988
72345
  if (skillMd === null) {
71989
72346
  return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
71990
72347
  }
72348
+ if (!meta?.revisionId) {
72349
+ const marker = readPullMarker(join29(getPortableSkillsRoot(corpusOptions), slug));
72350
+ if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
72351
+ return { name: slug, success: true, purged: true, removed: false };
72352
+ }
72353
+ }
72354
+ let provenRevisionId;
72355
+ try {
72356
+ provenRevisionId = meta?.revisionId ? provenRevision({ ...meta, skillMd }, slug, {}) : undefined;
72357
+ } catch (error2) {
72358
+ if (error2 instanceof PullSkillError) {
72359
+ return { name: slug, success: false, error: error2.message };
72360
+ }
72361
+ throw error2;
72362
+ }
71991
72363
  const written = writeCorpusSkill({ name: slug, skillMd, meta }, corpusOptions);
71992
- writePullMarker(written.path, { skill: slug, version: written.manifest.version });
72364
+ writePullMarker(written.path, {
72365
+ skill: slug,
72366
+ version: written.manifest.version,
72367
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {}
72368
+ });
71993
72369
  return {
71994
72370
  name: slug,
71995
72371
  success: true,
71996
72372
  path: written.path,
71997
72373
  kind: written.manifest.kind,
71998
72374
  version: written.manifest.version,
71999
- created: written.created
72375
+ created: written.created,
72376
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {}
72000
72377
  };
72001
72378
  }
72379
+ function provenRevision(meta, slug, bundle) {
72380
+ const declared = meta.revisionId;
72381
+ if (!declared)
72382
+ return "";
72383
+ const source = meta.publishedSource;
72384
+ if (typeof source !== "string" || source.length === 0) {
72385
+ throw new PullSkillError(`Revision proof failed for '${slug}': the instance declared revision '${declared.slice(0, 12)}\u2026' but did not serve the content fields needed to recompute it (publishedSource). Nothing was installed.`);
72386
+ }
72387
+ const recomputed = revisionIdOf({
72388
+ slug,
72389
+ displayName: meta.displayName ?? "",
72390
+ description: meta.description ?? "",
72391
+ category: meta.category ?? "",
72392
+ tags: meta.tags ?? [],
72393
+ source,
72394
+ kind: meta.kind ?? "instruction",
72395
+ ...meta.version ? { version: meta.version } : {},
72396
+ ...typeof meta.skillMd === "string" && meta.skillMd.length > 0 ? { skillMd: meta.skillMd } : {},
72397
+ ...bundle.sha256 ? { bundleSha256: bundle.sha256 } : {},
72398
+ ...bundle.byteSize !== undefined && bundle.byteSize !== null ? { bundleByteSize: bundle.byteSize } : {}
72399
+ });
72400
+ if (recomputed !== declared) {
72401
+ throw new PullSkillError(`Revision proof failed for '${slug}': the instance declared revision '${declared.slice(0, 12)}\u2026' but the served content recomputes to '${recomputed.slice(0, 12)}\u2026'. The declared revision does not identify the content that was received. Nothing was installed.`);
72402
+ }
72403
+ return declared;
72404
+ }
72405
+ function reconcileTombstone(slug, corpusOptions) {
72406
+ const target = join29(getPortableSkillsRoot(corpusOptions), slug);
72407
+ if (!existsSync27(join29(target, PULL_MARKER_FILE))) {
72408
+ return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
72409
+ }
72410
+ rmSync6(target, { recursive: true, force: true });
72411
+ return { name: slug, success: true, tombstoned: true, removed: true };
72412
+ }
72413
+ function readPullMarker(dir) {
72414
+ try {
72415
+ return JSON.parse(readFileSync22(join29(dir, PULL_MARKER_FILE), "utf-8"));
72416
+ } catch {
72417
+ return null;
72418
+ }
72419
+ }
72002
72420
  function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
72003
72421
  return response.arrayBuffer().then((buffer) => {
72004
72422
  const verified = verifyBundleResponseBytes(buffer, response, verify);
@@ -72010,11 +72428,17 @@ function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
72010
72428
  }
72011
72429
  const version2 = str(meta?.version) ?? versionFromEntries(entries) ?? "unknown";
72012
72430
  const sourceCommit = sourceCommitFromEntries(entries);
72431
+ const declaredRevision = verified.revisionId ?? meta?.revisionId;
72432
+ if (declaredRevision && meta?.revisionId && verified.revisionId && declaredRevision !== meta.revisionId) {
72433
+ throw new PullSkillError(`Revision proof failed for '${slug}': the bundle header declares '${verified.revisionId.slice(0, 12)}\u2026' but the metadata declares '${meta.revisionId.slice(0, 12)}\u2026'. The instance is inconsistent. Nothing was installed.`);
72434
+ }
72435
+ const provenRevisionId = declaredRevision ? provenRevision(meta ?? {}, slug, { sha256: verified.contentHash, byteSize: verified.bytes.byteLength }) : undefined;
72013
72436
  const installed = installBundleAtomically(slug, entries, corpusOptions, {
72014
72437
  version: version2,
72015
72438
  contentHash: verified.contentHash,
72016
72439
  ...sourceCommit ? { sourceCommit } : {},
72017
- ...verified.signature ? { signature: verified.signature } : {}
72440
+ ...verified.signature ? { signature: verified.signature } : {},
72441
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {}
72018
72442
  });
72019
72443
  return {
72020
72444
  name: slug,
@@ -72024,6 +72448,7 @@ function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
72024
72448
  version: version2,
72025
72449
  contentHash: verified.contentHash,
72026
72450
  ...sourceCommit ? { sourceCommit } : {},
72451
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {},
72027
72452
  created: installed.created
72028
72453
  };
72029
72454
  });
@@ -72074,11 +72499,20 @@ function sourceCommitFromEntries(entries) {
72074
72499
  function verifyBundleResponseBytes(buffer, response, verify = {}) {
72075
72500
  const serverHash = response.headers.get(BUNDLE_DIGEST_HEADER);
72076
72501
  const signature = response.headers.get(BUNDLE_SIGNATURE_HEADER);
72502
+ const revisionId = response.headers.get(BUNDLE_REVISION_ID_HEADER);
72503
+ const revisionNumberRaw = response.headers.get(BUNDLE_REVISION_NUMBER_HEADER);
72077
72504
  const bytes = new Uint8Array(buffer.slice(0));
72078
72505
  const contentHash = sha256Hex(bytes);
72079
72506
  if (serverHash && serverHash.toLowerCase() !== contentHash) {
72080
72507
  throw new PullSkillError(`Bundle digest mismatch: the instance declared ${serverHash} but the received bundle hashes to ${contentHash}.`, ["The bundle was tampered with or truncated in transit. Nothing was installed."]);
72081
72508
  }
72509
+ if (revisionId && !REVISION_ID_PATTERN.test(revisionId)) {
72510
+ throw new PullSkillError(`Malformed revision id: the instance declared '${revisionId}', which is not a 64-character lowercase hex sha-256.`, ["The revision headers were tampered with or the instance is broken. Nothing was installed."]);
72511
+ }
72512
+ const revisionNumber = revisionNumberRaw === null ? undefined : Number(revisionNumberRaw);
72513
+ if (revisionNumberRaw !== null && (!Number.isInteger(revisionNumber) || revisionNumber < 0)) {
72514
+ throw new PullSkillError(`Malformed revision number: the instance declared '${revisionNumberRaw}', which is not a non-negative integer.`, ["The revision headers were tampered with or the instance is broken. Nothing was installed."]);
72515
+ }
72082
72516
  if (signature) {
72083
72517
  const key = verify.signingKey;
72084
72518
  if (!key) {
@@ -72091,7 +72525,9 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
72091
72525
  bytes,
72092
72526
  contentHash,
72093
72527
  ...serverHash ? { serverHash } : {},
72094
- ...signature ? { signature } : {}
72528
+ ...signature ? { signature } : {},
72529
+ ...revisionId ? { revisionId } : {},
72530
+ ...revisionNumberRaw === null || revisionNumber === undefined ? {} : { revisionNumber }
72095
72531
  };
72096
72532
  }
72097
72533
  function installBundleAtomically(name, entries, options = {}, marker = {}) {
@@ -72105,7 +72541,7 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
72105
72541
  try {
72106
72542
  for (const entry of entries) {
72107
72543
  const destination = join29(staging, entry.path);
72108
- mkdirSync17(dirname11(destination), { recursive: true });
72544
+ mkdirSync17(dirname12(destination), { recursive: true });
72109
72545
  writeFileSync18(destination, entry.bytes, { mode: entry.mode });
72110
72546
  }
72111
72547
  writePullMarker(staging, {
@@ -72113,7 +72549,8 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
72113
72549
  ...marker.version ? { version: marker.version } : {},
72114
72550
  ...marker.contentHash ? { contentHash: marker.contentHash } : {},
72115
72551
  ...marker.sourceCommit ? { sourceCommit: marker.sourceCommit } : {},
72116
- ...marker.signature ? { signature: marker.signature } : {}
72552
+ ...marker.signature ? { signature: marker.signature } : {},
72553
+ ...marker.revisionId ? { revisionId: marker.revisionId } : {}
72117
72554
  });
72118
72555
  if (existsSync27(target)) {
72119
72556
  backup = mkdtempSync2(join29(root, `.pull-backup-${name}-`));
@@ -72138,11 +72575,12 @@ function writePullMarker(dir, record3) {
72138
72575
  const marker = {
72139
72576
  managedBy: "@hasna/skills",
72140
72577
  skill: record3.skill,
72141
- source: "pull",
72578
+ source: record3.source ?? "pull",
72142
72579
  ...record3.version ? { version: record3.version } : {},
72143
72580
  ...record3.contentHash ? { contentHash: record3.contentHash } : {},
72144
72581
  ...record3.sourceCommit ? { sourceCommit: record3.sourceCommit } : {},
72145
72582
  ...record3.signature ? { signature: record3.signature } : {},
72583
+ ...record3.revisionId ? { revisionId: record3.revisionId } : {},
72146
72584
  syncedAt: new Date().toISOString()
72147
72585
  };
72148
72586
  writeFileSync18(join29(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
@@ -72166,20 +72604,14 @@ async function safeMeta(client, slug) {
72166
72604
  ...str(record3.category) ? { category: str(record3.category) } : {},
72167
72605
  ...tags && tags.length ? { tags } : {},
72168
72606
  ...str(record3.version) ? { version: str(record3.version) } : {},
72169
- ...kind ? { kind } : {}
72607
+ ...kind ? { kind } : {},
72608
+ ...REVISION_ID_PATTERN.test(str(record3.revisionId) ?? "") ? { revisionId: str(record3.revisionId) } : {},
72609
+ ...typeof record3.skillMd === "string" && record3.skillMd.length > 0 ? { skillMd: record3.skillMd } : {},
72610
+ ...str(record3.publishedSource) ? { publishedSource: str(record3.publishedSource) } : {}
72170
72611
  };
72171
72612
  }
72172
72613
  function pickCorpusOptions(options) {
72173
- return { rootDir: resolvePullCorpusRoot(options) };
72174
- }
72175
- function resolvePullCorpusRoot(options) {
72176
- if (options.rootDir)
72177
- return options.rootDir;
72178
- const appDir = options.homeDir ? join29(options.homeDir, ".hasna", "skills") : getDataDir();
72179
- const cache3 = join29(appDir, SKILLS_CACHE_DIRNAME2);
72180
- if (existsSync27(join29(cache3, LAYOUT_MIGRATION_RECORD2)) && existsSync27(cache3))
72181
- return cache3;
72182
- return getPortableSkillsRoot(options);
72614
+ return { rootDir: resolveCorpusRoot(options) };
72183
72615
  }
72184
72616
  function extractSlug(entry) {
72185
72617
  if (!entry || typeof entry !== "object")
@@ -72193,13 +72625,15 @@ function dedupe(values2) {
72193
72625
  function str(value) {
72194
72626
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
72195
72627
  }
72196
- var BUNDLE_DIGEST_HEADER = "X-Skill-Bundle-Sha256", BUNDLE_SIGNATURE_HEADER = "X-Skill-Bundle-Signature", PULL_MARKER_FILE = ".hasna-skills.json", SKILLS_CACHE_DIRNAME2 = "skills", LAYOUT_MIGRATION_RECORD2 = ".layout-migration.json", PullSkillError;
72628
+ var BUNDLE_DIGEST_HEADER = "X-Skill-Bundle-Sha256", BUNDLE_SIGNATURE_HEADER = "X-Skill-Bundle-Signature", BUNDLE_REVISION_ID_HEADER = "X-Skill-Revision-Id", BUNDLE_REVISION_NUMBER_HEADER = "X-Skill-Revision-Number", PULL_MARKER_FILE = ".hasna-skills.json", PullSkillError;
72197
72629
  var init_pull = __esm(() => {
72198
72630
  init_remote_client();
72199
72631
  init_portable_skills();
72200
- init_config();
72632
+ init_home_migration();
72633
+ init_revision();
72201
72634
  init_skill_bundle();
72202
72635
  init_skill_bundles();
72636
+ init_revision();
72203
72637
  PullSkillError = class PullSkillError extends Error {
72204
72638
  detail;
72205
72639
  constructor(message, detail) {
@@ -72320,10 +72754,10 @@ __export(exports_publish, {
72320
72754
  pushSkill: () => pushSkill,
72321
72755
  PushSkillError: () => PushSkillError
72322
72756
  });
72323
- import { existsSync as existsSync28, readFileSync as readFileSync21 } from "fs";
72757
+ import { existsSync as existsSync28, readFileSync as readFileSync23 } from "fs";
72324
72758
  import { join as join30 } from "path";
72325
72759
  function registerPublish(parent) {
72326
- parent.command("push").argument("<name>", "Name of a skill in ~/.hasna/skills/installed").option("--version <version>", "Override the version recorded on the instance").option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
72760
+ parent.command("push").argument("<name>", "Name of a skill in the local corpus (~/.hasna/skills/installed or the migrated ~/.hasna/skills/skills)").option("--version <version>", "Override the version recorded on the instance").option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
72327
72761
  try {
72328
72762
  const result2 = await pushSkill(name, {
72329
72763
  dryRun: options.dryRun,
@@ -72363,7 +72797,7 @@ async function pushSkill(name, options = {}) {
72363
72797
  const manifest = readPortableSkillManifest(skill.path, skill.name);
72364
72798
  const packed = packSkillBundle(skill.path, { maxUnpackedBytes: MAX_UNPACKED_BYTES });
72365
72799
  const skillMdPath = join30(skill.path, "SKILL.md");
72366
- const skillMd = existsSync28(skillMdPath) ? readFileSync21(skillMdPath, "utf-8") : undefined;
72800
+ const skillMd = existsSync28(skillMdPath) ? readFileSync23(skillMdPath, "utf-8") : undefined;
72367
72801
  const base2 = {
72368
72802
  slug: skill.name,
72369
72803
  path: skill.path,
@@ -72381,6 +72815,8 @@ async function pushSkill(name, options = {}) {
72381
72815
  if (!client) {
72382
72816
  throw new PushSkillError("No API key configured, so there is nowhere to publish to.", ["Run `skills login`, or set SKILLS_API_KEY and SKILLS_API_URL for this instance."]);
72383
72817
  }
72818
+ const current = await client.getSkill(skill.name);
72819
+ const ifMatch = current && typeof current.revisionId === "string" && current.revisionId ? current.revisionId : undefined;
72384
72820
  const response = await client.publishSkill({
72385
72821
  slug: skill.name,
72386
72822
  displayName: manifest.displayName ?? skill.displayName,
@@ -72393,10 +72829,17 @@ async function pushSkill(name, options = {}) {
72393
72829
  bundleSha256: packed.sha256,
72394
72830
  contentHash: packed.sha256,
72395
72831
  ...skillMd ? { skillMd } : {}
72396
- }, packed.bytes);
72832
+ }, packed.bytes, ifMatch);
72397
72833
  const payload = await readBody(response);
72398
72834
  if (!response.ok) {
72399
- throw new PushSkillError(`Publishing '${skill.name}' failed: ${response.status} ${describeError(payload)}`, typeof payload === "object" && payload && "code" in payload ? [`code: ${String(payload.code)}`] : undefined);
72835
+ const code = typeof payload === "object" && payload && "code" in payload ? String(payload.code) : undefined;
72836
+ if (response.status === 409 || code === "REVISION_CONFLICT") {
72837
+ throw new PushSkillError(`Publishing '${skill.name}' failed: the instance serves a NEWER revision of this skill. ` + "Your push would silently overwrite it, so it was refused.", [
72838
+ `code: REVISION_CONFLICT`,
72839
+ "Reconcile first: pull the current revision (skills pull <name>), merge your changes, then push again."
72840
+ ]);
72841
+ }
72842
+ throw new PushSkillError(`Publishing '${skill.name}' failed: ${response.status} ${describeError(payload)}`, code ? [`code: ${code}`] : undefined);
72400
72843
  }
72401
72844
  return { ...base2, published: true, status: response.status, response: payload };
72402
72845
  }
@@ -72680,7 +73123,7 @@ function printLoginSuccess(loginResult, json) {
72680
73123
  \u2713 Signed in as ${loginResult.user.email}`));
72681
73124
  console.log(source_default.dim(` Organization: ${loginResult.organization.name}`));
72682
73125
  if (loginResult.firstLogin) {
72683
- console.log(source_default.dim(` API key saved to ~/.hasna/skills/auth.json`));
73126
+ console.log(source_default.dim(` API key saved to ${getAuthFilePath()}`));
72684
73127
  }
72685
73128
  }
72686
73129
  async function doLogin(email2, code, json) {
@@ -73055,7 +73498,7 @@ function registerStorage(parent) {
73055
73498
  const snapshot = exportSkillsLocalSnapshot(process.cwd(), { includeFileContents: false });
73056
73499
  const s3Plan = config2.s3Bucket ? planSkillsS3SnapshotUpload(snapshot, { prefix: config2.s3Prefix }) : [];
73057
73500
  const plan = {
73058
- package: "open-skills",
73501
+ package: "skills",
73059
73502
  noNetwork: true,
73060
73503
  databaseConfigured: Boolean(config2.databaseUrl),
73061
73504
  s3Configured: Boolean(config2.s3Bucket),
@@ -73124,6 +73567,428 @@ var init_storage = __esm(() => {
73124
73567
  init_home_migration();
73125
73568
  });
73126
73569
 
73570
+ // src/lib/registry-reconcile.ts
73571
+ import { existsSync as existsSync29, readFileSync as readFileSync24, statSync as statSync15, writeFileSync as writeFileSync19 } from "fs";
73572
+ import { join as join31 } from "path";
73573
+ function isDirectory2(path) {
73574
+ try {
73575
+ return statSync15(path).isDirectory();
73576
+ } catch {
73577
+ return false;
73578
+ }
73579
+ }
73580
+ function migrationNeeded(options) {
73581
+ if (options.rootDir)
73582
+ return false;
73583
+ const appDir = options.homeDir ? join31(options.homeDir, ".hasna", "skills") : getDataDir();
73584
+ return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join31(appDir, SKILLS_CACHE_DIRNAME)));
73585
+ }
73586
+ function readBaseline(skillDir) {
73587
+ const markerPath = join31(skillDir, PULL_MARKER_FILE);
73588
+ if (!existsSync29(markerPath))
73589
+ return;
73590
+ try {
73591
+ const marker = JSON.parse(readFileSync24(markerPath, "utf-8"));
73592
+ return {
73593
+ ...typeof marker.contentHash === "string" && marker.contentHash ? { contentHash: marker.contentHash } : {},
73594
+ ...typeof marker.version === "string" && marker.version ? { version: marker.version } : {}
73595
+ };
73596
+ } catch {
73597
+ return;
73598
+ }
73599
+ }
73600
+ function readCursor(root) {
73601
+ const path = join31(root, SYNC_CURSOR_FILE);
73602
+ if (!existsSync29(path))
73603
+ return { runCount: 0 };
73604
+ try {
73605
+ const cursor = JSON.parse(readFileSync24(path, "utf-8"));
73606
+ return { runCount: typeof cursor.runCount === "number" ? cursor.runCount : 0 };
73607
+ } catch {
73608
+ return { runCount: 0 };
73609
+ }
73610
+ }
73611
+ function resolveCorpusRootReadOnly(options) {
73612
+ if (options.rootDir)
73613
+ return { root: options.rootDir, migrationPending: false };
73614
+ const appDir = options.homeDir ? join31(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
73615
+ const cache3 = join31(appDir, SKILLS_CACHE_DIRNAME);
73616
+ if (isOwnerLayoutMigrated(appDir) && isDirectory2(cache3)) {
73617
+ return { root: cache3, migrationPending: false };
73618
+ }
73619
+ return { root: join31(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
73620
+ }
73621
+ function remoteRowToSkill(record3) {
73622
+ const slug = typeof record3.slug === "string" ? record3.slug : typeof record3.name === "string" ? record3.name : undefined;
73623
+ if (!slug)
73624
+ return;
73625
+ return {
73626
+ slug,
73627
+ version: typeof record3.version === "string" ? record3.version : undefined,
73628
+ sha256: typeof record3.bundleSha256 === "string" && record3.bundleSha256 ? record3.bundleSha256 : undefined
73629
+ };
73630
+ }
73631
+ function recheckLocalSide(plannedLocal, localDir, ops = {
73632
+ pack: (dir) => packSkillBundle(dir).sha256,
73633
+ exists: existsSync29
73634
+ }) {
73635
+ let localNow;
73636
+ try {
73637
+ localNow = ops.pack(localDir);
73638
+ } catch {
73639
+ localNow = undefined;
73640
+ }
73641
+ const dirExists = ops.exists(localDir);
73642
+ return plannedLocal !== undefined ? localNow !== plannedLocal : localNow !== undefined || dirExists;
73643
+ }
73644
+ function classifySkill(local, remote, baseline) {
73645
+ if (!remote)
73646
+ return { state: "local-only" };
73647
+ if (!local)
73648
+ return { state: "remote-only" };
73649
+ const baselineHash = baseline?.contentHash;
73650
+ const baselineVersion = baseline?.version;
73651
+ if (remote.sha256) {
73652
+ if (remote.sha256 === local.sha256) {
73653
+ const effectiveLocalVersion = baselineVersion ?? local.version ?? undefined;
73654
+ const remoteVersion = remote.version ?? undefined;
73655
+ if (effectiveLocalVersion !== undefined && remoteVersion !== undefined && effectiveLocalVersion !== remoteVersion) {
73656
+ return { state: "conflict", reason: "version divergence with identical digest" };
73657
+ }
73658
+ return { state: "in-sync" };
73659
+ }
73660
+ if (!baselineHash) {
73661
+ return { state: "conflict", reason: "no baseline marker: cannot prove which side changed" };
73662
+ }
73663
+ if (baselineHash === local.sha256)
73664
+ return { state: "changed-remotely" };
73665
+ if (baselineHash === remote.sha256)
73666
+ return { state: "changed-locally" };
73667
+ return { state: "conflict", reason: "both sides changed since the baseline marker" };
73668
+ }
73669
+ if (baselineHash && baselineHash !== local.sha256) {
73670
+ return { state: "changed-locally", reason: "local digest moved since the baseline marker" };
73671
+ }
73672
+ if (local.version !== undefined && remote.version !== undefined && local.version !== remote.version) {
73673
+ return { state: "changed-locally", reason: "version diverged from the bundled row" };
73674
+ }
73675
+ return { state: "in-sync", reason: "no remote digest; no version or baseline evidence of local divergence" };
73676
+ }
73677
+ function isDigestless(remote) {
73678
+ return remote !== undefined && !remote.sha256;
73679
+ }
73680
+ function resolveAction(state, direction, conflict) {
73681
+ switch (state) {
73682
+ case "local-only":
73683
+ case "changed-locally":
73684
+ return direction === "push" || direction === "all" ? { action: "push" } : { action: "skip", reason: "push not requested" };
73685
+ case "remote-only":
73686
+ case "changed-remotely":
73687
+ return direction === "pull" || direction === "all" ? { action: "pull" } : { action: "skip", reason: "pull not requested" };
73688
+ case "conflict":
73689
+ if (conflict === "local" && (direction === "push" || direction === "all"))
73690
+ return { action: "push" };
73691
+ if (conflict === "remote" && (direction === "pull" || direction === "all"))
73692
+ return { action: "pull" };
73693
+ return { action: "skip", reason: `conflict policy '${conflict}' does not resolve in this direction` };
73694
+ case "in-sync":
73695
+ return { action: "none" };
73696
+ }
73697
+ }
73698
+ async function currentRemoteDigest(client, slug) {
73699
+ const { status, body } = await client.getSkillStatus(slug);
73700
+ if (status === 404)
73701
+ return;
73702
+ if (status !== 200) {
73703
+ throw new ReconcileRegistryError(`Registry re-check for '${slug}' failed: HTTP ${status}.`);
73704
+ }
73705
+ if (!body || typeof body !== "object")
73706
+ return;
73707
+ const digest = body.bundleSha256;
73708
+ return typeof digest === "string" && digest ? digest : undefined;
73709
+ }
73710
+ async function reconcileRegistry(options = {}) {
73711
+ const conflict = options.conflict ?? "skip";
73712
+ if (!CONFLICT_POLICIES.includes(conflict)) {
73713
+ throw new ReconcileRegistryError(`Unknown conflict policy '${String(conflict)}'. Use one of: ${CONFLICT_POLICIES.join(", ")}.`);
73714
+ }
73715
+ const direction = options.pull && !options.push && !options.all ? "pull" : options.push && !options.pull && !options.all ? "push" : "all";
73716
+ const dryRun = options.dryRun ?? false;
73717
+ const client = options.client !== undefined ? options.client : dryRun ? createRemoteSkillsClientReadOnly() : createRemoteSkillsClient();
73718
+ if (!client) {
73719
+ throw new ReconcileRegistryError("No API key configured, so there is nowhere to sync to.", ["Run `skills login`, or set SKILLS_API_KEY and SKILLS_API_URL for this instance."]);
73720
+ }
73721
+ const { root, migrationPending } = dryRun ? resolveCorpusRootReadOnly(options) : { root: resolveCorpusRoot(options), migrationPending: migrationNeeded(options) };
73722
+ const localSkills = listPortableSkillMetas({ rootDir: root });
73723
+ const locals = new Map;
73724
+ for (const meta of localSkills) {
73725
+ try {
73726
+ const path = getPortableSkillPath(meta.name, { rootDir: root });
73727
+ const packed = packSkillBundle(path);
73728
+ locals.set(meta.name, { slug: meta.name, version: meta.version, sha256: packed.sha256 });
73729
+ } catch {
73730
+ locals.set(meta.name, { slug: meta.name, version: meta.version, sha256: "" });
73731
+ }
73732
+ }
73733
+ const remoteRowsPayload = await client.listSkills();
73734
+ if (!Array.isArray(remoteRowsPayload)) {
73735
+ const shape = remoteRowsPayload && typeof remoteRowsPayload === "object" ? `object with keys [${Object.keys(remoteRowsPayload).join(", ")}]` : typeof remoteRowsPayload;
73736
+ throw new ReconcileRegistryError(`Registry listing failed: expected an array of skills, got ${shape}.`, ["Check SKILLS_API_URL and the stored credential; a failed listing must not be read as an empty registry."]);
73737
+ }
73738
+ const remotes = new Map;
73739
+ for (const row of remoteRowsPayload) {
73740
+ if (!row || typeof row !== "object")
73741
+ continue;
73742
+ const skill = remoteRowToSkill(row);
73743
+ if (skill)
73744
+ remotes.set(skill.slug, skill);
73745
+ }
73746
+ const allSlugs = [...new Set([...locals.keys(), ...remotes.keys()])].sort();
73747
+ const skills = [];
73748
+ for (const slug of allSlugs) {
73749
+ const local = locals.get(slug);
73750
+ const remote = remotes.get(slug);
73751
+ const baseline = local ? readBaseline(join31(root, slug)) : undefined;
73752
+ const { state, reason } = classifySkill(local, remote, baseline);
73753
+ let { action, reason: actionReason } = resolveAction(state, direction, conflict);
73754
+ if (state === "remote-only" && isDigestless(remote)) {
73755
+ action = "skip";
73756
+ actionReason = "no bundle digest; verified pulls only (use `skills pull --all` for metadata-only)";
73757
+ }
73758
+ skills.push({
73759
+ slug,
73760
+ state,
73761
+ action,
73762
+ ...local?.version ? { localVersion: local.version } : {},
73763
+ ...remote?.version ? { remoteVersion: remote.version } : {},
73764
+ ...local?.sha256 ? { localSha256: local.sha256 } : {},
73765
+ ...remote?.sha256 ? { remoteSha256: remote.sha256 } : {},
73766
+ ...reason ?? actionReason ? { reason: reason ?? actionReason } : {}
73767
+ });
73768
+ }
73769
+ const summary = {
73770
+ local: locals.size,
73771
+ remote: remotes.size,
73772
+ inSync: 0,
73773
+ pushed: 0,
73774
+ pulled: 0,
73775
+ conflicts: 0,
73776
+ skipped: 0,
73777
+ errors: 0
73778
+ };
73779
+ for (const entry of skills) {
73780
+ if (entry.state === "in-sync")
73781
+ summary.inSync += 1;
73782
+ if (entry.state === "conflict")
73783
+ summary.conflicts += 1;
73784
+ if (entry.action === "skip")
73785
+ summary.skipped += 1;
73786
+ }
73787
+ const pushSlugs = skills.filter((entry) => entry.action === "push").map((entry) => entry.slug);
73788
+ const pullSlugs = skills.filter((entry) => entry.action === "pull").map((entry) => entry.slug);
73789
+ if (!dryRun) {
73790
+ for (const slug of pushSlugs) {
73791
+ const entry = skills.find((item) => item.slug === slug);
73792
+ const plannedRemote = remotes.get(slug)?.sha256;
73793
+ let currentRemote;
73794
+ try {
73795
+ currentRemote = await currentRemoteDigest(client, slug);
73796
+ } catch (error2) {
73797
+ entry.result = { ok: false, detail: error2.message };
73798
+ summary.errors += 1;
73799
+ continue;
73800
+ }
73801
+ if (plannedRemote === undefined ? currentRemote !== undefined : currentRemote !== plannedRemote) {
73802
+ entry.result = { ok: false, detail: "remote changed during sync; push skipped" };
73803
+ entry.reason = (entry.reason ? `${entry.reason}; ` : "") + "remote changed during sync";
73804
+ summary.skipped += 1;
73805
+ continue;
73806
+ }
73807
+ try {
73808
+ await pushSkill(slug, { rootDir: root, client });
73809
+ const pushed = locals.get(slug);
73810
+ writePullMarker(join31(root, slug), {
73811
+ skill: slug,
73812
+ ...pushed?.version ? { version: pushed.version } : {},
73813
+ ...pushed?.sha256 ? { contentHash: pushed.sha256 } : {},
73814
+ source: "sync"
73815
+ });
73816
+ entry.result = { ok: true };
73817
+ summary.pushed += 1;
73818
+ } catch (error2) {
73819
+ entry.result = { ok: false, detail: error2.message };
73820
+ summary.errors += 1;
73821
+ }
73822
+ }
73823
+ const verifiedPullSlugs = [];
73824
+ for (const slug of pullSlugs) {
73825
+ const entry = skills.find((item) => item.slug === slug);
73826
+ const plannedRemote = remotes.get(slug)?.sha256;
73827
+ let currentRemote;
73828
+ try {
73829
+ currentRemote = await currentRemoteDigest(client, slug);
73830
+ } catch (error2) {
73831
+ entry.result = { ok: false, detail: error2.message };
73832
+ summary.errors += 1;
73833
+ continue;
73834
+ }
73835
+ if (plannedRemote === undefined ? currentRemote !== undefined : currentRemote !== plannedRemote) {
73836
+ entry.result = { ok: false, detail: "remote changed during sync; pull skipped" };
73837
+ entry.reason = (entry.reason ? `${entry.reason}; ` : "") + "remote changed during sync";
73838
+ summary.skipped += 1;
73839
+ continue;
73840
+ }
73841
+ const plannedLocal = locals.get(slug)?.sha256;
73842
+ const localDir = getPortableSkillPath(slug, { rootDir: root });
73843
+ const localMoved = recheckLocalSide(plannedLocal, localDir);
73844
+ if (localMoved) {
73845
+ entry.result = { ok: false, detail: "local changed during sync; pull skipped" };
73846
+ entry.reason = (entry.reason ? `${entry.reason}; ` : "") + "local changed during sync";
73847
+ summary.skipped += 1;
73848
+ continue;
73849
+ }
73850
+ verifiedPullSlugs.push(slug);
73851
+ }
73852
+ if (verifiedPullSlugs.length > 0) {
73853
+ try {
73854
+ const pulled = await pullSkills({ names: verifiedPullSlugs, rootDir: root, client, signingKey: options.signingKey });
73855
+ for (const result2 of pulled.results) {
73856
+ const entry = skills.find((item) => item.slug === result2.name);
73857
+ if (entry) {
73858
+ entry.result = { ok: result2.success, ...result2.error ? { detail: result2.error } : {} };
73859
+ if (result2.success)
73860
+ summary.pulled += 1;
73861
+ else
73862
+ summary.errors += 1;
73863
+ }
73864
+ }
73865
+ } catch (error2) {
73866
+ for (const slug of verifiedPullSlugs) {
73867
+ const entry = skills.find((item) => item.slug === slug);
73868
+ if (entry)
73869
+ entry.result = { ok: false, detail: error2.message };
73870
+ }
73871
+ summary.errors += verifiedPullSlugs.length;
73872
+ }
73873
+ }
73874
+ if (summary.errors === 0) {
73875
+ const cursor = {
73876
+ schemaVersion: SYNC_CURSOR_SCHEMA_VERSION,
73877
+ managedBy: "@hasna/skills",
73878
+ lastSyncedAt: new Date().toISOString(),
73879
+ runCount: readCursor(root).runCount + 1,
73880
+ summary
73881
+ };
73882
+ writeFileSync19(join31(root, SYNC_CURSOR_FILE), `${JSON.stringify(cursor, null, 2)}
73883
+ `);
73884
+ return {
73885
+ corpusRoot: root,
73886
+ migrationPending,
73887
+ direction,
73888
+ dryRun: false,
73889
+ conflictPolicy: conflict,
73890
+ conflictPolicyDescription: conflict === "skip" ? DEFAULT_CONFLICT_POLICY : conflict,
73891
+ summary,
73892
+ skills,
73893
+ cursor: { lastSyncedAt: cursor.lastSyncedAt, runCount: cursor.runCount }
73894
+ };
73895
+ }
73896
+ }
73897
+ if (dryRun) {
73898
+ summary.pushed = pushSlugs.length;
73899
+ summary.pulled = pullSlugs.length;
73900
+ }
73901
+ return {
73902
+ corpusRoot: root,
73903
+ migrationPending,
73904
+ direction,
73905
+ dryRun,
73906
+ conflictPolicy: conflict,
73907
+ conflictPolicyDescription: conflict === "skip" ? DEFAULT_CONFLICT_POLICY : conflict,
73908
+ summary,
73909
+ skills
73910
+ };
73911
+ }
73912
+ var DEFAULT_CONFLICT_POLICY = "local-wins-on-identical-digest-else-skip-and-report", CONFLICT_POLICIES, SYNC_CURSOR_FILE = ".sync-cursor.json", SYNC_CURSOR_SCHEMA_VERSION = 1, ReconcileRegistryError;
73913
+ var init_registry_reconcile = __esm(() => {
73914
+ init_publish();
73915
+ init_config();
73916
+ init_home_migration();
73917
+ init_portable_skills();
73918
+ init_pull();
73919
+ init_remote_client();
73920
+ init_skill_bundle();
73921
+ CONFLICT_POLICIES = ["local", "remote", "skip"];
73922
+ ReconcileRegistryError = class ReconcileRegistryError extends Error {
73923
+ detail;
73924
+ constructor(message, detail) {
73925
+ super(message);
73926
+ this.detail = detail;
73927
+ this.name = "ReconcileRegistryError";
73928
+ }
73929
+ };
73930
+ });
73931
+
73932
+ // src/cli/commands/registry-reconcile.ts
73933
+ var exports_registry_reconcile = {};
73934
+ __export(exports_registry_reconcile, {
73935
+ registerRegistryReconcile: () => registerRegistryReconcile
73936
+ });
73937
+ function registerRegistryReconcile(parent) {
73938
+ const group = parent.command("cloud").description("Synchronize this machine's skill corpus with the hosted registry");
73939
+ group.command("sync").option("--push", "Push local changes to the registry (local-only, changed-locally, and conflicts won by local)", false).option("--pull", "Pull registry changes into the local corpus (remote-only, changed-remotely, and conflicts won by remote)", false).option("--all", "Both directions (the default when neither --push nor --pull is given)", false).option("--dry-run", "Plan and report without writing anything", false).option("--json", "Output the full result as JSON", false).option("--conflict <policy>", `Conflict policy: ${CONFLICT_POLICIES.join(" | ")}. Default: ${DEFAULT_CONFLICT_POLICY}.`, "skip").description("Two-way reconcile between the local corpus and the hosted registry").action(async (options) => {
73940
+ try {
73941
+ const result2 = await reconcileRegistry({
73942
+ push: options.push,
73943
+ pull: options.pull,
73944
+ all: options.all,
73945
+ dryRun: options.dryRun,
73946
+ conflict: options.conflict
73947
+ });
73948
+ if (options.json) {
73949
+ console.log(JSON.stringify(result2, null, 2));
73950
+ if (result2.summary.errors > 0)
73951
+ process.exitCode = 1;
73952
+ return;
73953
+ }
73954
+ printHuman2(result2);
73955
+ if (result2.summary.errors > 0)
73956
+ process.exitCode = 1;
73957
+ } catch (error2) {
73958
+ if (options.json) {
73959
+ console.log(JSON.stringify({ error: error2.message }, null, 2));
73960
+ } else {
73961
+ console.error(source_default.red(error2.message));
73962
+ if (error2 instanceof ReconcileRegistryError)
73963
+ for (const line of error2.detail ?? [])
73964
+ console.error(source_default.dim(` - ${line}`));
73965
+ }
73966
+ process.exitCode = 1;
73967
+ }
73968
+ });
73969
+ }
73970
+ function printHuman2(result2) {
73971
+ const { summary } = result2;
73972
+ const heading = result2.dryRun ? "Dry run: what a sync would do" : summary.errors > 0 ? "Sync completed with errors" : "Sync complete";
73973
+ console.log(source_default.bold(`${heading} (${result2.direction === "all" ? "all" : result2.direction}${result2.dryRun ? ", dry-run" : ""})`));
73974
+ console.log(` ${source_default.dim("corpus")} ${result2.corpusRoot}`);
73975
+ console.log(` ${source_default.dim("conflict")} ${result2.conflictPolicy}`);
73976
+ console.log(` ${source_default.dim("summary")} ${summary.pushed} pushed \xB7 ${summary.pulled} pulled \xB7 ${summary.inSync} in-sync \xB7 ${summary.conflicts} conflict(s) \xB7 ${summary.skipped} skipped \xB7 ${summary.errors} error(s)`);
73977
+ const interesting = result2.skills.filter((entry) => entry.action !== "none" || entry.state === "conflict");
73978
+ for (const entry of interesting) {
73979
+ const verb = entry.action === "push" ? source_default.green("push") : entry.action === "pull" ? source_default.cyan("pull") : source_default.yellow(entry.action);
73980
+ console.log(` ${verb.padEnd(7)} ${entry.slug}${entry.reason ? source_default.dim(` \u2014 ${entry.reason}`) : ""}`);
73981
+ if (entry.result && !entry.result.ok) {
73982
+ console.log(source_default.dim(` failed: ${entry.result.detail ?? "unknown error"}`));
73983
+ }
73984
+ }
73985
+ console.log("");
73986
+ }
73987
+ var init_registry_reconcile2 = __esm(() => {
73988
+ init_source();
73989
+ init_registry_reconcile();
73990
+ });
73991
+
73127
73992
  // ../../node_modules/.bun/ink@5.2.1+6e93904c7bfe2418/node_modules/ink/build/render.js
73128
73993
  import { Stream } from "stream";
73129
73994
  import process12 from "process";
@@ -78144,8 +79009,9 @@ var import_react21 = __toESM(require_react(), 1);
78144
79009
  // src/cli/index.tsx
78145
79010
  init_esm();
78146
79011
 
78147
- // ../../node_modules/.bun/@hasna+events@0.1.7/node_modules/@hasna/events/dist/commander.js
79012
+ // ../events/dist/commander.js
78148
79013
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
79014
+ import { Buffer as Buffer22 } from "buffer";
78149
79015
  import { existsSync as existsSync2 } from "fs";
78150
79016
  import { homedir } from "os";
78151
79017
  import { join as join2 } from "path";
@@ -78161,29 +79027,83 @@ function getPathValue(input, path) {
78161
79027
  return;
78162
79028
  }, input);
78163
79029
  }
78164
- function wildcardToRegExp(pattern) {
78165
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
78166
- return new RegExp(`^${escaped}$`);
79030
+ function getFieldValues(input, path) {
79031
+ const values2 = [];
79032
+ const push = (value) => {
79033
+ if (!values2.some((item) => Object.is(item, value)))
79034
+ values2.push(value);
79035
+ };
79036
+ if (path.includes(".") && path in input)
79037
+ push(input[path]);
79038
+ const nestedValue = getPathValue(input, path);
79039
+ if (nestedValue !== undefined || !path.includes("."))
79040
+ push(nestedValue);
79041
+ return values2;
78167
79042
  }
78168
- function matchString(value, matcher) {
79043
+ function wildcardToRegExp(pattern, options = {}) {
79044
+ let body = "";
79045
+ for (let index = 0;index < pattern.length; index += 1) {
79046
+ const char = pattern[index];
79047
+ if (char === "*") {
79048
+ if (pattern[index + 1] === "*") {
79049
+ body += ".*";
79050
+ index += 1;
79051
+ } else {
79052
+ body += options.segmentSafe ? "[^/]*" : ".*";
79053
+ }
79054
+ } else {
79055
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
79056
+ }
79057
+ }
79058
+ return new RegExp(`^${body}$`);
79059
+ }
79060
+ function matchString(value, matcher, options = {}) {
78169
79061
  if (matcher === undefined)
78170
79062
  return true;
78171
79063
  if (value === undefined)
78172
79064
  return false;
78173
79065
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
78174
- return matchers.some((item) => wildcardToRegExp(item).test(value));
79066
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
78175
79067
  }
78176
79068
  function matchRecord(input, matcher) {
78177
79069
  if (!matcher)
78178
79070
  return true;
78179
79071
  return Object.entries(matcher).every(([path, expected]) => {
78180
- const actual = getPathValue(input, path);
78181
- if (typeof expected === "string" || Array.isArray(expected)) {
78182
- return matchString(actual === undefined ? undefined : String(actual), expected);
78183
- }
78184
- return actual === expected;
79072
+ const actualValues = getFieldValues(input, path);
79073
+ return matchField(actualValues, expected, path);
78185
79074
  });
78186
79075
  }
79076
+ function matchField(actualValues, expected, path) {
79077
+ if (isNegativeMatcher(expected)) {
79078
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
79079
+ }
79080
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
79081
+ }
79082
+ function matchPositiveField(actual, expected, path) {
79083
+ if (typeof expected === "string" || Array.isArray(expected)) {
79084
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
79085
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
79086
+ }));
79087
+ }
79088
+ if (Array.isArray(actual)) {
79089
+ return actual.some((item) => item === expected);
79090
+ }
79091
+ return actual === expected;
79092
+ }
79093
+ function stringCandidates(actual) {
79094
+ if (actual === undefined)
79095
+ return [];
79096
+ if (Array.isArray(actual)) {
79097
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
79098
+ }
79099
+ return [String(actual)];
79100
+ }
79101
+ function isPrimitiveFieldValue(value) {
79102
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
79103
+ }
79104
+ function isNegativeMatcher(value) {
79105
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
79106
+ }
78187
79107
  function eventMatchesFilter(event, filter2) {
78188
79108
  return matchString(event.source, filter2.source) && matchString(event.type, filter2.type) && matchString(event.subject, filter2.subject) && matchString(event.severity, filter2.severity) && matchRecord(event.data, filter2.data) && matchRecord(event.metadata, filter2.metadata);
78189
79109
  }
@@ -78196,17 +79116,29 @@ function channelMatchesEvent(channel, event) {
78196
79116
  }
78197
79117
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
78198
79118
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
79119
+ var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
79120
+ var DEFAULT_EVENT_PAGE_LIMIT = 100;
79121
+ var MAX_EVENT_PAGE_LIMIT = 1000;
78199
79122
  function getEventsDataDir(override) {
78200
79123
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join2(homedir(), ".hasna", "events");
78201
79124
  }
79125
+ function getActiveEventsDirEnv() {
79126
+ if (process.env[HASNA_EVENTS_DIR_ENV])
79127
+ return HASNA_EVENTS_DIR_ENV;
79128
+ if (process.env[HASNA_EVENTS_HOME_ENV])
79129
+ return HASNA_EVENTS_HOME_ENV;
79130
+ return null;
79131
+ }
78202
79132
 
78203
79133
  class JsonEventsStore {
78204
79134
  dataDir;
79135
+ runtime;
78205
79136
  channelsPath;
78206
79137
  eventsPath;
78207
79138
  deliveriesPath;
78208
79139
  constructor(dataDir = getEventsDataDir()) {
78209
79140
  this.dataDir = dataDir;
79141
+ this.runtime = localJsonRuntime(dataDir);
78210
79142
  this.channelsPath = join2(dataDir, "channels.json");
78211
79143
  this.eventsPath = join2(dataDir, "events.json");
78212
79144
  this.deliveriesPath = join2(dataDir, "deliveries.json");
@@ -78254,13 +79186,58 @@ class JsonEventsStore {
78254
79186
  await this.writeJson(this.eventsPath, events);
78255
79187
  return event;
78256
79188
  }
78257
- async listEvents() {
79189
+ async appendEventOnce(event, options = {}) {
79190
+ await this.init();
79191
+ const events = await this.readJson(this.eventsPath, []);
79192
+ const dedupe = options.dedupe !== false;
79193
+ if (dedupe) {
79194
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
79195
+ if (existing) {
79196
+ return {
79197
+ event: existing,
79198
+ stored: false,
79199
+ deduped: true,
79200
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
79201
+ };
79202
+ }
79203
+ }
79204
+ events.push(event);
79205
+ await this.writeJson(this.eventsPath, events);
79206
+ return {
79207
+ event,
79208
+ stored: true,
79209
+ deduped: false,
79210
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
79211
+ };
79212
+ }
79213
+ async listEvents(options = {}) {
78258
79214
  await this.init();
78259
- return this.readJson(this.eventsPath, []);
79215
+ const events = await this.readJson(this.eventsPath, []);
79216
+ return queryEvents(events, options);
79217
+ }
79218
+ async listEventsPage(options = {}) {
79219
+ await this.init();
79220
+ const events = await this.readJson(this.eventsPath, []);
79221
+ const queried = queryEvents(events, {
79222
+ eventId: options.eventId,
79223
+ source: options.source,
79224
+ type: options.type
79225
+ });
79226
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
79227
+ const limit = normalizeEventPageLimit(options.limit);
79228
+ const pageEvents = queried.slice(offset, offset + limit);
79229
+ const nextOffset = offset + pageEvents.length;
79230
+ const hasMore = nextOffset < queried.length;
79231
+ return {
79232
+ events: pageEvents,
79233
+ cursor: options.cursor,
79234
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
79235
+ hasMore
79236
+ };
78260
79237
  }
78261
79238
  async findEventByIdentity(identity2) {
78262
79239
  const events = await this.listEvents();
78263
- return events.find((event) => identity2.id !== undefined && event.id === identity2.id || identity2.dedupeKey !== undefined && event.dedupeKey === identity2.dedupeKey);
79240
+ return findEventByIdentity(events, identity2);
78264
79241
  }
78265
79242
  async appendDelivery(result2) {
78266
79243
  await this.init();
@@ -78311,6 +79288,130 @@ class JsonEventsStore {
78311
79288
  });
78312
79289
  }
78313
79290
  }
79291
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
79292
+ return {
79293
+ mode: "local-files",
79294
+ name: "json-events-store",
79295
+ remote: false,
79296
+ localFiles: true,
79297
+ localSqlite: false,
79298
+ postgres: false,
79299
+ s3: false,
79300
+ aws: false,
79301
+ durable: true,
79302
+ idempotency: "best-effort-local",
79303
+ replayCursors: true,
79304
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
79305
+ };
79306
+ }
79307
+ function encodeLocalJsonEventCursor(offset, options = {}) {
79308
+ if (!Number.isInteger(offset) || offset < 0)
79309
+ throw new Error(`Invalid event cursor offset: ${offset}`);
79310
+ const payload = {
79311
+ offset,
79312
+ eventId: options.eventId,
79313
+ source: options.source,
79314
+ type: options.type
79315
+ };
79316
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer22.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
79317
+ }
79318
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
79319
+ if (!cursor)
79320
+ return 0;
79321
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
79322
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
79323
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
79324
+ let payload;
79325
+ try {
79326
+ payload = JSON.parse(Buffer22.from(rawPayload, "base64url").toString("utf-8"));
79327
+ } catch {
79328
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
79329
+ }
79330
+ const offset = payload.offset;
79331
+ if (!Number.isInteger(offset) || offset < 0)
79332
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
79333
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
79334
+ assertCursorFilter("source", payload.source, options.source);
79335
+ assertCursorFilter("type", payload.type, options.type);
79336
+ return offset;
79337
+ }
79338
+ function normalizeEventPageLimit(limit) {
79339
+ if (limit === undefined)
79340
+ return DEFAULT_EVENT_PAGE_LIMIT;
79341
+ if (!Number.isInteger(limit) || limit < 1)
79342
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
79343
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
79344
+ }
79345
+ function queryEvents(events, options) {
79346
+ let rows = events;
79347
+ if (options.eventId)
79348
+ rows = rows.filter((event) => event.id === options.eventId);
79349
+ if (options.source)
79350
+ rows = rows.filter((event) => event.source === options.source);
79351
+ if (options.type)
79352
+ rows = rows.filter((event) => event.type === options.type);
79353
+ if (options.cursor) {
79354
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
79355
+ rows = rows.slice(offset);
79356
+ }
79357
+ if (options.limit !== undefined)
79358
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
79359
+ return rows;
79360
+ }
79361
+ function assertCursorFilter(name, cursorValue, optionValue) {
79362
+ if (cursorValue !== optionValue)
79363
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
79364
+ }
79365
+ function findEventByIdentity(events, identity2) {
79366
+ return events.find((event) => identity2.id !== undefined && event.id === identity2.id || identity2.dedupeKey !== undefined && event.dedupeKey === identity2.dedupeKey);
79367
+ }
79368
+ async function getEventsStatus(dataDir) {
79369
+ const store = new JsonEventsStore(dataDir);
79370
+ await store.init();
79371
+ const [channels, events, deliveries] = await Promise.all([
79372
+ store.listChannels(),
79373
+ store.listEvents(),
79374
+ store.listDeliveries()
79375
+ ]);
79376
+ const transports = channels.reduce((counts, channel) => {
79377
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
79378
+ return counts;
79379
+ }, {});
79380
+ return {
79381
+ service: "events",
79382
+ schemaVersion: "1.0",
79383
+ dataDir: store.dataDir,
79384
+ storage: store.runtime,
79385
+ env: {
79386
+ primary: HASNA_EVENTS_DIR_ENV,
79387
+ fallback: HASNA_EVENTS_HOME_ENV,
79388
+ active: getActiveEventsDirEnv()
79389
+ },
79390
+ files: {
79391
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
79392
+ events: statusFile(store.dataDir, "events.json", events.length),
79393
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
79394
+ },
79395
+ counts: {
79396
+ channels: channels.length,
79397
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
79398
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
79399
+ events: events.length,
79400
+ deliveries: deliveries.length
79401
+ },
79402
+ transports,
79403
+ safety: {
79404
+ includesEventPayloads: false,
79405
+ includesWebhookSecrets: false,
79406
+ listOutputsRedactSecrets: true,
79407
+ statusOutputIsMetadataOnly: true
79408
+ }
79409
+ };
79410
+ }
79411
+ function statusFile(dataDir, fileName, records) {
79412
+ const path = join2(dataDir, fileName);
79413
+ return { path, exists: existsSync2(path), records };
79414
+ }
78314
79415
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
78315
79416
  function buildSignatureBase(timestamp, body) {
78316
79417
  return `${timestamp}.${body}`;
@@ -78325,21 +79426,27 @@ function now2() {
78325
79426
  function truncate2(value, max2 = 4096) {
78326
79427
  return value.length > max2 ? `${value.slice(0, max2)}...` : value;
78327
79428
  }
78328
- function buildWebhookRequest(event, channel) {
79429
+ function buildWebhookRequest(event, channel, options = {}) {
78329
79430
  if (!channel.webhook)
78330
79431
  throw new Error(`Channel ${channel.id} has no webhook config`);
79432
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
79433
+ if (/^x-hasna-/i.test(name)) {
79434
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
79435
+ }
79436
+ }
78331
79437
  const body = JSON.stringify(event);
78332
- const timestamp = event.time;
79438
+ const timestamp = options.timestamp ?? new Date().toISOString();
78333
79439
  const headers = {
78334
79440
  "Content-Type": "application/json",
78335
79441
  "User-Agent": "@hasna/events",
78336
79442
  "X-Hasna-Event-Id": event.id,
78337
79443
  "X-Hasna-Event-Type": event.type,
78338
- "X-Hasna-Timestamp": timestamp,
78339
- ...channel.webhook.headers
79444
+ ...channel.webhook.headers,
79445
+ "X-Hasna-Timestamp": timestamp
78340
79446
  };
78341
- if (channel.webhook.secret) {
78342
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
79447
+ const secret = options.secret ?? channel.webhook.secret;
79448
+ if (secret) {
79449
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
78343
79450
  }
78344
79451
  return { body, headers };
78345
79452
  }
@@ -78347,7 +79454,21 @@ async function dispatchWebhook(event, channel, options = {}) {
78347
79454
  if (!channel.webhook)
78348
79455
  throw new Error(`Channel ${channel.id} has no webhook config`);
78349
79456
  const startedAt = now2();
78350
- const { body, headers } = buildWebhookRequest(event, channel);
79457
+ let secret = channel.webhook.secret;
79458
+ if (channel.webhook.secretRef) {
79459
+ if (!options.secretResolver) {
79460
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
79461
+ }
79462
+ try {
79463
+ secret = await options.secretResolver(channel.webhook.secretRef);
79464
+ } catch {
79465
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
79466
+ }
79467
+ if (!secret)
79468
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
79469
+ }
79470
+ const timestamp = (options.now?.() ?? new Date).toISOString();
79471
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
78351
79472
  const controller = new AbortController;
78352
79473
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
78353
79474
  try {
@@ -78379,6 +79500,15 @@ async function dispatchWebhook(event, channel, options = {}) {
78379
79500
  clearTimeout(timeout);
78380
79501
  }
78381
79502
  }
79503
+ function failedAttempt(startedAt, error) {
79504
+ return {
79505
+ attempt: 1,
79506
+ status: "failed",
79507
+ startedAt,
79508
+ completedAt: now2(),
79509
+ error
79510
+ };
79511
+ }
78382
79512
  async function dispatchCommand(event, channel) {
78383
79513
  if (!channel.command)
78384
79514
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -78467,6 +79597,90 @@ function createDeliveryResult(event, channel, attempts) {
78467
79597
  completedAt: attempts.at(-1)?.completedAt ?? now2()
78468
79598
  };
78469
79599
  }
79600
+
79601
+ class EventValidationError extends Error {
79602
+ eventType;
79603
+ issues;
79604
+ constructor(eventType, issues) {
79605
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
79606
+ super(`Event validation failed for type "${eventType}": ${detail}`);
79607
+ this.name = "EventValidationError";
79608
+ this.eventType = eventType;
79609
+ this.issues = issues;
79610
+ }
79611
+ }
79612
+
79613
+ class EventTypeCatalog {
79614
+ definitions = new Map;
79615
+ register(definition) {
79616
+ this.definitions.set(definition.type, definition);
79617
+ return this;
79618
+ }
79619
+ unregister(type) {
79620
+ return this.definitions.delete(type);
79621
+ }
79622
+ has(type) {
79623
+ return this.definitions.has(type);
79624
+ }
79625
+ get(type) {
79626
+ return this.definitions.get(type);
79627
+ }
79628
+ list() {
79629
+ return [...this.definitions.values()];
79630
+ }
79631
+ validateEvent(event) {
79632
+ const definition = this.definitions.get(event.type);
79633
+ if (!definition)
79634
+ return { ok: true };
79635
+ return definition.validate(event.data, event);
79636
+ }
79637
+ assertEventValid(event) {
79638
+ const result2 = this.validateEvent(event);
79639
+ if (!result2.ok) {
79640
+ throw new EventValidationError(event.type, result2.issues);
79641
+ }
79642
+ }
79643
+ }
79644
+ var defaultEventTypeCatalog = new EventTypeCatalog;
79645
+ var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
79646
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
79647
+ if (paths.length === 0)
79648
+ return event;
79649
+ const copy = structuredClone(event);
79650
+ for (const path of paths) {
79651
+ setPath(copy, path, replacement);
79652
+ }
79653
+ return copy;
79654
+ }
79655
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
79656
+ return redactValue(event, replacement);
79657
+ }
79658
+ function shouldRedactKey(key) {
79659
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
79660
+ }
79661
+ function redactValue(value, replacement) {
79662
+ if (Array.isArray(value))
79663
+ return value.map((item) => redactValue(item, replacement));
79664
+ if (!value || typeof value !== "object")
79665
+ return value;
79666
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
79667
+ key,
79668
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
79669
+ ]));
79670
+ }
79671
+ function setPath(input, path, replacement) {
79672
+ const parts = path.split(".");
79673
+ let cursor = input;
79674
+ for (const part of parts.slice(0, -1)) {
79675
+ const next = cursor[part];
79676
+ if (!next || typeof next !== "object")
79677
+ return;
79678
+ cursor = next;
79679
+ }
79680
+ const last2 = parts.at(-1);
79681
+ if (last2 && last2 in cursor)
79682
+ cursor[last2] = replacement;
79683
+ }
78470
79684
  function createEvent(input) {
78471
79685
  return {
78472
79686
  id: input.id ?? randomUUID2(),
@@ -78487,10 +79701,18 @@ class EventsClient {
78487
79701
  store;
78488
79702
  redactors;
78489
79703
  transportOptions;
79704
+ catalog;
79705
+ validateCatalogTypes;
78490
79706
  constructor(options = {}) {
78491
79707
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
78492
79708
  this.redactors = options.redactors ?? [];
78493
- this.transportOptions = { fetchImpl: options.fetchImpl };
79709
+ this.transportOptions = {
79710
+ fetchImpl: options.fetchImpl,
79711
+ secretResolver: options.secretResolver,
79712
+ now: options.now
79713
+ };
79714
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
79715
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
78494
79716
  }
78495
79717
  async addChannel(input) {
78496
79718
  const timestamp = new Date().toISOString();
@@ -78508,18 +79730,40 @@ class EventsClient {
78508
79730
  }
78509
79731
  async emit(input, options = {}) {
78510
79732
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
78511
- if (options.dedupe !== false) {
78512
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
78513
- if (existing) {
78514
- return { event: existing, deliveries: [], deduped: true };
78515
- }
78516
- }
78517
- await this.store.appendEvent(event);
78518
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
78519
- return { event, deliveries, deduped: false };
78520
- }
78521
- async listEvents() {
78522
- return this.store.listEvents();
79733
+ if (options.validate ?? this.validateCatalogTypes) {
79734
+ this.catalog.assertEventValid(event);
79735
+ }
79736
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
79737
+ if (append.deduped) {
79738
+ return { event: append.event, deliveries: [], deduped: true };
79739
+ }
79740
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
79741
+ return { event: append.event, deliveries, deduped: false };
79742
+ }
79743
+ async listEvents(options = {}) {
79744
+ if (Object.keys(options).length === 0)
79745
+ return this.store.listEvents();
79746
+ return queryClientEvents(await this.store.listEvents(), options);
79747
+ }
79748
+ async listEventsPage(options = {}) {
79749
+ if (this.store.listEventsPage)
79750
+ return this.store.listEventsPage(options);
79751
+ const events = queryClientEvents(await this.store.listEvents(), {
79752
+ eventId: options.eventId,
79753
+ source: options.source,
79754
+ type: options.type
79755
+ });
79756
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
79757
+ const limit = normalizeEventPageLimit(options.limit);
79758
+ const pageEvents = events.slice(offset, offset + limit);
79759
+ const nextOffset = offset + pageEvents.length;
79760
+ const hasMore = nextOffset < events.length;
79761
+ return {
79762
+ events: pageEvents,
79763
+ cursor: options.cursor,
79764
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
79765
+ hasMore
79766
+ };
78523
79767
  }
78524
79768
  async listDeliveries() {
78525
79769
  return this.store.listDeliveries();
@@ -78536,7 +79780,7 @@ class EventsClient {
78536
79780
  }
78537
79781
  return deliveries;
78538
79782
  }
78539
- async testChannel(id, input = {}) {
79783
+ async matchChannel(id, input = {}) {
78540
79784
  const channel = await this.store.getChannel(id);
78541
79785
  if (!channel)
78542
79786
  throw new Error(`Channel not found: ${id}`);
@@ -78553,28 +79797,71 @@ class EventsClient {
78553
79797
  time: input.time,
78554
79798
  id: input.id
78555
79799
  });
79800
+ const matched = channelMatchesEvent(channel, event);
79801
+ return {
79802
+ channelId: channel.id,
79803
+ matched,
79804
+ event,
79805
+ filters: channel.filters,
79806
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
79807
+ };
79808
+ }
79809
+ async testChannel(id, input = {}, options = {}) {
79810
+ const channel = await this.store.getChannel(id);
79811
+ if (!channel)
79812
+ throw new Error(`Channel not found: ${id}`);
79813
+ const match = await this.matchChannel(id, input);
79814
+ const event = match.event;
79815
+ if (options.honorFilters && !match.matched) {
79816
+ const timestamp = new Date().toISOString();
79817
+ const result22 = createDeliveryResult(event, channel, [{
79818
+ attempt: 1,
79819
+ status: "skipped",
79820
+ startedAt: timestamp,
79821
+ completedAt: timestamp,
79822
+ error: match.reason
79823
+ }]);
79824
+ result22.metadata = { reason: "filter_mismatch" };
79825
+ await this.store.appendDelivery(result22);
79826
+ return result22;
79827
+ }
78556
79828
  const eventForChannel = await this.applyRedaction(event, channel);
78557
79829
  const result2 = await this.deliverWithRetry(eventForChannel, channel);
78558
79830
  await this.store.appendDelivery(result2);
78559
79831
  return result2;
78560
79832
  }
78561
79833
  async replay(options = {}) {
78562
- const events = (await this.store.listEvents()).filter((event) => {
78563
- if (options.eventId && event.id !== options.eventId)
78564
- return false;
78565
- if (options.source && event.source !== options.source)
78566
- return false;
78567
- if (options.type && event.type !== options.type)
78568
- return false;
78569
- return true;
78570
- });
79834
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
78571
79835
  if (options.dryRun)
78572
- return { events, deliveries: [] };
79836
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
78573
79837
  const deliveries = [];
78574
- for (const event of events) {
79838
+ for (const event of page.events) {
78575
79839
  deliveries.push(...await this.deliver(event));
78576
79840
  }
78577
- return { events, deliveries };
79841
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
79842
+ }
79843
+ async appendEvent(event, options) {
79844
+ if (this.store.appendEventOnce) {
79845
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
79846
+ }
79847
+ if (options.dedupe) {
79848
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
79849
+ if (existing) {
79850
+ return {
79851
+ event: existing,
79852
+ stored: false,
79853
+ deduped: true,
79854
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
79855
+ };
79856
+ }
79857
+ }
79858
+ const stored = await this.store.appendEvent(event);
79859
+ return {
79860
+ event: stored,
79861
+ stored: true,
79862
+ deduped: false,
79863
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
79864
+ };
78578
79865
  }
78579
79866
  async applyRedaction(event, channel) {
78580
79867
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -78601,15 +79888,6 @@ class EventsClient {
78601
79888
  return createDeliveryResult(event, channel, attempts);
78602
79889
  }
78603
79890
  }
78604
- function redactPaths(event, paths, replacement = "[REDACTED]") {
78605
- if (paths.length === 0)
78606
- return event;
78607
- const copy = structuredClone(event);
78608
- for (const path of paths) {
78609
- setPath(copy, path, replacement);
78610
- }
78611
- return copy;
78612
- }
78613
79891
  function sanitizeChannelForOutput(channel) {
78614
79892
  const copy = structuredClone(channel);
78615
79893
  if (copy.webhook?.secret)
@@ -78622,34 +79900,19 @@ function sanitizeChannelForOutput(channel) {
78622
79900
  function sanitizeChannelsForOutput(channels) {
78623
79901
  return channels.map(sanitizeChannelForOutput);
78624
79902
  }
78625
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
78626
- return redactValue(event, replacement);
78627
- }
78628
- function shouldRedactKey(key) {
78629
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
78630
- }
78631
- function redactValue(value, replacement) {
78632
- if (Array.isArray(value))
78633
- return value.map((item) => redactValue(item, replacement));
78634
- if (!value || typeof value !== "object")
78635
- return value;
78636
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
78637
- key,
78638
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
78639
- ]));
78640
- }
78641
- function setPath(input, path, replacement) {
78642
- const parts = path.split(".");
78643
- let cursor = input;
78644
- for (const part of parts.slice(0, -1)) {
78645
- const next = cursor[part];
78646
- if (!next || typeof next !== "object")
78647
- return;
78648
- cursor = next;
78649
- }
78650
- const last2 = parts.at(-1);
78651
- if (last2 && last2 in cursor)
78652
- cursor[last2] = replacement;
79903
+ function queryClientEvents(events, options) {
79904
+ let rows = events;
79905
+ if (options.eventId)
79906
+ rows = rows.filter((event) => event.id === options.eventId);
79907
+ if (options.source)
79908
+ rows = rows.filter((event) => event.source === options.source);
79909
+ if (options.type)
79910
+ rows = rows.filter((event) => event.type === options.type);
79911
+ if (options.cursor)
79912
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
79913
+ if (options.limit !== undefined)
79914
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
79915
+ return rows;
78653
79916
  }
78654
79917
  function normalizeTime(value) {
78655
79918
  if (!value)
@@ -78663,6 +79926,77 @@ function normalizeRetryPolicy(policy) {
78663
79926
  multiplier: Math.max(1, policy?.multiplier ?? 2)
78664
79927
  };
78665
79928
  }
79929
+ function parseFieldMatchers(values2, label, typed = false) {
79930
+ if (!values2?.length)
79931
+ return;
79932
+ const result2 = {};
79933
+ for (const value of values2) {
79934
+ const parsed = parseMatcherExpression(value, label);
79935
+ const path = parsed.path;
79936
+ if (path in result2)
79937
+ throw new Error(`Duplicate ${label} filter path: ${path}`);
79938
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
79939
+ result2[path] = parsed.negated ? { not: matcherValue } : matcherValue;
79940
+ }
79941
+ return result2;
79942
+ }
79943
+ function parseFilterOptions(options) {
79944
+ const filter2 = {};
79945
+ if (options.source)
79946
+ filter2.source = options.source;
79947
+ if (options.type)
79948
+ filter2.type = options.type;
79949
+ if (options.subject)
79950
+ filter2.subject = options.subject;
79951
+ if (options.severity)
79952
+ filter2.severity = options.severity;
79953
+ const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
79954
+ const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
79955
+ if (Object.keys(data).length > 0)
79956
+ filter2.data = data;
79957
+ if (Object.keys(metadata).length > 0)
79958
+ filter2.metadata = metadata;
79959
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
79960
+ }
79961
+ function mergeMatchers(...records) {
79962
+ const result2 = {};
79963
+ for (const record of records) {
79964
+ if (!record)
79965
+ continue;
79966
+ for (const [path, value] of Object.entries(record)) {
79967
+ if (path in result2)
79968
+ throw new Error(`Duplicate filter path: ${path}`);
79969
+ result2[path] = value;
79970
+ }
79971
+ }
79972
+ return result2;
79973
+ }
79974
+ function parseTypedMatcherValue(value, label) {
79975
+ const parsed = JSON.parse(value);
79976
+ if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
79977
+ return parsed;
79978
+ }
79979
+ throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
79980
+ }
79981
+ function parseMatcherExpression(value, label) {
79982
+ const negativeSeparator = value.indexOf("!=");
79983
+ if (negativeSeparator > 0) {
79984
+ return {
79985
+ path: value.slice(0, negativeSeparator),
79986
+ rawValue: value.slice(negativeSeparator + 2),
79987
+ negated: true
79988
+ };
79989
+ }
79990
+ const separator = value.indexOf("=");
79991
+ if (separator <= 0)
79992
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
79993
+ return {
79994
+ path: value.slice(0, separator),
79995
+ rawValue: value.slice(separator + 1),
79996
+ negated: false
79997
+ };
79998
+ }
79999
+ var DEFAULT_EVENT_LIST_LIMIT = 100;
78666
80000
  function parseJsonObject(value, fallback) {
78667
80001
  if (!value)
78668
80002
  return fallback;
@@ -78684,18 +80018,6 @@ function parseHeaders(values2) {
78684
80018
  }
78685
80019
  return headers;
78686
80020
  }
78687
- function parseFilter(options) {
78688
- const filter2 = {};
78689
- if (options.source)
78690
- filter2.source = options.source;
78691
- if (options.type)
78692
- filter2.type = options.type;
78693
- if (options.subject)
78694
- filter2.subject = options.subject;
78695
- if (options.severity)
78696
- filter2.severity = options.severity;
78697
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
78698
- }
78699
80021
  function createClient(options) {
78700
80022
  if (options.createClient)
78701
80023
  return options.createClient();
@@ -78707,22 +80029,30 @@ function print(value, json, text) {
78707
80029
  else
78708
80030
  console.log(text);
78709
80031
  }
80032
+ function fail(error, json) {
80033
+ const message = error instanceof Error ? error.message : String(error);
80034
+ if (json)
80035
+ console.log(JSON.stringify({ error: message }, null, 2));
80036
+ else
80037
+ console.error(message);
80038
+ process.exitCode = 1;
80039
+ }
78710
80040
  function hasJsonOption(options) {
78711
80041
  return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
78712
80042
  }
78713
80043
  function wantsJson(actionOptions, command) {
78714
80044
  return hasJsonOption(actionOptions) || hasJsonOption(command);
78715
80045
  }
78716
- function registerWebhookCommands(program2, options) {
78717
- const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
78718
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
80046
+ function registerChannelCommands(program2, options) {
80047
+ const channels = program2.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
80048
+ channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
78719
80049
  const timestamp = new Date().toISOString();
78720
80050
  const channel = {
78721
80051
  id: actionOptions.id,
78722
80052
  name: actionOptions.name,
78723
80053
  enabled: !actionOptions.disabled,
78724
80054
  transport: actionOptions.transport,
78725
- filters: parseFilter(actionOptions),
80055
+ filters: parseFilterOptions(actionOptions),
78726
80056
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
78727
80057
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
78728
80058
  createdAt: timestamp,
@@ -78738,35 +80068,61 @@ function registerWebhookCommands(program2, options) {
78738
80068
  const saved = await createClient(options).addChannel(channel);
78739
80069
  print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
78740
80070
  });
78741
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
78742
- const channels = await createClient(options).listChannels();
80071
+ channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
80072
+ const channels2 = await createClient(options).listChannels();
78743
80073
  if (wantsJson(actionOptions, command)) {
78744
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
80074
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
78745
80075
  return;
78746
80076
  }
78747
- if (!channels.length) {
80077
+ if (!channels2.length) {
78748
80078
  console.log("No channels configured.");
78749
80079
  return;
78750
80080
  }
78751
- for (const channel of channels) {
80081
+ for (const channel of channels2) {
78752
80082
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
78753
80083
  }
78754
80084
  });
78755
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
80085
+ channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
80086
+ const status = await getEventsStatus(options.dataDir);
80087
+ print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
80088
+ });
80089
+ channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
78756
80090
  const removed = await createClient(options).removeChannel(id);
78757
80091
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
78758
80092
  });
78759
- webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
78760
- const result2 = await createClient(options).testChannel(id, {
78761
- source: options.source,
78762
- type: actionOptions.type,
78763
- subject: actionOptions.subject ?? id,
78764
- message: actionOptions.message,
78765
- data: parseJsonObject(actionOptions.data, { test: true })
78766
- });
78767
- print(result2, wantsJson(actionOptions, command), `${result2.status}: ${result2.channelId}`);
80093
+ channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
80094
+ const json = wantsJson(actionOptions, command);
80095
+ try {
80096
+ const result2 = await createClient(options).testChannel(id, {
80097
+ source: actionOptions.source ?? options.source,
80098
+ type: actionOptions.type,
80099
+ subject: actionOptions.subject ?? id,
80100
+ message: actionOptions.message,
80101
+ data: parseJsonObject(actionOptions.data, { test: true }),
80102
+ metadata: parseJsonObject(actionOptions.metadata, {})
80103
+ }, { honorFilters: actionOptions.honorFilters });
80104
+ print(result2, json, `${result2.status}: ${result2.channelId}`);
80105
+ } catch (error) {
80106
+ fail(error, json);
80107
+ }
80108
+ });
80109
+ channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
80110
+ const json = wantsJson(actionOptions, command);
80111
+ try {
80112
+ const result2 = await createClient(options).matchChannel(id, {
80113
+ source: actionOptions.source ?? options.source,
80114
+ type: actionOptions.type,
80115
+ subject: actionOptions.subject ?? id,
80116
+ message: actionOptions.message,
80117
+ data: parseJsonObject(actionOptions.data, { test: true }),
80118
+ metadata: parseJsonObject(actionOptions.metadata, {})
80119
+ });
80120
+ print(result2, json, `${result2.matched ? "matched" : "skipped"}: ${result2.channelId}`);
80121
+ } catch (error) {
80122
+ fail(error, json);
80123
+ }
78768
80124
  });
78769
- return webhooks;
80125
+ return channels;
78770
80126
  }
78771
80127
  function registerEventCommands(program2, options) {
78772
80128
  const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
@@ -78783,7 +80139,8 @@ function registerEventCommands(program2, options) {
78783
80139
  }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
78784
80140
  print(result2, wantsJson(actionOptions, command), `${result2.deduped ? "Deduped" : "Emitted"} ${result2.event.id} to ${result2.deliveries.length} channel(s)`);
78785
80141
  });
78786
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
80142
+ const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
80143
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
78787
80144
  let rows = await createClient(options).listEvents();
78788
80145
  if (actionOptions.source)
78789
80146
  rows = rows.filter((event) => event.source === actionOptions.source);
@@ -78802,19 +80159,21 @@ function registerEventCommands(program2, options) {
78802
80159
  for (const event of rows)
78803
80160
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
78804
80161
  });
78805
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
80162
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
78806
80163
  const result2 = await createClient(options).replay({
78807
80164
  eventId: actionOptions.id,
78808
80165
  source: actionOptions.source,
78809
80166
  type: actionOptions.type,
80167
+ cursor: actionOptions.cursor,
80168
+ limit: actionOptions.limit,
78810
80169
  dryRun: actionOptions.dryRun
78811
80170
  });
78812
- print(result2, wantsJson(actionOptions, command), `Replayed ${result2.events.length} event(s), ${result2.deliveries.length} delivery result(s)`);
80171
+ print(result2, wantsJson(actionOptions, command), replaySummary(result2.events.length, result2.deliveries.length, result2.nextCursor));
78813
80172
  });
78814
80173
  return events;
78815
80174
  }
78816
80175
  function registerEventsCommands(program2, options) {
78817
- registerWebhookCommands(program2, options);
80176
+ registerChannelCommands(program2, options);
78818
80177
  registerEventCommands(program2, options);
78819
80178
  }
78820
80179
  function parseNumber(value) {
@@ -78827,6 +80186,10 @@ function collectValues(value, previous) {
78827
80186
  previous.push(value);
78828
80187
  return previous;
78829
80188
  }
80189
+ function replaySummary(events, deliveries, nextCursor) {
80190
+ const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
80191
+ return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
80192
+ }
78830
80193
 
78831
80194
  // src/cli/index.tsx
78832
80195
  init_source();
@@ -79986,6 +81349,8 @@ var { registerFeedback: registerFeedback2 } = await Promise.resolve().then(() =>
79986
81349
  registerFeedback2(program2);
79987
81350
  var { registerStorage: registerStorage2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
79988
81351
  registerStorage2(program2);
81352
+ var { registerRegistryReconcile: registerRegistryReconcile2 } = await Promise.resolve().then(() => (init_registry_reconcile2(), exports_registry_reconcile));
81353
+ registerRegistryReconcile2(program2);
79989
81354
  registerEventsCommands(program2, { source: "skills" });
79990
81355
  try {
79991
81356
  await program2.parseAsync();