@hasna/skills 0.1.63 → 0.1.66

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 (42) hide show
  1. package/README.md +61 -22
  2. package/bin/index.js +2120 -670
  3. package/bin/mcp.js +555 -249
  4. package/bin/migrate.js +229 -33
  5. package/bin/server.js +1542 -327
  6. package/bin/worker.js +716 -207
  7. package/dist/cli/commands/registry-reconcile.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +682 -276
  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 +29 -1
  16. package/dist/lib/portable-skills.d.ts +49 -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-types.d.ts +9 -0
  20. package/dist/lib/registry.d.ts +7 -4
  21. package/dist/lib/remote-client.d.ts +118 -1
  22. package/dist/lib/remote-registry.d.ts +26 -0
  23. package/dist/lib/revision.d.ts +29 -0
  24. package/dist/lib/run-routing.d.ts +60 -0
  25. package/dist/sdk/index.js +14412 -13338
  26. package/dist/server/app.d.ts +4 -1
  27. package/dist/server/config.d.ts +12 -2
  28. package/dist/server/rows.d.ts +2 -1
  29. package/dist/server/skills-api.d.ts +108 -6
  30. package/dist/server/sqlite-store.d.ts +20 -3
  31. package/dist/server/store-fixtures.d.ts +6 -0
  32. package/dist/server/store.d.ts +31 -5
  33. package/dist/server/types.d.ts +98 -3
  34. package/dist/storage.d.ts +1 -1
  35. package/dist/storage.js +57 -2
  36. package/migrations/postgres/0004_hosted_pins.sql +28 -0
  37. package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
  38. package/migrations/postgres/0005_tag_projection.sql +36 -0
  39. package/migrations/sqlite/0004_hosted_pins.sql +21 -0
  40. package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
  41. package/migrations/sqlite/0005_tag_projection.sql +25 -0
  42. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -47,8 +47,8 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = import.meta.require;
48
48
 
49
49
  // src/lib/registry.ts
50
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5 } from "fs";
51
- import { join as join6 } from "path";
50
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync6 } from "fs";
51
+ import { join as join7 } from "path";
52
52
 
53
53
  // src/lib/config.ts
54
54
  import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
@@ -154,6 +154,11 @@ function normalizeConfigValue(key, value) {
154
154
  }
155
155
  var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
156
156
  var INSTALLED_SKILLS_DIRNAME = "installed";
157
+ var SKILLS_CACHE_DIRNAME = "skills";
158
+ var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
159
+ function isOwnerLayoutMigrated(appDir) {
160
+ return existsSync(join(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
161
+ }
157
162
  function getDataDir() {
158
163
  const override = process.env[DATA_DIR_ENV];
159
164
  if (override) {
@@ -267,15 +272,16 @@ function unsetConfig(key, scope = "project") {
267
272
  // src/lib/portable-skills.ts
268
273
  import {
269
274
  cpSync as cpSync2,
270
- existsSync as existsSync5,
275
+ existsSync as existsSync6,
271
276
  mkdirSync as mkdirSync3,
272
- readdirSync as readdirSync4,
277
+ mkdtempSync,
278
+ readdirSync as readdirSync5,
273
279
  renameSync,
274
280
  rmSync,
275
- statSync as statSync5,
281
+ statSync as statSync6,
276
282
  writeFileSync as writeFileSync3
277
283
  } from "fs";
278
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join5, normalize as normalize2 } from "path";
284
+ import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join6, normalize as normalize2 } from "path";
279
285
 
280
286
  // src/lib/registry-data/development-tools.ts
281
287
  var DEVELOPMENT_TOOLS_SKILLS = [
@@ -435,7 +441,7 @@ var DEVELOPMENT_TOOLS_SKILLS = [
435
441
  {
436
442
  name: "monitor",
437
443
  displayName: "Monitor",
438
- description: "Operate the open-monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
444
+ description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
439
445
  category: "Development Tools",
440
446
  tags: ["monitoring", "mcp", "processes", "operations"]
441
447
  },
@@ -481,6 +487,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
481
487
  description: "Validate configuration files for syntax and schema compliance",
482
488
  category: "Development Tools",
483
489
  tags: ["config", "validation", "schema", "linting"]
490
+ },
491
+ {
492
+ name: "session-inject-monitor",
493
+ displayName: "Session Inject Monitor",
494
+ 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",
495
+ category: "Development Tools",
496
+ tags: ["monitor", "session", "injection", "automation", "wake"],
497
+ kind: "instruction"
484
498
  }
485
499
  ];
486
500
 
@@ -868,7 +882,7 @@ var DESIGN_BRANDING_SKILLS = [
868
882
  displayName: "Site Analyze",
869
883
  description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
870
884
  category: "Design & Branding",
871
- tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"]
885
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
872
886
  }
873
887
  ];
874
888
 
@@ -978,11 +992,9 @@ var SKILLS = [
978
992
  ...EVENT_MANAGEMENT_SKILLS
979
993
  ];
980
994
 
981
- // src/lib/skill-validation.ts
982
- import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
983
- import { isAbsolute, join as join2, normalize } from "path";
984
-
985
995
  // src/lib/hosted-skill-set.ts
996
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
997
+ import { join as join2 } from "path";
986
998
  var HOSTED_RUNTIMES = new Set(["hosted"]);
987
999
  var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
988
1000
  function normalizeMarker(value) {
@@ -994,6 +1006,16 @@ function isHostedMetadataPackage(pkg) {
994
1006
  return false;
995
1007
  return HOSTED_RUNTIMES.has(normalizeMarker(skills.runtime)) || HOSTED_SOURCES.has(normalizeMarker(skills.source));
996
1008
  }
1009
+ function isHostedMetadataSkillDir(skillDir) {
1010
+ const pkgPath = join2(skillDir, "package.json");
1011
+ if (!existsSync2(pkgPath))
1012
+ return false;
1013
+ try {
1014
+ return isHostedMetadataPackage(JSON.parse(readFileSync2(pkgPath, "utf8")));
1015
+ } catch {
1016
+ return false;
1017
+ }
1018
+ }
997
1019
  var HOSTED_METADATA_SET_EMPTY_ERROR = [
998
1020
  "The hosted metadata skill set is empty, but the packaging guards that depend on it",
999
1021
  "only mean anything while it is non-empty: an empty set makes every one of them pass",
@@ -1010,6 +1032,8 @@ var BRACE_SOURCE_EXCLUSION = new RegExp(`^!skills/\\{(${SLUG}(?:,${SLUG})+)\\}/s
1010
1032
  var SINGLE_SOURCE_EXCLUSION = new RegExp(`^!skills/(${SLUG})/src$`);
1011
1033
 
1012
1034
  // src/lib/skill-validation.ts
1035
+ import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1036
+ import { isAbsolute, join as join3, normalize } from "path";
1013
1037
  var VALID_SKILL_KINDS = ["executable", "instruction"];
1014
1038
  var DOC_FILES = ["SKILL.md", "README.md", "CLAUDE.md"];
1015
1039
  var RESERVED_SKILL_ENTRIES = new Set([
@@ -1063,7 +1087,7 @@ function add(target, code, message) {
1063
1087
  target.push({ code, message });
1064
1088
  }
1065
1089
  function readJsonFile(path) {
1066
- return JSON.parse(readFileSync2(path, "utf-8"));
1090
+ return JSON.parse(readFileSync3(path, "utf-8"));
1067
1091
  }
1068
1092
  function asRecord(value) {
1069
1093
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
@@ -1147,7 +1171,7 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
1147
1171
  binCommands: [],
1148
1172
  docFiles: []
1149
1173
  };
1150
- if (!existsSync2(skillPath)) {
1174
+ if (!existsSync3(skillPath)) {
1151
1175
  add(issues, "skill.dir_missing", `Skill directory not found: ${skillPath}`);
1152
1176
  return {
1153
1177
  name: bareName,
@@ -1161,8 +1185,8 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
1161
1185
  if (!VALID_BIN_COMMAND.test(bareName)) {
1162
1186
  add(issues, "skill.name_invalid", `Skill name '${bareName}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
1163
1187
  }
1164
- for (const entry of readdirSync2(skillPath).sort()) {
1165
- const entryPath = join2(skillPath, entry);
1188
+ for (const entry of readdirSync3(skillPath).sort()) {
1189
+ const entryPath = join3(skillPath, entry);
1166
1190
  if (RESERVED_SKILL_ENTRIES.has(entry)) {
1167
1191
  add(issues, "skill.reserved_file", `Reserved file '${entry}' is not allowed in skill packages`);
1168
1192
  }
@@ -1174,15 +1198,15 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
1174
1198
  }
1175
1199
  }
1176
1200
  for (const docFile of DOC_FILES) {
1177
- if (existsSync2(join2(skillPath, docFile)))
1201
+ if (existsSync3(join3(skillPath, docFile)))
1178
1202
  metadata.docFiles.push(docFile);
1179
1203
  }
1180
1204
  if (metadata.docFiles.length === 0) {
1181
1205
  add(issues, "skill.docs_missing", "Missing documentation file: expected SKILL.md, README.md, or CLAUDE.md");
1182
1206
  }
1183
- const skillMdPath = join2(skillPath, "SKILL.md");
1184
- if (existsSync2(skillMdPath)) {
1185
- const frontmatter = parseSkillFrontmatter(readFileSync2(skillMdPath, "utf-8"));
1207
+ const skillMdPath = join3(skillPath, "SKILL.md");
1208
+ if (existsSync3(skillMdPath)) {
1209
+ const frontmatter = parseSkillFrontmatter(readFileSync3(skillMdPath, "utf-8"));
1186
1210
  if (!frontmatter) {
1187
1211
  add(warnings, "skill.frontmatter_missing", "SKILL.md has no YAML frontmatter");
1188
1212
  } else {
@@ -1224,8 +1248,8 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
1224
1248
  }
1225
1249
  metadata.kind = resolvedKind;
1226
1250
  const isInstruction = resolvedKind === "instruction";
1227
- const pkgPath = join2(skillPath, "package.json");
1228
- if (!existsSync2(pkgPath)) {
1251
+ const pkgPath = join3(skillPath, "package.json");
1252
+ if (!existsSync3(pkgPath)) {
1229
1253
  if (!isInstruction)
1230
1254
  add(issues, "package.missing", "Missing package.json");
1231
1255
  } else {
@@ -1283,10 +1307,10 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
1283
1307
  add(issues, "package.bin_target_unsafe", `package.json bin '${command}' target '${target}' must stay inside the skill directory`);
1284
1308
  continue;
1285
1309
  }
1286
- const targetPath = join2(skillPath, target);
1287
- if (!existsSync2(targetPath)) {
1310
+ const targetPath = join3(skillPath, target);
1311
+ if (!existsSync3(targetPath)) {
1288
1312
  add(warnings, "package.bin_target_missing", `package.json bin '${command}' target '${target}' is not present before build`);
1289
- } else if (statSync2(targetPath).isDirectory()) {
1313
+ } else if (statSync3(targetPath).isDirectory()) {
1290
1314
  add(issues, "package.bin_target_directory", `package.json bin '${command}' target '${target}' must point to a file, not a directory`);
1291
1315
  }
1292
1316
  }
@@ -1301,18 +1325,18 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
1301
1325
  metadata.runtime = "none";
1302
1326
  } else {
1303
1327
  metadata.runtime = hostedMetadata ? "hosted" : "local";
1304
- const srcDir = join2(skillPath, "src");
1328
+ const srcDir = join3(skillPath, "src");
1305
1329
  if (hostedMetadata) {
1306
- if (existsSync2(srcDir)) {
1330
+ if (existsSync3(srcDir)) {
1307
1331
  add(issues, "skill.hosted_source_forbidden", "Hosted metadata skills must not include local implementation source");
1308
1332
  }
1309
- } else if (!existsSync2(srcDir)) {
1333
+ } else if (!existsSync3(srcDir)) {
1310
1334
  add(issues, "skill.src_missing", "Missing src/ directory");
1311
- } else if (!existsSync2(join2(srcDir, "index.ts")) && !existsSync2(join2(srcDir, "index.js"))) {
1335
+ } else if (!existsSync3(join3(srcDir, "index.ts")) && !existsSync3(join3(srcDir, "index.js"))) {
1312
1336
  add(issues, "skill.src_index_missing", "Missing src/index.ts or src/index.js");
1313
1337
  } else {
1314
- const indexPath = existsSync2(join2(srcDir, "index.ts")) ? join2(srcDir, "index.ts") : join2(srcDir, "index.js");
1315
- const size = statSync2(indexPath).size;
1338
+ const indexPath = existsSync3(join3(srcDir, "index.ts")) ? join3(srcDir, "index.ts") : join3(srcDir, "index.js");
1339
+ const size = statSync3(indexPath).size;
1316
1340
  if (size < 50)
1317
1341
  add(warnings, "skill.src_index_minimal", `Source entry point is very small (${size}B)`);
1318
1342
  }
@@ -1335,9 +1359,9 @@ function validateRegistryConsistency(registry, skillsDir) {
1335
1359
  seen.add(name);
1336
1360
  return false;
1337
1361
  })));
1338
- const skillDirs = existsSync2(skillsDir) ? readdirSync2(skillsDir).filter((entry) => {
1339
- const fullPath = join2(skillsDir, entry);
1340
- return !entry.startsWith(".") && entry !== "_common" && statSync2(fullPath).isDirectory();
1362
+ const skillDirs = existsSync3(skillsDir) ? readdirSync3(skillsDir).filter((entry) => {
1363
+ const fullPath = join3(skillsDir, entry);
1364
+ return !entry.startsWith(".") && entry !== "_common" && statSync3(fullPath).isDirectory();
1341
1365
  }) : [];
1342
1366
  const directoryNames = new Set(skillDirs);
1343
1367
  const registryNameSet = new Set(registryNames);
@@ -1353,8 +1377,8 @@ function validateRegistryConsistency(registry, skillsDir) {
1353
1377
 
1354
1378
  // src/lib/skill-hash.ts
1355
1379
  import { createHash } from "crypto";
1356
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1357
- import { join as join3, sep } from "path";
1380
+ import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
1381
+ import { join as join4, sep } from "path";
1358
1382
  var CONTENT_HASH_ALGORITHM = "sha256";
1359
1383
  var HASH_EXCLUDE_DIRS = new Set([".git", "node_modules", "dist", "build", ".turbo"]);
1360
1384
  var HASH_COVERAGE = [
@@ -1413,10 +1437,10 @@ function collectBundleFiles(skillPath) {
1413
1437
  if (seen.has(entry))
1414
1438
  continue;
1415
1439
  seen.add(entry);
1416
- const absolute = join3(skillPath, entry);
1417
- if (!existsSync3(absolute))
1440
+ const absolute = join4(skillPath, entry);
1441
+ if (!existsSync4(absolute))
1418
1442
  continue;
1419
- if (statSync3(absolute).isDirectory())
1443
+ if (statSync4(absolute).isDirectory())
1420
1444
  collectDirectory(files, absolute, entry);
1421
1445
  else
1422
1446
  collectFile(files, absolute, entry);
@@ -1424,14 +1448,14 @@ function collectBundleFiles(skillPath) {
1424
1448
  return files.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
1425
1449
  }
1426
1450
  function collectDirectory(files, dir, rel) {
1427
- for (const entry of readdirSync3(dir).sort()) {
1451
+ for (const entry of readdirSync4(dir).sort()) {
1428
1452
  if (entry.startsWith("."))
1429
1453
  continue;
1430
- const absolute = join3(dir, entry);
1454
+ const absolute = join4(dir, entry);
1431
1455
  const childRel = `${rel}/${entry}`;
1432
1456
  let stats;
1433
1457
  try {
1434
- stats = statSync3(absolute);
1458
+ stats = statSync4(absolute);
1435
1459
  } catch {
1436
1460
  continue;
1437
1461
  }
@@ -1447,7 +1471,7 @@ function collectDirectory(files, dir, rel) {
1447
1471
  }
1448
1472
  }
1449
1473
  function collectFile(files, absolute, rel) {
1450
- const buffer = readFileSync3(absolute);
1474
+ const buffer = readFileSync4(absolute);
1451
1475
  if (rel === "skill.json") {
1452
1476
  files.push({ rel: rel.split(sep).join("/"), content: new TextEncoder().encode(canonicalizeManifest(new TextDecoder().decode(buffer))) });
1453
1477
  return;
@@ -1647,14 +1671,14 @@ function validateRuntimeContract(manifest, issues, strict) {
1647
1671
  // src/lib/portable-skills-files.ts
1648
1672
  import {
1649
1673
  cpSync,
1650
- existsSync as existsSync4,
1674
+ existsSync as existsSync5,
1651
1675
  lstatSync as lstatSync2,
1652
1676
  mkdirSync as mkdirSync2,
1653
- readFileSync as readFileSync4,
1677
+ readFileSync as readFileSync5,
1654
1678
  realpathSync,
1655
1679
  writeFileSync as writeFileSync2
1656
1680
  } from "fs";
1657
- import { basename, dirname as dirname2, join as join4, relative } from "path";
1681
+ import { basename, dirname as dirname2, join as join5, relative } from "path";
1658
1682
  var ANY_SEGMENT_COPY_EXCLUDES = new Set([
1659
1683
  ".git",
1660
1684
  ".DS_Store",
@@ -1697,12 +1721,12 @@ function normalizePortableSkillName(name) {
1697
1721
  return normalized;
1698
1722
  }
1699
1723
  function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
1700
- const skillJsonPath = join4(skillPath, "skill.json");
1701
- const skillMdPath = join4(skillPath, "SKILL.md");
1702
- const pkgPath = join4(skillPath, "package.json");
1703
- const jsonManifest = existsSync4(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
1704
- const frontmatter = existsSync4(skillMdPath) ? parseSkillFrontmatter(readFileSync4(skillMdPath, "utf-8")) ?? undefined : undefined;
1705
- const pkg = existsSync4(pkgPath) ? readJsonObject(pkgPath) : undefined;
1724
+ const skillJsonPath = join5(skillPath, "skill.json");
1725
+ const skillMdPath = join5(skillPath, "SKILL.md");
1726
+ const pkgPath = join5(skillPath, "package.json");
1727
+ const jsonManifest = existsSync5(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
1728
+ const frontmatter = existsSync5(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
1729
+ const pkg = existsSync5(pkgPath) ? readJsonObject(pkgPath) : undefined;
1706
1730
  const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
1707
1731
  const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
1708
1732
  const version = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
@@ -1748,7 +1772,7 @@ function createInstructionManifest(name, options) {
1748
1772
  }
1749
1773
  function writeInstructionSkillTemplate(skillPath, manifest) {
1750
1774
  mkdirSync2(skillPath, { recursive: true });
1751
- writeFileSync2(join4(skillPath, "SKILL.md"), renderInstructionSkillMd(manifest));
1775
+ writeFileSync2(join5(skillPath, "SKILL.md"), renderInstructionSkillMd(manifest));
1752
1776
  writeSkillJsonWithHash(skillPath, manifest);
1753
1777
  }
1754
1778
  function renderInstructionSkillMd(manifest) {
@@ -1797,12 +1821,12 @@ function createPortableManifest(name, options) {
1797
1821
  };
1798
1822
  }
1799
1823
  function writePortableSkillTemplate(skillPath, manifest) {
1800
- mkdirSync2(join4(skillPath, "src"), { recursive: true });
1801
- writeFileSync2(join4(skillPath, "SKILL.md"), renderSkillMd(manifest));
1802
- writeFileSync2(join4(skillPath, "AGENTS.md"), renderAgentsMd(manifest));
1803
- writeFileSync2(join4(skillPath, "package.json"), renderPackageJson(manifest));
1804
- writeFileSync2(join4(skillPath, "tsconfig.json"), renderTsconfig());
1805
- writeFileSync2(join4(skillPath, "src", "index.ts"), renderEntrypoint(manifest));
1824
+ mkdirSync2(join5(skillPath, "src"), { recursive: true });
1825
+ writeFileSync2(join5(skillPath, "SKILL.md"), renderSkillMd(manifest));
1826
+ writeFileSync2(join5(skillPath, "AGENTS.md"), renderAgentsMd(manifest));
1827
+ writeFileSync2(join5(skillPath, "package.json"), renderPackageJson(manifest));
1828
+ writeFileSync2(join5(skillPath, "tsconfig.json"), renderTsconfig());
1829
+ writeFileSync2(join5(skillPath, "src", "index.ts"), renderEntrypoint(manifest));
1806
1830
  writeSkillJsonWithHash(skillPath, manifest);
1807
1831
  }
1808
1832
  function fillContractDefaults(manifest, entrypoint) {
@@ -1827,7 +1851,7 @@ function writeSkillJsonWithHash(skillPath, manifest) {
1827
1851
  content_hash: undefined
1828
1852
  }
1829
1853
  };
1830
- writeFileSync2(join4(skillPath, "skill.json"), `${JSON.stringify({ ...existing, ...renderSkillJsonObject(withoutHash) }, null, 2)}
1854
+ writeFileSync2(join5(skillPath, "skill.json"), `${JSON.stringify({ ...existing, ...renderSkillJsonObject(withoutHash) }, null, 2)}
1831
1855
  `);
1832
1856
  const hash = computeContentHash(skillPath);
1833
1857
  const withHash = {
@@ -1837,16 +1861,16 @@ function writeSkillJsonWithHash(skillPath, manifest) {
1837
1861
  content_hash: hash
1838
1862
  }
1839
1863
  };
1840
- writeFileSync2(join4(skillPath, "skill.json"), `${JSON.stringify({ ...existing, ...renderSkillJsonObject(withHash) }, null, 2)}
1864
+ writeFileSync2(join5(skillPath, "skill.json"), `${JSON.stringify({ ...existing, ...renderSkillJsonObject(withHash) }, null, 2)}
1841
1865
  `);
1842
1866
  return withHash;
1843
1867
  }
1844
1868
  function readExistingSkillJson(skillPath) {
1845
- const path = join4(skillPath, "skill.json");
1846
- if (!existsSync4(path))
1869
+ const path = join5(skillPath, "skill.json");
1870
+ if (!existsSync5(path))
1847
1871
  return {};
1848
1872
  try {
1849
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
1873
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
1850
1874
  return isRecord2(parsed) ? parsed : {};
1851
1875
  } catch {
1852
1876
  return {};
@@ -1876,28 +1900,28 @@ function ensurePortableSkillFiles(skillPath, manifest) {
1876
1900
  tags: next.tags?.length ? next.tags : ["custom", next.name]
1877
1901
  };
1878
1902
  const entry = next.commands[0]?.entry ?? "src/index.ts";
1879
- if (entry && !existsSync4(join4(skillPath, entry))) {
1880
- mkdirSync2(dirname2(join4(skillPath, entry)), { recursive: true });
1881
- writeFileSync2(join4(skillPath, entry), renderEntrypoint(next));
1903
+ if (entry && !existsSync5(join5(skillPath, entry))) {
1904
+ mkdirSync2(dirname2(join5(skillPath, entry)), { recursive: true });
1905
+ writeFileSync2(join5(skillPath, entry), renderEntrypoint(next));
1882
1906
  }
1883
- if (!existsSync4(join4(skillPath, "SKILL.md")))
1884
- writeFileSync2(join4(skillPath, "SKILL.md"), renderSkillMd(next));
1907
+ if (!existsSync5(join5(skillPath, "SKILL.md")))
1908
+ writeFileSync2(join5(skillPath, "SKILL.md"), renderSkillMd(next));
1885
1909
  else
1886
- writeFileSync2(join4(skillPath, "SKILL.md"), ensureSkillMdFrontmatter(readFileSync4(join4(skillPath, "SKILL.md"), "utf-8"), next));
1887
- if (!existsSync4(join4(skillPath, "AGENTS.md")))
1888
- writeFileSync2(join4(skillPath, "AGENTS.md"), renderAgentsMd(next));
1910
+ writeFileSync2(join5(skillPath, "SKILL.md"), ensureSkillMdFrontmatter(readFileSync5(join5(skillPath, "SKILL.md"), "utf-8"), next));
1911
+ if (!existsSync5(join5(skillPath, "AGENTS.md")))
1912
+ writeFileSync2(join5(skillPath, "AGENTS.md"), renderAgentsMd(next));
1889
1913
  ensurePackageJson(skillPath, next);
1890
- if (!existsSync4(join4(skillPath, "tsconfig.json")))
1891
- writeFileSync2(join4(skillPath, "tsconfig.json"), renderTsconfig());
1914
+ if (!existsSync5(join5(skillPath, "tsconfig.json")))
1915
+ writeFileSync2(join5(skillPath, "tsconfig.json"), renderTsconfig());
1892
1916
  writeSkillJsonWithHash(skillPath, next);
1893
1917
  return readPortableSkillManifest(skillPath, next.name);
1894
1918
  }
1895
1919
  function ensurePackageJson(skillPath, manifest) {
1896
- const pkgPath = join4(skillPath, "package.json");
1920
+ const pkgPath = join5(skillPath, "package.json");
1897
1921
  const first = manifest.commands[0] ?? { name: manifest.name, entry: "src/index.ts" };
1898
1922
  const commandName = normalizePortableSkillName(first.name || manifest.name);
1899
1923
  const entry = (first.entry ?? "src/index.ts").replace(/^\.\//, "");
1900
- if (!existsSync4(pkgPath)) {
1924
+ if (!existsSync5(pkgPath)) {
1901
1925
  writeFileSync2(pkgPath, renderPackageJson(manifest));
1902
1926
  return;
1903
1927
  }
@@ -1940,8 +1964,8 @@ function ensureInstructionSkillFiles(skillPath, manifest) {
1940
1964
  inputs: [],
1941
1965
  commands: []
1942
1966
  };
1943
- if (!existsSync4(join4(skillPath, "SKILL.md"))) {
1944
- writeFileSync2(join4(skillPath, "SKILL.md"), renderSkillMd(next));
1967
+ if (!existsSync5(join5(skillPath, "SKILL.md"))) {
1968
+ writeFileSync2(join5(skillPath, "SKILL.md"), renderSkillMd(next));
1945
1969
  }
1946
1970
  writeSkillJsonWithHash(skillPath, next);
1947
1971
  return readPortableSkillManifest(skillPath, next.name);
@@ -2192,7 +2216,7 @@ function inferPackageCommands(pkg, fallbackName) {
2192
2216
  return;
2193
2217
  }
2194
2218
  function readJsonObject(path) {
2195
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
2219
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
2196
2220
  if (!isRecord2(parsed))
2197
2221
  throw new Error(`${basename(path)} must contain a JSON object`);
2198
2222
  return parsed;
@@ -2233,33 +2257,36 @@ var LEGACY_CUSTOM_DIRNAME = "custom";
2233
2257
  function getPortableSkillsRoot(options = {}) {
2234
2258
  if (options.rootDir)
2235
2259
  return options.rootDir;
2236
- const appDir = options.homeDir ? join5(options.homeDir, ".hasna", "skills") : getDataDir();
2237
- const installed = join5(appDir, INSTALLED_SKILLS_DIRNAME);
2260
+ const appDir = options.homeDir ? join6(options.homeDir, ".hasna", "skills") : getDataDir();
2261
+ const cache = join6(appDir, SKILLS_CACHE_DIRNAME);
2262
+ if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache))
2263
+ return cache;
2264
+ const installed = join6(appDir, INSTALLED_SKILLS_DIRNAME);
2238
2265
  migrateLegacySkillLayout(appDir, installed);
2239
2266
  return installed;
2240
2267
  }
2241
2268
  function looksLikeSkillDirectory(path) {
2242
2269
  if (!safeIsDirectory(path))
2243
2270
  return false;
2244
- return existsSync5(join5(path, "SKILL.md")) || existsSync5(join5(path, "skill.json")) || existsSync5(join5(path, "package.json"));
2271
+ return existsSync6(join6(path, "SKILL.md")) || existsSync6(join6(path, "skill.json")) || existsSync6(join6(path, "package.json"));
2245
2272
  }
2246
2273
  function migrateLegacySkillLayout(appDir, installed) {
2247
2274
  if (!safeIsDirectory(appDir))
2248
2275
  return;
2249
2276
  const candidates = [];
2250
2277
  try {
2251
- for (const entry of readdirSync4(appDir)) {
2278
+ for (const entry of readdirSync5(appDir)) {
2252
2279
  if (entry.startsWith(".") || entry === INSTALLED_SKILLS_DIRNAME)
2253
2280
  continue;
2254
- const path = join5(appDir, entry);
2281
+ const path = join6(appDir, entry);
2255
2282
  if (entry === LEGACY_CUSTOM_DIRNAME) {
2256
2283
  if (!safeIsDirectory(path))
2257
2284
  continue;
2258
2285
  try {
2259
- for (const nested of readdirSync4(path)) {
2286
+ for (const nested of readdirSync5(path)) {
2260
2287
  if (nested.startsWith("."))
2261
2288
  continue;
2262
- const nestedPath = join5(path, nested);
2289
+ const nestedPath = join6(path, nested);
2263
2290
  if (looksLikeSkillDirectory(nestedPath))
2264
2291
  candidates.push({ from: nestedPath, name: nested });
2265
2292
  }
@@ -2273,10 +2300,10 @@ function migrateLegacySkillLayout(appDir, installed) {
2273
2300
  return;
2274
2301
  }
2275
2302
  for (const { from, name } of candidates) {
2276
- const target = join5(installed, name);
2277
- if (existsSync5(target))
2303
+ const target = join6(installed, name);
2304
+ if (existsSync6(target))
2278
2305
  continue;
2279
- const staging = join5(installed, `.migrating-${name}-${process.pid}`);
2306
+ const staging = join6(installed, `.migrating-${name}-${process.pid}`);
2280
2307
  try {
2281
2308
  rmSync(staging, { recursive: true, force: true });
2282
2309
  cpSync2(from, staging, { recursive: true, errorOnExist: false });
@@ -2289,7 +2316,7 @@ function migrateLegacySkillLayout(appDir, installed) {
2289
2316
  }
2290
2317
  }
2291
2318
  function getPortableSkillPath(name, options = {}) {
2292
- return join5(getPortableSkillsRoot(options), normalizePortableSkillName(name));
2319
+ return join6(getPortableSkillsRoot(options), normalizePortableSkillName(name));
2293
2320
  }
2294
2321
  function findPortableSkill(name, options = {}) {
2295
2322
  let normalized;
@@ -2299,7 +2326,7 @@ function findPortableSkill(name, options = {}) {
2299
2326
  return null;
2300
2327
  }
2301
2328
  const path = getPortableSkillPath(normalized, options);
2302
- if (!existsSync5(path) || !statSync5(path).isDirectory())
2329
+ if (!existsSync6(path) || !statSync6(path).isDirectory())
2303
2330
  return null;
2304
2331
  try {
2305
2332
  return summarizePortableSkill(path, normalized);
@@ -2312,10 +2339,10 @@ function listPortableSkills(options = {}) {
2312
2339
  if (!safeIsDirectory(root))
2313
2340
  return [];
2314
2341
  const skills = [];
2315
- for (const entry of readdirSync4(root).sort()) {
2342
+ for (const entry of readdirSync5(root).sort()) {
2316
2343
  if (entry.startsWith("."))
2317
2344
  continue;
2318
- const path = join5(root, entry);
2345
+ const path = join6(root, entry);
2319
2346
  if (!safeIsDirectory(path))
2320
2347
  continue;
2321
2348
  try {
@@ -2337,7 +2364,8 @@ function listPortableSkillMetas(options = {}) {
2337
2364
  tags: manifest.tags || ["custom"],
2338
2365
  version: skill.version,
2339
2366
  ...manifest.kind ? { kind: manifest.kind } : {},
2340
- source: "custom"
2367
+ source: "custom",
2368
+ ...isHostedMetadataSkillDir(skill.path) ? { serverOwned: true } : {}
2341
2369
  };
2342
2370
  });
2343
2371
  }
@@ -2348,8 +2376,8 @@ function isOfficialSkillName(name) {
2348
2376
  function scaffoldPortableSkill(name, options = {}) {
2349
2377
  const skillName = normalizePortableSkillName(name);
2350
2378
  const root = getPortableSkillsRoot(options);
2351
- const skillPath = join5(root, skillName);
2352
- if (existsSync5(skillPath)) {
2379
+ const skillPath = join6(root, skillName);
2380
+ if (existsSync6(skillPath)) {
2353
2381
  if (!options.overwrite)
2354
2382
  throw new Error(`Skill '${skillName}' already exists at ${skillPath}`);
2355
2383
  rmSync(skillPath, { recursive: true, force: true });
@@ -2367,7 +2395,7 @@ function scaffoldPortableSkill(name, options = {}) {
2367
2395
  }
2368
2396
  function portPortableSkillDirectory(sourceDir, options = {}) {
2369
2397
  const absoluteSource = normalize2(sourceDir);
2370
- if (!existsSync5(absoluteSource) || !statSync5(absoluteSource).isDirectory()) {
2398
+ if (!existsSync6(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
2371
2399
  throw new Error(`Import directory not found: ${sourceDir}`);
2372
2400
  }
2373
2401
  const continueOnError = options.continueOnError ?? true;
@@ -2378,9 +2406,9 @@ function portPortableSkillDirectory(sourceDir, options = {}) {
2378
2406
  };
2379
2407
  const imported = [];
2380
2408
  const skipped = [];
2381
- const entries = readdirSync4(absoluteSource, { withFileTypes: true }).map((entry) => entry.name).filter((entryName) => !entryName.startsWith(".")).sort();
2409
+ const entries = readdirSync5(absoluteSource, { withFileTypes: true }).map((entry) => entry.name).filter((entryName) => !entryName.startsWith(".")).sort();
2382
2410
  for (const entryName of entries) {
2383
- const childPath = join5(absoluteSource, entryName);
2411
+ const childPath = join6(absoluteSource, entryName);
2384
2412
  if (!safeIsDirectory(childPath))
2385
2413
  continue;
2386
2414
  if (!isSkillCandidate(childPath)) {
@@ -2409,11 +2437,11 @@ function portPortableSkillDirectory(sourceDir, options = {}) {
2409
2437
  };
2410
2438
  }
2411
2439
  function isSkillCandidate(dir) {
2412
- return existsSync5(join5(dir, "SKILL.md")) || existsSync5(join5(dir, "skill.json")) || existsSync5(join5(dir, "package.json"));
2440
+ return existsSync6(join6(dir, "SKILL.md")) || existsSync6(join6(dir, "skill.json")) || existsSync6(join6(dir, "package.json"));
2413
2441
  }
2414
2442
  function portPortableSkill(sourcePath, options = {}) {
2415
2443
  const absoluteSource = normalize2(sourcePath);
2416
- if (!existsSync5(absoluteSource) || !statSync5(absoluteSource).isDirectory()) {
2444
+ if (!existsSync6(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
2417
2445
  throw new Error(`Skill source directory not found: ${sourcePath}`);
2418
2446
  }
2419
2447
  const inferred = readPortableSkillManifest(absoluteSource, basename2(absoluteSource));
@@ -2425,8 +2453,8 @@ function portPortableSkill(sourcePath, options = {}) {
2425
2453
  throw new Error(`${via} Importing it would shadow the official '${skillName}'. ` + `Pass --name to choose a different name, or --allow-shadow to override deliberately.`);
2426
2454
  }
2427
2455
  const root = getPortableSkillsRoot(options);
2428
- const destination = join5(root, skillName);
2429
- if (existsSync5(destination)) {
2456
+ const destination = join6(root, skillName);
2457
+ if (existsSync6(destination)) {
2430
2458
  if (!options.overwrite)
2431
2459
  throw new Error(`Skill '${skillName}' already exists at ${destination}`);
2432
2460
  rmSync(destination, { recursive: true, force: true });
@@ -2448,19 +2476,10 @@ function safeNormalizeName(name) {
2448
2476
  return;
2449
2477
  }
2450
2478
  }
2451
- function writeCorpusSkill(input, options = {}) {
2452
- const name = normalizePortableSkillName(input.name);
2453
- const root = getPortableSkillsRoot(options);
2454
- const skillPath = join5(root, name);
2455
- const created = !existsSync5(skillPath);
2456
- mkdirSync3(skillPath, { recursive: true });
2457
- const skillMd = input.skillMd.endsWith(`
2458
- `) ? input.skillMd : `${input.skillMd}
2459
- `;
2460
- writeFileSync3(join5(skillPath, "SKILL.md"), skillMd);
2479
+ function buildCorpusManifest(input, name) {
2461
2480
  const frontmatter = parseSkillFrontmatter(input.skillMd) ?? undefined;
2462
2481
  const kind = input.meta?.kind ?? parseSkillKind(frontmatter?.kind) ?? "instruction";
2463
- const manifest = {
2482
+ return {
2464
2483
  $schema: PORTABLE_SKILL_SCHEMA,
2465
2484
  standard: PORTABLE_SKILL_STANDARD,
2466
2485
  name,
@@ -2473,19 +2492,60 @@ function writeCorpusSkill(input, options = {}) {
2473
2492
  inputs: [],
2474
2493
  commands: []
2475
2494
  };
2495
+ }
2496
+ function writeCorpusSkill(input, options = {}) {
2497
+ const name = normalizePortableSkillName(input.name);
2498
+ const root = getPortableSkillsRoot(options);
2499
+ const skillPath = join6(root, name);
2500
+ const created = !existsSync6(skillPath);
2501
+ mkdirSync3(skillPath, { recursive: true });
2502
+ writeFileSync3(join6(skillPath, "SKILL.md"), input.skillMd);
2503
+ const manifest = buildCorpusManifest(input, name);
2476
2504
  writeSkillJsonWithHash(skillPath, manifest);
2477
2505
  return { name, path: skillPath, manifest, created };
2478
2506
  }
2507
+ function installCorpusSkillAtomically(input, options = {}) {
2508
+ const name = normalizePortableSkillName(input.name);
2509
+ const root = getPortableSkillsRoot(options);
2510
+ const target = join6(root, name);
2511
+ const created = !existsSync6(target);
2512
+ mkdirSync3(root, { recursive: true });
2513
+ const staging = mkdtempSync(join6(root, `.pull-${name}-`));
2514
+ let moved = false;
2515
+ let backup = null;
2516
+ try {
2517
+ writeFileSync3(join6(staging, "SKILL.md"), input.skillMd);
2518
+ const manifest = buildCorpusManifest(input, name);
2519
+ writeSkillJsonWithHash(staging, manifest);
2520
+ if (existsSync6(target)) {
2521
+ backup = mkdtempSync(join6(root, `.pull-backup-${name}-`));
2522
+ renameSync(target, join6(backup, name));
2523
+ moved = true;
2524
+ }
2525
+ renameSync(staging, target);
2526
+ if (moved && backup)
2527
+ rmSync(backup, { recursive: true, force: true });
2528
+ return { name, path: target, manifest, created };
2529
+ } catch (error) {
2530
+ rmSync(staging, { recursive: true, force: true });
2531
+ if (moved && backup && existsSync6(join6(backup, name))) {
2532
+ try {
2533
+ renameSync(join6(backup, name), target);
2534
+ } catch {}
2535
+ }
2536
+ throw error;
2537
+ }
2538
+ }
2479
2539
  function validatePortableSkillDirectory(name, skillPath) {
2480
2540
  const normalizedName = normalizePortableSkillName(name);
2481
2541
  const base = validateSkillDirectory(normalizedName, skillPath);
2482
2542
  const issues = [...base.issues];
2483
2543
  const warnings = [...base.warnings];
2484
2544
  let manifest;
2485
- if (existsSync5(skillPath)) {
2486
- const skillJsonPath = join5(skillPath, "skill.json");
2487
- const skillMdPath = join5(skillPath, "SKILL.md");
2488
- if (!existsSync5(skillJsonPath) && !existsSync5(skillMdPath)) {
2545
+ if (existsSync6(skillPath)) {
2546
+ const skillJsonPath = join6(skillPath, "skill.json");
2547
+ const skillMdPath = join6(skillPath, "SKILL.md");
2548
+ if (!existsSync6(skillJsonPath) && !existsSync6(skillMdPath)) {
2489
2549
  add3(issues, "portable.manifest_missing", "Missing portable manifest: expected SKILL.md frontmatter and/or skill.json");
2490
2550
  }
2491
2551
  try {
@@ -2504,7 +2564,7 @@ function validatePortableSkillDirectory(name, skillPath) {
2504
2564
  add3(issues, "portable.version_missing", "Portable manifest missing version");
2505
2565
  }
2506
2566
  const contractIssues = validatePortableManifestContract(manifest, {
2507
- strict: existsSync5(join5(skillPath, "skill.json")),
2567
+ strict: existsSync6(join6(skillPath, "skill.json")),
2508
2568
  skillPath
2509
2569
  });
2510
2570
  for (const issue of contractIssues)
@@ -2540,10 +2600,10 @@ function validatePortableSkillDirectory(name, skillPath) {
2540
2600
  add3(issues, "portable.command_entry_unsafe", `Command '${command.name}' entry '${command.entry}' must stay inside the skill directory`);
2541
2601
  continue;
2542
2602
  }
2543
- const entryPath = join5(skillPath, command.entry);
2544
- if (!existsSync5(entryPath))
2603
+ const entryPath = join6(skillPath, command.entry);
2604
+ if (!existsSync6(entryPath))
2545
2605
  add3(issues, "portable.command_entry_missing", `Command '${command.name}' entry '${command.entry}' is missing`);
2546
- else if (statSync5(entryPath).isDirectory())
2606
+ else if (statSync6(entryPath).isDirectory())
2547
2607
  add3(issues, "portable.command_entry_directory", `Command '${command.name}' entry '${command.entry}' must be a file`);
2548
2608
  }
2549
2609
  }
@@ -2551,7 +2611,7 @@ function validatePortableSkillDirectory(name, skillPath) {
2551
2611
  } catch (error) {
2552
2612
  add3(issues, "portable.manifest_invalid", error.message);
2553
2613
  }
2554
- if (manifest?.kind !== "instruction" && !existsSync5(join5(skillPath, "AGENTS.md"))) {
2614
+ if (manifest?.kind !== "instruction" && !existsSync6(join6(skillPath, "AGENTS.md"))) {
2555
2615
  add3(issues, "portable.agents_missing", "Missing AGENTS.md with build-out instructions for coding agents");
2556
2616
  }
2557
2617
  }
@@ -2587,13 +2647,13 @@ async function runPortableSkill(name, args, options = {}) {
2587
2647
  if (!isSafeRelativePath2(command.entry)) {
2588
2648
  return { exitCode: 1, error: `Portable skill '${name}' command entry is unsafe` };
2589
2649
  }
2590
- const entryPath = join5(skill.path, command.entry);
2591
- if (!existsSync5(entryPath)) {
2650
+ const entryPath = join6(skill.path, command.entry);
2651
+ if (!existsSync6(entryPath)) {
2592
2652
  return { exitCode: 1, error: `Entry point '${command.entry}' not found in portable skill '${name}'` };
2593
2653
  }
2594
- const pkgPath = join5(skill.path, "package.json");
2595
- const nodeModules = join5(skill.path, "node_modules");
2596
- if (existsSync5(pkgPath) && !existsSync5(nodeModules) && hasPackageDependencies(pkgPath)) {
2654
+ const pkgPath = join6(skill.path, "package.json");
2655
+ const nodeModules = join6(skill.path, "node_modules");
2656
+ if (existsSync6(pkgPath) && !existsSync6(nodeModules) && hasPackageDependencies(pkgPath)) {
2597
2657
  const install = Bun.spawn(["bun", "install", "--no-save"], {
2598
2658
  cwd: skill.path,
2599
2659
  stdout: "pipe",
@@ -2633,7 +2693,7 @@ function summarizePortableSkill(skillPath, fallbackName) {
2633
2693
  }
2634
2694
  function safeIsDirectory(path) {
2635
2695
  try {
2636
- return statSync5(path).isDirectory();
2696
+ return statSync6(path).isDirectory();
2637
2697
  } catch {
2638
2698
  return false;
2639
2699
  }
@@ -2862,20 +2922,20 @@ function parseSkillMdFrontmatter(content) {
2862
2922
  return Object.keys(result).length > 0 ? result : null;
2863
2923
  }
2864
2924
  function discoverSkillsInDir(dir, source = "custom") {
2865
- if (!existsSync6(dir))
2925
+ if (!existsSync7(dir))
2866
2926
  return [];
2867
2927
  const result = [];
2868
2928
  try {
2869
- const entries = readdirSync5(dir, { withFileTypes: true });
2929
+ const entries = readdirSync6(dir, { withFileTypes: true });
2870
2930
  for (const entry of entries) {
2871
2931
  if (!entry.isDirectory())
2872
2932
  continue;
2873
- const skillMdPath = join6(dir, entry.name, "SKILL.md");
2874
- if (!existsSync6(skillMdPath))
2933
+ const skillMdPath = join7(dir, entry.name, "SKILL.md");
2934
+ if (!existsSync7(skillMdPath))
2875
2935
  continue;
2876
2936
  let content;
2877
2937
  try {
2878
- content = readFileSync5(skillMdPath, "utf-8");
2938
+ content = readFileSync6(skillMdPath, "utf-8");
2879
2939
  } catch {
2880
2940
  continue;
2881
2941
  }
@@ -2890,6 +2950,7 @@ function discoverSkillsInDir(dir, source = "custom") {
2890
2950
  category: fm.category || "Development Tools",
2891
2951
  tags: fm.tags || [],
2892
2952
  ...fm.kind ? { kind: fm.kind } : {},
2953
+ ...isHostedMetadataSkillDir(join7(dir, entry.name)) ? { serverOwned: true } : {},
2893
2954
  source
2894
2955
  });
2895
2956
  }
@@ -2898,20 +2959,20 @@ function discoverSkillsInDir(dir, source = "custom") {
2898
2959
  }
2899
2960
  function findExtensionSkillPath(name) {
2900
2961
  const config = loadConfig();
2901
- if (!config.extensionsDir || !existsSync6(config.extensionsDir))
2962
+ if (!config.extensionsDir || !existsSync7(config.extensionsDir))
2902
2963
  return null;
2903
2964
  try {
2904
- const entries = readdirSync5(config.extensionsDir, { withFileTypes: true });
2965
+ const entries = readdirSync6(config.extensionsDir, { withFileTypes: true });
2905
2966
  for (const entry of entries) {
2906
2967
  if (!entry.isDirectory())
2907
2968
  continue;
2908
- const skillDir = join6(config.extensionsDir, entry.name);
2909
- const skillMdPath = join6(skillDir, "SKILL.md");
2910
- if (!existsSync6(skillMdPath))
2969
+ const skillDir = join7(config.extensionsDir, entry.name);
2970
+ const skillMdPath = join7(skillDir, "SKILL.md");
2971
+ if (!existsSync7(skillMdPath))
2911
2972
  continue;
2912
2973
  let content;
2913
2974
  try {
2914
- content = readFileSync5(skillMdPath, "utf-8");
2975
+ content = readFileSync6(skillMdPath, "utf-8");
2915
2976
  } catch {
2916
2977
  continue;
2917
2978
  }
@@ -2944,7 +3005,7 @@ function loadRegistry(cwd) {
2944
3005
  const official = SKILLS.map((s) => ({ ...s, source: "official" }));
2945
3006
  const extensions = config.extensionsDir ? discoverSkillsInDir(config.extensionsDir, "extension") : [];
2946
3007
  const portableCustom = listPortableSkillMetas();
2947
- const legacyCustom = discoverSkillsInDir(join6(dataDir, "custom"));
3008
+ const legacyCustom = discoverSkillsInDir(join7(dataDir, "custom"));
2948
3009
  const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
2949
3010
  registryCache = mergeSkillRegistryLists(official, extensions, globalCustom);
2950
3011
  registryCacheTime = now;
@@ -2991,7 +3052,7 @@ function getAllTags() {
2991
3052
  return Array.from(tagSet).sort();
2992
3053
  }
2993
3054
  // src/lib/installer.ts
2994
- import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
3055
+ import { existsSync as existsSync10, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
2995
3056
  import { dirname as dirname5, join as join10 } from "path";
2996
3057
  import { homedir as homedir3 } from "os";
2997
3058
  import { fileURLToPath } from "url";
@@ -3000,36 +3061,19 @@ import { fileURLToPath } from "url";
3000
3061
  import {
3001
3062
  cpSync as cpSync3,
3002
3063
  existsSync as existsSync8,
3003
- mkdirSync as mkdirSync5,
3004
- mkdtempSync,
3005
- readFileSync as readFileSync6,
3064
+ mkdirSync as mkdirSync4,
3065
+ mkdtempSync as mkdtempSync2,
3066
+ readFileSync as readFileSync7,
3006
3067
  readdirSync as readdirSync7,
3007
- renameSync as renameSync3,
3008
- rmSync as rmSync3,
3068
+ renameSync as renameSync2,
3069
+ rmSync as rmSync2,
3009
3070
  statSync as statSync7,
3010
- writeFileSync as writeFileSync5
3071
+ writeFileSync as writeFileSync4
3011
3072
  } from "fs";
3012
3073
  import { homedir as homedir2 } from "os";
3013
3074
  import { basename as basename3, dirname as dirname4, join as join8 } from "path";
3014
-
3015
3075
  // src/lib/home-migration.ts
3016
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync6, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync6, writeFileSync as writeFileSync4 } from "fs";
3017
- import { join as join7 } from "path";
3018
- var SKILLS_CACHE_DIRNAME = "skills";
3019
- var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
3020
- function layoutMigrationRecordPath(appDir) {
3021
- return join7(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD);
3022
- }
3023
- function isOwnerLayoutMigrated(appDir) {
3024
- return existsSync7(layoutMigrationRecordPath(appDir));
3025
- }
3026
3076
  function resolveCorpusRoot(options = {}) {
3027
- if (options.rootDir)
3028
- return options.rootDir;
3029
- const appDir = options.homeDir ? join7(options.homeDir, ".hasna", "skills") : getDataDir();
3030
- const cache = join7(appDir, SKILLS_CACHE_DIRNAME);
3031
- if (isOwnerLayoutMigrated(appDir) && existsSync7(cache))
3032
- return cache;
3033
3077
  return getPortableSkillsRoot(options);
3034
3078
  }
3035
3079
 
@@ -3075,6 +3119,10 @@ ${lines.join(`
3075
3119
  ---
3076
3120
  ${body}`;
3077
3121
  }
3122
+ var POINTER_MARKER_PHRASE = "This is an executable skill from the @hasna/skills catalog";
3123
+ function isPointerSkillMd(markdown) {
3124
+ return markdown.includes(POINTER_MARKER_PHRASE) && /^kind:\s*executable\b/m.test(markdown);
3125
+ }
3078
3126
  function pointerSkillMd(name, description) {
3079
3127
  const display = name.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
3080
3128
  return [
@@ -3100,7 +3148,7 @@ function resolveSyncCorpus(options = {}) {
3100
3148
  if (explicit) {
3101
3149
  const roots = packageSourceRoots(explicit);
3102
3150
  if (roots.length === 0) {
3103
- throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills/ and/or agent-skills/`);
3151
+ throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills/`);
3104
3152
  }
3105
3153
  return { roots, source: "source" };
3106
3154
  }
@@ -3108,7 +3156,7 @@ function resolveSyncCorpus(options = {}) {
3108
3156
  }
3109
3157
  function packageSourceRoots(source) {
3110
3158
  const roots = [];
3111
- for (const sub of ["skills", "agent-skills"]) {
3159
+ for (const sub of ["skills"]) {
3112
3160
  const candidate = join8(source, sub);
3113
3161
  if (existsSync8(candidate) && isDirectory(candidate))
3114
3162
  roots.push(candidate);
@@ -3177,7 +3225,7 @@ function syncSkillsToAgents(options = {}) {
3177
3225
  }
3178
3226
  for (const skill of targets) {
3179
3227
  const manifest = readPortableSkillManifest(skill.path, skill.name);
3180
- const kind = manifest.kind ?? "executable";
3228
+ const kind = manifest.kind;
3181
3229
  const sourceMd = sourceSkillMd(skill.path, skill.name, manifest.description, kind, source === "source");
3182
3230
  for (const agent of agents) {
3183
3231
  const adapted = adaptSkillMdForAgent(sourceMd, agent);
@@ -3233,12 +3281,27 @@ function writeManagedSkillDir(dir, skillMd, options) {
3233
3281
  reason: "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
3234
3282
  };
3235
3283
  }
3284
+ if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
3285
+ let existingIsStub = false;
3286
+ try {
3287
+ existingIsStub = isPointerSkillMd(readFileSync7(skillMdPath, "utf-8"));
3288
+ } catch {
3289
+ existingIsStub = false;
3290
+ }
3291
+ if (!existingIsStub) {
3292
+ return {
3293
+ action: "skip",
3294
+ path: skillMdPath,
3295
+ reason: "refusing to replace a content-bearing managed home with an executable pointer stub (the corpus entry lacks kind: instruction); pass --force to overwrite"
3296
+ };
3297
+ }
3298
+ }
3236
3299
  const action = dirExists ? "update" : "create";
3237
3300
  if (options.dryRun)
3238
3301
  return { action, path: skillMdPath };
3239
3302
  const parentDir = dirname4(dir);
3240
- mkdirSync5(parentDir, { recursive: true });
3241
- const transactionDir = mkdtempSync(join8(parentDir, `.hasna-skills-write-${basename3(dir)}-`));
3303
+ mkdirSync4(parentDir, { recursive: true });
3304
+ const transactionDir = mkdtempSync2(join8(parentDir, `.hasna-skills-write-${basename3(dir)}-`));
3242
3305
  const candidateDir = join8(transactionDir, "candidate");
3243
3306
  const backupDir = join8(transactionDir, "backup");
3244
3307
  const candidateSkillMdPath = join8(candidateDir, "SKILL.md");
@@ -3249,19 +3312,19 @@ function writeManagedSkillDir(dir, skillMd, options) {
3249
3312
  source: options.source ?? "corpus",
3250
3313
  syncedAt: new Date().toISOString()
3251
3314
  };
3252
- const renameDirectory = options.renameDirectory ?? renameSync3;
3315
+ const renameDirectory = options.renameDirectory ?? renameSync2;
3253
3316
  let originalMoved = false;
3254
3317
  let preserveTransaction = false;
3255
3318
  try {
3256
3319
  if (options.resourceDir) {
3257
3320
  cpSync3(options.resourceDir, candidateDir, { recursive: true, force: true });
3258
3321
  } else {
3259
- mkdirSync5(candidateDir, { recursive: true });
3322
+ mkdirSync4(candidateDir, { recursive: true });
3260
3323
  }
3261
- writeFileSync5(candidateSkillMdPath, skillMd.endsWith(`
3324
+ writeFileSync4(candidateSkillMdPath, skillMd.endsWith(`
3262
3325
  `) ? skillMd : `${skillMd}
3263
3326
  `);
3264
- writeFileSync5(candidateMarkerPath, `${JSON.stringify(marker, null, 2)}
3327
+ writeFileSync4(candidateMarkerPath, `${JSON.stringify(marker, null, 2)}
3265
3328
  `);
3266
3329
  if (dirExists) {
3267
3330
  originalMoved = true;
@@ -3272,7 +3335,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
3272
3335
  if (originalMoved && existsSync8(backupDir)) {
3273
3336
  try {
3274
3337
  if (existsSync8(dir))
3275
- rmSync3(dir, { recursive: true, force: true });
3338
+ rmSync2(dir, { recursive: true, force: true });
3276
3339
  renameDirectory(backupDir, dir);
3277
3340
  originalMoved = false;
3278
3341
  } catch (rollbackError) {
@@ -3284,7 +3347,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
3284
3347
  } finally {
3285
3348
  if (!preserveTransaction) {
3286
3349
  try {
3287
- rmSync3(transactionDir, { recursive: true, force: true });
3350
+ rmSync2(transactionDir, { recursive: true, force: true });
3288
3351
  } catch {}
3289
3352
  }
3290
3353
  }
@@ -3294,14 +3357,14 @@ function removeManagedAgentSkill(skill, agent, homeDir = homedir2()) {
3294
3357
  const dir = join8(agentGlobalSkillsDir(agent, homeDir), skill);
3295
3358
  if (!existsSync8(join8(dir, SYNC_MARKER_FILE)))
3296
3359
  return false;
3297
- rmSync3(dir, { recursive: true, force: true });
3360
+ rmSync2(dir, { recursive: true, force: true });
3298
3361
  return true;
3299
3362
  }
3300
3363
  function sourceSkillMd(skillPath, name, description, kind, preferBundledDocs = false) {
3301
- if (kind === "instruction" || preferBundledDocs) {
3364
+ if (kind === undefined || kind === "instruction" || preferBundledDocs) {
3302
3365
  const skillMdPath = join8(skillPath, "SKILL.md");
3303
3366
  if (existsSync8(skillMdPath))
3304
- return readFileSync6(skillMdPath, "utf-8");
3367
+ return readFileSync7(skillMdPath, "utf-8");
3305
3368
  }
3306
3369
  return pointerSkillMd(name, description);
3307
3370
  }
@@ -3331,7 +3394,7 @@ function normalizeSkillName(name) {
3331
3394
  }
3332
3395
 
3333
3396
  // src/lib/project-state.ts
3334
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
3397
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3335
3398
  import { join as join9 } from "path";
3336
3399
  var VALID_PIN_SOURCES = [
3337
3400
  "official",
@@ -3357,7 +3420,7 @@ function loadProjectConfig(targetDir = process.cwd()) {
3357
3420
  if (!existsSync9(path))
3358
3421
  return null;
3359
3422
  try {
3360
- return normalizeProjectConfig(JSON.parse(readFileSync7(path, "utf-8")));
3423
+ return normalizeProjectConfig(JSON.parse(readFileSync8(path, "utf-8")));
3361
3424
  } catch {
3362
3425
  return null;
3363
3426
  }
@@ -3378,9 +3441,9 @@ function ensureProjectConfig(targetDir = process.cwd()) {
3378
3441
  }
3379
3442
  function saveProjectConfig(config, targetDir = process.cwd()) {
3380
3443
  const dir = getProjectStateDir(targetDir);
3381
- mkdirSync6(dir, { recursive: true });
3444
+ mkdirSync5(dir, { recursive: true });
3382
3445
  const normalized = normalizeProjectConfig({ ...config, updatedAt: new Date().toISOString() });
3383
- writeFileSync6(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
3446
+ writeFileSync5(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
3384
3447
  `);
3385
3448
  }
3386
3449
  function pinProjectSkill(name, details = {}, targetDir = process.cwd()) {
@@ -3559,7 +3622,7 @@ function createLocalSkillManifest(name, generateSkillMd) {
3559
3622
  let skillMd = "";
3560
3623
  const skillMdPath = join10(sourcePath, "SKILL.md");
3561
3624
  if (existsSync10(skillMdPath)) {
3562
- skillMd = readFileSync8(skillMdPath, "utf-8");
3625
+ skillMd = readFileSync9(skillMdPath, "utf-8");
3563
3626
  } else if (generateSkillMd) {
3564
3627
  skillMd = generateSkillMd(name) ?? "";
3565
3628
  } else {
@@ -3670,14 +3733,14 @@ function removeSkillForAgent(name, options) {
3670
3733
  const dir = getAgentSkillPath(canonicalName, options.agent, scope, options.projectDir);
3671
3734
  if (!existsSync10(join10(dir, SYNC_MARKER_FILE)))
3672
3735
  return false;
3673
- rmSync4(dir, { recursive: true, force: true });
3736
+ rmSync3(dir, { recursive: true, force: true });
3674
3737
  return true;
3675
3738
  }
3676
3739
  function resolveAgentSkillMd(name, generateSkillMd) {
3677
3740
  const sourcePath = getSkillPath(name);
3678
3741
  const skillMdPath = join10(sourcePath, "SKILL.md");
3679
3742
  if (existsSync10(skillMdPath))
3680
- return readFileSync8(skillMdPath, "utf-8");
3743
+ return readFileSync9(skillMdPath, "utf-8");
3681
3744
  if (generateSkillMd)
3682
3745
  return generateSkillMd(name);
3683
3746
  return generateMinimalSkillMd(name);
@@ -3733,21 +3796,21 @@ function readBundledSkillVersion(name) {
3733
3796
  if (!existsSync10(pkgPath))
3734
3797
  return "unknown";
3735
3798
  try {
3736
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
3799
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
3737
3800
  return pkg.version || "unknown";
3738
3801
  } catch {
3739
3802
  return "unknown";
3740
3803
  }
3741
3804
  }
3742
3805
  function readFileIfExists(path) {
3743
- return existsSync10(path) ? readFileSync8(path, "utf-8") : null;
3806
+ return existsSync10(path) ? readFileSync9(path, "utf-8") : null;
3744
3807
  }
3745
3808
  function loadProjectConfigCompat(targetDir) {
3746
3809
  return loadProjectConfig(targetDir);
3747
3810
  }
3748
3811
  // src/lib/run-state.ts
3749
3812
  import { createHash as createHash2, randomBytes } from "crypto";
3750
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync9, readdirSync as readdirSync8, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
3813
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync10, readdirSync as readdirSync8, statSync as statSync8, writeFileSync as writeFileSync6 } from "fs";
3751
3814
  import { extname, join as join11, relative as relative2 } from "path";
3752
3815
  function createSkillRun(params, targetDir = process.cwd()) {
3753
3816
  const now = new Date;
@@ -3758,9 +3821,9 @@ function createSkillRun(params, targetDir = process.cwd()) {
3758
3821
  const runDir = join11(root, "runs", day, id);
3759
3822
  const logsDir = join11(runDir, "logs");
3760
3823
  const exportDir = join11(root, "exports", skillName, id);
3761
- mkdirSync7(logsDir, { recursive: true });
3762
- mkdirSync7(exportDir, { recursive: true });
3763
- mkdirSync7(join11(root, "tmp"), { recursive: true });
3824
+ mkdirSync6(logsDir, { recursive: true });
3825
+ mkdirSync6(exportDir, { recursive: true });
3826
+ mkdirSync6(join11(root, "tmp"), { recursive: true });
3764
3827
  const record = {
3765
3828
  id,
3766
3829
  skill: skillName,
@@ -3811,15 +3874,15 @@ function updateSkillRun(context, patch) {
3811
3874
  return context.record;
3812
3875
  }
3813
3876
  function writeRunLogs(context, stdout = "", stderr = "") {
3814
- writeFileSync7(join11(context.logsDir, "stdout.log"), stdout);
3815
- writeFileSync7(join11(context.logsDir, "stderr.log"), stderr);
3877
+ writeFileSync6(join11(context.logsDir, "stdout.log"), stdout);
3878
+ writeFileSync6(join11(context.logsDir, "stderr.log"), stderr);
3816
3879
  }
3817
3880
  function appendRunEvent(context, event, data = {}) {
3818
3881
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
3819
3882
  `;
3820
3883
  const path = join11(context.runDir, "events.ndjson");
3821
- const previous = existsSync11(path) ? readFileSync9(path, "utf-8") : "";
3822
- writeFileSync7(path, previous + line);
3884
+ const previous = existsSync11(path) ? readFileSync10(path, "utf-8") : "";
3885
+ writeFileSync6(path, previous + line);
3823
3886
  }
3824
3887
  function listSkillRuns(targetDir = process.cwd(), limit = 50) {
3825
3888
  const runsRoot = join11(getProjectStateDir(targetDir), "runs");
@@ -3855,11 +3918,11 @@ function getRunExportDir(runId, skill, targetDir = process.cwd()) {
3855
3918
  return join11(getProjectStateDir(targetDir), "exports", normalizeSkillName(skill), runId);
3856
3919
  }
3857
3920
  function writeRunRecord(context) {
3858
- writeFileSync7(join11(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
3921
+ writeFileSync6(join11(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
3859
3922
  `);
3860
3923
  }
3861
3924
  function writeArtifactsManifest(context, artifacts) {
3862
- writeFileSync7(join11(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
3925
+ writeFileSync6(join11(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
3863
3926
  `);
3864
3927
  }
3865
3928
  function collectRunArtifacts(context) {
@@ -3868,7 +3931,7 @@ function collectRunArtifacts(context) {
3868
3931
  const artifacts = [];
3869
3932
  for (const path of walkFiles(context.exportDir)) {
3870
3933
  const stat = statSync8(path);
3871
- const bytes = readFileSync9(path);
3934
+ const bytes = readFileSync10(path);
3872
3935
  artifacts.push({
3873
3936
  path: toProjectRelative(context.targetDir, path),
3874
3937
  mime: mimeForPath(path),
@@ -3883,7 +3946,7 @@ function readRunRecord(runDir) {
3883
3946
  if (!existsSync11(path))
3884
3947
  return null;
3885
3948
  try {
3886
- return JSON.parse(readFileSync9(path, "utf-8"));
3949
+ return JSON.parse(readFileSync10(path, "utf-8"));
3887
3950
  } catch {
3888
3951
  return null;
3889
3952
  }
@@ -3937,7 +4000,7 @@ function mimeForPath(path) {
3937
4000
  }
3938
4001
  }
3939
4002
  // src/lib/skillinfo.ts
3940
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4003
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3941
4004
  import { join as join12 } from "path";
3942
4005
  function isInstructionSkillDir(skillPath, meta) {
3943
4006
  if (meta?.kind === "instruction")
@@ -3946,7 +4009,7 @@ function isInstructionSkillDir(skillPath, meta) {
3946
4009
  if (!existsSync12(skillMdPath))
3947
4010
  return false;
3948
4011
  try {
3949
- return parseSkillFrontmatter(readFileSync10(skillMdPath, "utf-8"))?.kind === "instruction";
4012
+ return parseSkillFrontmatter(readFileSync11(skillMdPath, "utf-8"))?.kind === "instruction";
3950
4013
  } catch {
3951
4014
  return false;
3952
4015
  }
@@ -4031,7 +4094,7 @@ function getSkillRequirements(name) {
4031
4094
  const pkgPath = join12(skillPath, "package.json");
4032
4095
  if (existsSync12(pkgPath)) {
4033
4096
  try {
4034
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
4097
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
4035
4098
  dependencies = pkg.dependencies || {};
4036
4099
  } catch {}
4037
4100
  }
@@ -4064,7 +4127,7 @@ async function runSkill(name, args, options = {}) {
4064
4127
  }
4065
4128
  let entryPoint;
4066
4129
  try {
4067
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
4130
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
4068
4131
  if (pkg.bin) {
4069
4132
  const binValues = Object.values(pkg.bin);
4070
4133
  entryPoint = binValues[0];
@@ -4170,7 +4233,7 @@ function generateSkillMd(name) {
4170
4233
  const pkgPath = join12(skillPath, "package.json");
4171
4234
  if (existsSync12(pkgPath)) {
4172
4235
  try {
4173
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
4236
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
4174
4237
  if (pkg.bin) {
4175
4238
  const binKeys = Object.keys(pkg.bin);
4176
4239
  if (binKeys.length > 0)
@@ -4246,7 +4309,7 @@ function extractEnvVars(text) {
4246
4309
  function readIfExists(path) {
4247
4310
  try {
4248
4311
  if (existsSync12(path)) {
4249
- return readFileSync10(path, "utf-8");
4312
+ return readFileSync11(path, "utf-8");
4250
4313
  }
4251
4314
  } catch {}
4252
4315
  return null;
@@ -8249,18 +8312,22 @@ function requireApiUrl(action = "This command", config, env) {
8249
8312
  }
8250
8313
 
8251
8314
  // src/lib/auth-store.ts
8252
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync } from "fs";
8253
- import { join as join13 } from "path";
8315
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync } from "fs";
8316
+ import { dirname as dirname6, join as join13 } from "path";
8254
8317
  import { homedir as homedir4 } from "os";
8255
- var AUTH_DIR = join13(homedir4(), ".hasna", "skills");
8256
- var AUTH_FILE = join13(AUTH_DIR, "auth.json");
8257
- var LEGACY_AUTH_FILE = join13(homedir4(), ".skills", "auth.json");
8318
+ function getAuthFilePath() {
8319
+ return join13(getDataDir(), "auth.json");
8320
+ }
8321
+ function legacyAuthFilePath() {
8322
+ return join13(process.env["HOME"] || process.env["USERPROFILE"] || homedir4(), ".skills", "auth.json");
8323
+ }
8258
8324
  var cachedConfig;
8259
8325
  function getAuthConfig() {
8260
8326
  if (cachedConfig !== undefined)
8261
8327
  return cachedConfig;
8262
8328
  try {
8263
- const raw = readFileSync11(existsSync13(AUTH_FILE) ? AUTH_FILE : LEGACY_AUTH_FILE, "utf-8");
8329
+ const file = existsSync13(getAuthFilePath()) ? getAuthFilePath() : legacyAuthFilePath();
8330
+ const raw = readFileSync12(file, "utf-8");
8264
8331
  const config = JSON.parse(raw);
8265
8332
  if (!config.apiKey) {
8266
8333
  cachedConfig = null;
@@ -9084,6 +9151,30 @@ function pickNumber(record, key) {
9084
9151
  }
9085
9152
 
9086
9153
  // src/lib/remote-client.ts
9154
+ class RemoteRouteUnsupportedError extends Error {
9155
+ path;
9156
+ status;
9157
+ instance;
9158
+ constructor(path, status, instance) {
9159
+ 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.`);
9160
+ this.path = path;
9161
+ this.status = status;
9162
+ this.instance = instance;
9163
+ this.name = "RemoteRouteUnsupportedError";
9164
+ }
9165
+ }
9166
+
9167
+ class RemoteRequestError extends Error {
9168
+ path;
9169
+ status;
9170
+ constructor(path, status, statusText) {
9171
+ super(`Remote request to ${path} failed: HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
9172
+ this.path = path;
9173
+ this.status = status;
9174
+ this.name = "RemoteRequestError";
9175
+ }
9176
+ }
9177
+
9087
9178
  class RemoteSkillsClient {
9088
9179
  apiUrl;
9089
9180
  apiKey;
@@ -9101,6 +9192,20 @@ class RemoteSkillsClient {
9101
9192
  }
9102
9193
  });
9103
9194
  }
9195
+ async requestNewRoute(path, options, opts = {}) {
9196
+ const response = await this.request(path, options);
9197
+ const routePath = path.split("?")[0];
9198
+ if (response.status === 404 || response.status === 405) {
9199
+ if (response.status === 404 && opts.domainNotFoundCodes?.length && await responseBodyCarriesCode(response, opts.domainNotFoundCodes)) {
9200
+ return response;
9201
+ }
9202
+ throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
9203
+ }
9204
+ if (!response.ok) {
9205
+ throw new RemoteRequestError(routePath, response.status, response.statusText);
9206
+ }
9207
+ return response;
9208
+ }
9104
9209
  async listSkills() {
9105
9210
  const res = await this.request("/api/v1/skills");
9106
9211
  return res.json();
@@ -9117,6 +9222,14 @@ class RemoteSkillsClient {
9117
9222
  return null;
9118
9223
  return res.json();
9119
9224
  }
9225
+ async getSkillStatus(slug) {
9226
+ const res = await this.request(`/api/v1/skills/${encodeURIComponent(slug)}`, { method: "GET" });
9227
+ let body = null;
9228
+ try {
9229
+ body = await res.json();
9230
+ } catch {}
9231
+ return { status: res.status, body };
9232
+ }
9120
9233
  async submitRun(slug, input, args) {
9121
9234
  const res = await this.request(`/api/v1/runs/${slug}`, {
9122
9235
  method: "POST",
@@ -9150,15 +9263,18 @@ class RemoteSkillsClient {
9150
9263
  method: "GET"
9151
9264
  });
9152
9265
  }
9153
- async publishSkill(manifest, bundle) {
9266
+ async publishSkill(manifest, bundle, ifMatch) {
9154
9267
  const form = new FormData;
9155
9268
  form.set("manifest", JSON.stringify(manifest));
9156
9269
  if (bundle) {
9157
9270
  form.set("bundle", new Blob([bundle], { type: "application/gzip" }), `${String(manifest.slug ?? "skill")}.tar.gz`);
9158
9271
  }
9272
+ const headers = { Authorization: `Bearer ${this.apiKey}` };
9273
+ if (ifMatch)
9274
+ headers["If-Match"] = ifMatch;
9159
9275
  return fetch(`${this.apiUrl}/api/v1/skills`, {
9160
9276
  method: "POST",
9161
- headers: { Authorization: `Bearer ${this.apiKey}` },
9277
+ headers,
9162
9278
  body: form
9163
9279
  });
9164
9280
  }
@@ -9174,6 +9290,135 @@ class RemoteSkillsClient {
9174
9290
  return null;
9175
9291
  return response;
9176
9292
  }
9293
+ async listPins() {
9294
+ const response = await this.requestNewRoute("/api/v1/pins");
9295
+ return normalizePinList(await response.json());
9296
+ }
9297
+ async pin(slug, metadata) {
9298
+ const path = `/api/v1/pins/${encodeURIComponent(slug)}`;
9299
+ const response = await this.requestNewRoute(path, {
9300
+ method: "PUT",
9301
+ body: JSON.stringify({ ...metadata ? { metadata } : {} })
9302
+ });
9303
+ return normalizePin(await response.json());
9304
+ }
9305
+ async unpin(slug) {
9306
+ const path = `/api/v1/pins/${encodeURIComponent(slug)}`;
9307
+ const response = await this.requestNewRoute(path, { method: "DELETE" }, { domainNotFoundCodes: ["PIN_NOT_FOUND"] });
9308
+ return response.status !== 404;
9309
+ }
9310
+ async listTags() {
9311
+ const response = await this.requestNewRoute("/api/v1/tags");
9312
+ const payload = await response.json();
9313
+ if (!Array.isArray(payload)) {
9314
+ throw new Error("Remote tags payload did not match the expected contract (expected an array of tag names)");
9315
+ }
9316
+ for (const tag of payload) {
9317
+ if (typeof tag !== "string" || tag.trim().length === 0) {
9318
+ throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name)");
9319
+ }
9320
+ }
9321
+ return payload;
9322
+ }
9323
+ async skillsByTag(tag) {
9324
+ const path = `/api/v1/tags/${encodeURIComponent(tag)}/skills`;
9325
+ const response = await this.requestNewRoute(path);
9326
+ return normalizeSkillSummaryList(await response.json());
9327
+ }
9328
+ async listUpdatedSince(since, options = {}) {
9329
+ const params = new URLSearchParams({ since });
9330
+ if (options.cursor)
9331
+ params.set("cursor", options.cursor);
9332
+ if (options.limit !== undefined)
9333
+ params.set("limit", String(options.limit));
9334
+ const response = await this.requestNewRoute(`/api/v1/skills/updated?${params.toString()}`);
9335
+ return normalizeUpdatedSincePage(await response.json());
9336
+ }
9337
+ }
9338
+ function requireOptionalString(record, field) {
9339
+ if (record[field] === undefined)
9340
+ return;
9341
+ if (typeof record[field] !== "string") {
9342
+ throw new Error(`Remote payload did not match the expected contract (${field} must be a string when present)`);
9343
+ }
9344
+ return record[field];
9345
+ }
9346
+ function normalizePin(entry) {
9347
+ if (!entry || typeof entry !== "object") {
9348
+ throw new Error("Remote pin payload did not match the expected contract (expected an object)");
9349
+ }
9350
+ const record = entry;
9351
+ const slug = typeof record.slug === "string" && record.slug.trim() ? record.slug.trim() : undefined;
9352
+ if (!slug) {
9353
+ throw new Error("Remote pin payload did not match the expected contract (missing slug)");
9354
+ }
9355
+ let metadata;
9356
+ if (record.metadata !== undefined) {
9357
+ if (!record.metadata || typeof record.metadata !== "object" || Array.isArray(record.metadata)) {
9358
+ throw new Error("Remote pin payload did not match the expected contract (metadata must be a JSON object when present)");
9359
+ }
9360
+ metadata = record.metadata;
9361
+ }
9362
+ const pinnedAt = requireOptionalString(record, "pinnedAt");
9363
+ return {
9364
+ slug,
9365
+ ...pinnedAt !== undefined ? { pinnedAt } : {},
9366
+ ...metadata ? { metadata } : {}
9367
+ };
9368
+ }
9369
+ function normalizePinList(payload) {
9370
+ if (!Array.isArray(payload)) {
9371
+ throw new Error("Remote pins payload did not match the expected contract (expected an array of pins)");
9372
+ }
9373
+ return payload.map(normalizePin);
9374
+ }
9375
+ function normalizeSkillSummary(entry) {
9376
+ if (!entry || typeof entry !== "object") {
9377
+ throw new Error("Remote skill payload did not match the expected contract (expected an object)");
9378
+ }
9379
+ const record = entry;
9380
+ const slug = typeof record.slug === "string" && record.slug.trim() ? record.slug.trim() : undefined;
9381
+ if (!slug) {
9382
+ throw new Error("Remote skill payload did not match the expected contract (missing slug)");
9383
+ }
9384
+ return {
9385
+ slug,
9386
+ ...requireOptionalString(record, "name") !== undefined ? { name: requireOptionalString(record, "name") } : {},
9387
+ ...requireOptionalString(record, "version") !== undefined ? { version: requireOptionalString(record, "version") } : {},
9388
+ ...requireOptionalString(record, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record, "updatedAt") } : {}
9389
+ };
9390
+ }
9391
+ function normalizeSkillSummaryList(payload) {
9392
+ if (!Array.isArray(payload)) {
9393
+ throw new Error("Remote skills payload did not match the expected contract (expected an array of skills)");
9394
+ }
9395
+ return payload.map(normalizeSkillSummary);
9396
+ }
9397
+ async function responseBodyCarriesCode(response, codes) {
9398
+ try {
9399
+ const payload = await response.clone().json();
9400
+ if (!payload || typeof payload !== "object")
9401
+ return false;
9402
+ const code = payload.code;
9403
+ return typeof code === "string" && codes.includes(code);
9404
+ } catch {
9405
+ return false;
9406
+ }
9407
+ }
9408
+ function normalizeUpdatedSincePage(payload) {
9409
+ if (!payload || typeof payload !== "object") {
9410
+ throw new Error("Updated-since payload did not match the expected contract (expected an object)");
9411
+ }
9412
+ const record = payload;
9413
+ if (!Array.isArray(record.skills)) {
9414
+ throw new Error("Updated-since payload did not match the expected contract (missing skills array)");
9415
+ }
9416
+ const skills = record.skills.map(normalizeSkillSummary);
9417
+ const nextCursor = record.nextCursor === undefined || record.nextCursor === null ? null : record.nextCursor;
9418
+ if (nextCursor !== null && typeof nextCursor !== "string") {
9419
+ throw new Error("Updated-since payload did not match the expected contract (nextCursor must be a string or absent)");
9420
+ }
9421
+ return { skills, nextCursor };
9177
9422
  }
9178
9423
  function createRemoteSkillsClient() {
9179
9424
  const apiKey = getApiKey();
@@ -9182,7 +9427,7 @@ function createRemoteSkillsClient() {
9182
9427
  return new RemoteSkillsClient(apiKey);
9183
9428
  }
9184
9429
  // src/lib/scheduler.ts
9185
- import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9 } from "fs";
9430
+ import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
9186
9431
  import { join as join14 } from "path";
9187
9432
  function getSchedulesPath(targetDir = process.cwd()) {
9188
9433
  return join14(targetDir, ".skills", "schedules.json");
@@ -9191,7 +9436,7 @@ function loadSchedules(targetDir = process.cwd()) {
9191
9436
  const path = getSchedulesPath(targetDir);
9192
9437
  if (existsSync14(path)) {
9193
9438
  try {
9194
- return JSON.parse(readFileSync12(path, "utf-8"));
9439
+ return JSON.parse(readFileSync13(path, "utf-8"));
9195
9440
  } catch {}
9196
9441
  }
9197
9442
  return { version: 1, schedules: [] };
@@ -9200,8 +9445,8 @@ function saveSchedules(data, targetDir = process.cwd()) {
9200
9445
  const path = getSchedulesPath(targetDir);
9201
9446
  const dir = join14(targetDir, ".skills");
9202
9447
  if (!existsSync14(dir))
9203
- mkdirSync9(dir, { recursive: true });
9204
- writeFileSync9(path, JSON.stringify(data, null, 2));
9448
+ mkdirSync8(dir, { recursive: true });
9449
+ writeFileSync8(path, JSON.stringify(data, null, 2));
9205
9450
  }
9206
9451
  function validateCronField(expr, min, max, label) {
9207
9452
  for (const part of expr.split(",")) {
@@ -9386,11 +9631,34 @@ function recordScheduleRun(id, status, targetDir) {
9386
9631
  saveSchedules(data, targetDir);
9387
9632
  }
9388
9633
  // src/lib/pull.ts
9389
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync10 } from "fs";
9390
- import { dirname as dirname6, join as join15 } from "path";
9634
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync14, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
9635
+ import { dirname as dirname7, join as join15 } from "path";
9391
9636
 
9392
- // src/lib/skill-bundle.ts
9637
+ // src/lib/revision.ts
9393
9638
  import { createHash as createHash3 } from "crypto";
9639
+ var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
9640
+ function revisionIdOf(content) {
9641
+ const canonical = JSON.stringify({
9642
+ slug: content.slug,
9643
+ displayName: content.displayName,
9644
+ description: content.description,
9645
+ category: content.category,
9646
+ tags: content.tags,
9647
+ source: content.source,
9648
+ kind: content.kind,
9649
+ version: content.version ?? null,
9650
+ skillMd: content.skillMd ?? null,
9651
+ bundleSha256: content.bundleSha256 ?? null,
9652
+ bundleByteSize: content.bundleByteSize ?? null
9653
+ });
9654
+ return createHash3("sha256").update(canonical).digest("hex");
9655
+ }
9656
+ function revisionIdOfRecord(record) {
9657
+ return revisionIdOf(record);
9658
+ }
9659
+
9660
+ // src/lib/skill-bundle.ts
9661
+ import { createHash as createHash4 } from "crypto";
9394
9662
  var BLOCK = 512;
9395
9663
  var ANY_SEGMENT_EXCLUDES = new Set([
9396
9664
  ".git",
@@ -9403,6 +9671,7 @@ var ANY_SEGMENT_EXCLUDES = new Set([
9403
9671
  ".docker"
9404
9672
  ]);
9405
9673
  var ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
9674
+ var TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
9406
9675
  var CREDENTIAL_FILENAMES = new Set([
9407
9676
  ".npmrc",
9408
9677
  ".pypirc",
@@ -9519,7 +9788,7 @@ function ownBytes(view) {
9519
9788
  return out;
9520
9789
  }
9521
9790
  function sha256Hex(bytes) {
9522
- return createHash3("sha256").update(bytes).digest("hex");
9791
+ return createHash4("sha256").update(bytes).digest("hex");
9523
9792
  }
9524
9793
  function unpackSkillBundle(bundle) {
9525
9794
  return readTar(ownBytes(Bun.gunzipSync(ownBytes(bundle))));
@@ -9598,9 +9867,9 @@ function verifyBundleSignature(bytes, signature, key) {
9598
9867
  // src/lib/pull.ts
9599
9868
  var BUNDLE_DIGEST_HEADER = "X-Skill-Bundle-Sha256";
9600
9869
  var BUNDLE_SIGNATURE_HEADER = "X-Skill-Bundle-Signature";
9870
+ var BUNDLE_REVISION_ID_HEADER = "X-Skill-Revision-Id";
9871
+ var BUNDLE_REVISION_NUMBER_HEADER = "X-Skill-Revision-Number";
9601
9872
  var PULL_MARKER_FILE = ".hasna-skills.json";
9602
- var SKILLS_CACHE_DIRNAME2 = "skills";
9603
- var LAYOUT_MIGRATION_RECORD2 = ".layout-migration.json";
9604
9873
 
9605
9874
  class PullSkillError extends Error {
9606
9875
  detail;
@@ -9650,6 +9919,15 @@ async function pullOne(client, rawName, corpusOptions, verify) {
9650
9919
  return { name: slug, success: false, error: `Failed to fetch '${slug}': ${error.message}` };
9651
9920
  }
9652
9921
  if (bundleResponse && !bundleResponse.ok) {
9922
+ if (bundleResponse.status === 410) {
9923
+ return reconcileTombstone(slug, corpusOptions);
9924
+ }
9925
+ if (bundleResponse.status === 404) {
9926
+ const marker = readPullMarker(join15(getPortableSkillsRoot(corpusOptions), slug));
9927
+ if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
9928
+ return { name: slug, success: true, purged: true, removed: false };
9929
+ }
9930
+ }
9653
9931
  return {
9654
9932
  name: slug,
9655
9933
  success: false,
@@ -9675,17 +9953,78 @@ async function pullOne(client, rawName, corpusOptions, verify) {
9675
9953
  if (skillMd === null) {
9676
9954
  return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
9677
9955
  }
9678
- const written = writeCorpusSkill({ name: slug, skillMd, meta }, corpusOptions);
9679
- writePullMarker(written.path, { skill: slug, version: written.manifest.version });
9956
+ if (!meta?.revisionId) {
9957
+ const marker = readPullMarker(join15(getPortableSkillsRoot(corpusOptions), slug));
9958
+ if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
9959
+ return { name: slug, success: true, purged: true, removed: false };
9960
+ }
9961
+ }
9962
+ let provenRevisionId;
9963
+ try {
9964
+ provenRevisionId = meta?.revisionId ? provenRevision({ ...meta, skillMd }, slug, {}) : undefined;
9965
+ } catch (error) {
9966
+ if (error instanceof PullSkillError) {
9967
+ return { name: slug, success: false, error: error.message };
9968
+ }
9969
+ throw error;
9970
+ }
9971
+ const written = installCorpusSkillAtomically({ name: slug, skillMd, meta }, corpusOptions);
9972
+ writePullMarker(written.path, {
9973
+ skill: slug,
9974
+ version: written.manifest.version,
9975
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {}
9976
+ });
9680
9977
  return {
9681
9978
  name: slug,
9682
9979
  success: true,
9683
9980
  path: written.path,
9684
9981
  kind: written.manifest.kind,
9685
9982
  version: written.manifest.version,
9686
- created: written.created
9983
+ created: written.created,
9984
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {}
9687
9985
  };
9688
9986
  }
9987
+ function provenRevision(meta, slug, bundle) {
9988
+ const declared = meta.revisionId;
9989
+ if (!declared)
9990
+ return "";
9991
+ const source = meta.publishedSource;
9992
+ if (typeof source !== "string" || source.length === 0) {
9993
+ 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.`);
9994
+ }
9995
+ const recomputed = revisionIdOf({
9996
+ slug,
9997
+ displayName: meta.displayName ?? "",
9998
+ description: meta.description ?? "",
9999
+ category: meta.category ?? "",
10000
+ tags: meta.tags ?? [],
10001
+ source,
10002
+ kind: meta.kind ?? "instruction",
10003
+ ...meta.version ? { version: meta.version } : {},
10004
+ ...typeof meta.skillMd === "string" && meta.skillMd.length > 0 ? { skillMd: meta.skillMd } : {},
10005
+ ...bundle.sha256 ? { bundleSha256: bundle.sha256 } : {},
10006
+ ...bundle.byteSize !== undefined && bundle.byteSize !== null ? { bundleByteSize: bundle.byteSize } : {}
10007
+ });
10008
+ if (recomputed !== declared) {
10009
+ 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.`);
10010
+ }
10011
+ return declared;
10012
+ }
10013
+ function reconcileTombstone(slug, corpusOptions) {
10014
+ const target = join15(getPortableSkillsRoot(corpusOptions), slug);
10015
+ if (!existsSync15(join15(target, PULL_MARKER_FILE))) {
10016
+ return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
10017
+ }
10018
+ rmSync4(target, { recursive: true, force: true });
10019
+ return { name: slug, success: true, tombstoned: true, removed: true };
10020
+ }
10021
+ function readPullMarker(dir) {
10022
+ try {
10023
+ return JSON.parse(readFileSync14(join15(dir, PULL_MARKER_FILE), "utf-8"));
10024
+ } catch {
10025
+ return null;
10026
+ }
10027
+ }
9689
10028
  function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
9690
10029
  return response.arrayBuffer().then((buffer) => {
9691
10030
  const verified = verifyBundleResponseBytes(buffer, response, verify);
@@ -9697,11 +10036,17 @@ function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
9697
10036
  }
9698
10037
  const version = str(meta?.version) ?? versionFromEntries(entries) ?? "unknown";
9699
10038
  const sourceCommit = sourceCommitFromEntries(entries);
10039
+ const declaredRevision = verified.revisionId ?? meta?.revisionId;
10040
+ if (declaredRevision && meta?.revisionId && verified.revisionId && declaredRevision !== meta.revisionId) {
10041
+ 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.`);
10042
+ }
10043
+ const provenRevisionId = declaredRevision ? provenRevision(meta ?? {}, slug, { sha256: verified.contentHash, byteSize: verified.bytes.byteLength }) : undefined;
9700
10044
  const installed = installBundleAtomically(slug, entries, corpusOptions, {
9701
10045
  version,
9702
10046
  contentHash: verified.contentHash,
9703
10047
  ...sourceCommit ? { sourceCommit } : {},
9704
- ...verified.signature ? { signature: verified.signature } : {}
10048
+ ...verified.signature ? { signature: verified.signature } : {},
10049
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {}
9705
10050
  });
9706
10051
  return {
9707
10052
  name: slug,
@@ -9711,6 +10056,7 @@ function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
9711
10056
  version,
9712
10057
  contentHash: verified.contentHash,
9713
10058
  ...sourceCommit ? { sourceCommit } : {},
10059
+ ...provenRevisionId ? { revisionId: provenRevisionId } : {},
9714
10060
  created: installed.created
9715
10061
  };
9716
10062
  });
@@ -9761,11 +10107,20 @@ function sourceCommitFromEntries(entries) {
9761
10107
  function verifyBundleResponseBytes(buffer, response, verify = {}) {
9762
10108
  const serverHash = response.headers.get(BUNDLE_DIGEST_HEADER);
9763
10109
  const signature = response.headers.get(BUNDLE_SIGNATURE_HEADER);
10110
+ const revisionId = response.headers.get(BUNDLE_REVISION_ID_HEADER);
10111
+ const revisionNumberRaw = response.headers.get(BUNDLE_REVISION_NUMBER_HEADER);
9764
10112
  const bytes = new Uint8Array(buffer.slice(0));
9765
10113
  const contentHash = sha256Hex(bytes);
9766
10114
  if (serverHash && serverHash.toLowerCase() !== contentHash) {
9767
10115
  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."]);
9768
10116
  }
10117
+ if (revisionId && !REVISION_ID_PATTERN.test(revisionId)) {
10118
+ 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."]);
10119
+ }
10120
+ const revisionNumber = revisionNumberRaw === null ? undefined : Number(revisionNumberRaw);
10121
+ if (revisionNumberRaw !== null && (!Number.isInteger(revisionNumber) || revisionNumber < 0)) {
10122
+ 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."]);
10123
+ }
9769
10124
  if (signature) {
9770
10125
  const key = verify.signingKey;
9771
10126
  if (!key) {
@@ -9778,43 +10133,46 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
9778
10133
  bytes,
9779
10134
  contentHash,
9780
10135
  ...serverHash ? { serverHash } : {},
9781
- ...signature ? { signature } : {}
10136
+ ...signature ? { signature } : {},
10137
+ ...revisionId ? { revisionId } : {},
10138
+ ...revisionNumberRaw === null || revisionNumber === undefined ? {} : { revisionNumber }
9782
10139
  };
9783
10140
  }
9784
10141
  function installBundleAtomically(name, entries, options = {}, marker = {}) {
9785
10142
  const root = getPortableSkillsRoot(options);
9786
- mkdirSync10(root, { recursive: true });
10143
+ mkdirSync9(root, { recursive: true });
9787
10144
  const target = join15(root, name);
9788
10145
  const created = !existsSync15(target);
9789
- const staging = mkdtempSync2(join15(root, `.pull-${name}-`));
10146
+ const staging = mkdtempSync3(join15(root, `.pull-${name}-`));
9790
10147
  let moved = false;
9791
10148
  let backup = null;
9792
10149
  try {
9793
10150
  for (const entry of entries) {
9794
10151
  const destination = join15(staging, entry.path);
9795
- mkdirSync10(dirname6(destination), { recursive: true });
9796
- writeFileSync10(destination, entry.bytes, { mode: entry.mode });
10152
+ mkdirSync9(dirname7(destination), { recursive: true });
10153
+ writeFileSync9(destination, entry.bytes, { mode: entry.mode });
9797
10154
  }
9798
10155
  writePullMarker(staging, {
9799
10156
  skill: name,
9800
10157
  ...marker.version ? { version: marker.version } : {},
9801
10158
  ...marker.contentHash ? { contentHash: marker.contentHash } : {},
9802
10159
  ...marker.sourceCommit ? { sourceCommit: marker.sourceCommit } : {},
9803
- ...marker.signature ? { signature: marker.signature } : {}
10160
+ ...marker.signature ? { signature: marker.signature } : {},
10161
+ ...marker.revisionId ? { revisionId: marker.revisionId } : {}
9804
10162
  });
9805
10163
  if (existsSync15(target)) {
9806
- backup = mkdtempSync2(join15(root, `.pull-backup-${name}-`));
9807
- renameSync4(target, join15(backup, name));
10164
+ backup = mkdtempSync3(join15(root, `.pull-backup-${name}-`));
10165
+ renameSync3(target, join15(backup, name));
9808
10166
  moved = true;
9809
10167
  }
9810
- renameSync4(staging, target);
10168
+ renameSync3(staging, target);
9811
10169
  if (moved && backup)
9812
- rmSync5(backup, { recursive: true, force: true });
10170
+ rmSync4(backup, { recursive: true, force: true });
9813
10171
  } catch (error) {
9814
- rmSync5(staging, { recursive: true, force: true });
10172
+ rmSync4(staging, { recursive: true, force: true });
9815
10173
  if (moved && backup && existsSync15(join15(backup, name))) {
9816
10174
  try {
9817
- renameSync4(join15(backup, name), target);
10175
+ renameSync3(join15(backup, name), target);
9818
10176
  } catch {}
9819
10177
  }
9820
10178
  throw error;
@@ -9825,14 +10183,15 @@ function writePullMarker(dir, record) {
9825
10183
  const marker = {
9826
10184
  managedBy: "@hasna/skills",
9827
10185
  skill: record.skill,
9828
- source: "pull",
10186
+ source: record.source ?? "pull",
9829
10187
  ...record.version ? { version: record.version } : {},
9830
10188
  ...record.contentHash ? { contentHash: record.contentHash } : {},
9831
10189
  ...record.sourceCommit ? { sourceCommit: record.sourceCommit } : {},
9832
10190
  ...record.signature ? { signature: record.signature } : {},
10191
+ ...record.revisionId ? { revisionId: record.revisionId } : {},
9833
10192
  syncedAt: new Date().toISOString()
9834
10193
  };
9835
- writeFileSync10(join15(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
10194
+ writeFileSync9(join15(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
9836
10195
  `);
9837
10196
  }
9838
10197
  async function safeMeta(client, slug) {
@@ -9853,20 +10212,14 @@ async function safeMeta(client, slug) {
9853
10212
  ...str(record.category) ? { category: str(record.category) } : {},
9854
10213
  ...tags && tags.length ? { tags } : {},
9855
10214
  ...str(record.version) ? { version: str(record.version) } : {},
9856
- ...kind ? { kind } : {}
10215
+ ...kind ? { kind } : {},
10216
+ ...REVISION_ID_PATTERN.test(str(record.revisionId) ?? "") ? { revisionId: str(record.revisionId) } : {},
10217
+ ...typeof record.skillMd === "string" && record.skillMd.length > 0 ? { skillMd: record.skillMd } : {},
10218
+ ...str(record.publishedSource) ? { publishedSource: str(record.publishedSource) } : {}
9857
10219
  };
9858
10220
  }
9859
10221
  function pickCorpusOptions(options) {
9860
- return { rootDir: resolvePullCorpusRoot(options) };
9861
- }
9862
- function resolvePullCorpusRoot(options) {
9863
- if (options.rootDir)
9864
- return options.rootDir;
9865
- const appDir = options.homeDir ? join15(options.homeDir, ".hasna", "skills") : getDataDir();
9866
- const cache = join15(appDir, SKILLS_CACHE_DIRNAME2);
9867
- if (existsSync15(join15(cache, LAYOUT_MIGRATION_RECORD2)) && existsSync15(cache))
9868
- return cache;
9869
- return getPortableSkillsRoot(options);
10222
+ return { rootDir: resolveCorpusRoot(options) };
9870
10223
  }
9871
10224
  function extractSlug(entry) {
9872
10225
  if (!entry || typeof entry !== "object")
@@ -9984,12 +10337,12 @@ function findSkillsParityForMcpTool(tool) {
9984
10337
  return SKILLS_CLI_MCP_PARITY.find((entry) => entry.mcpTools.includes(tool));
9985
10338
  }
9986
10339
  // src/lib/registry-sync.ts
9987
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
9988
- import { dirname as dirname7, relative as relative3 } from "path";
10340
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
10341
+ import { dirname as dirname8, relative as relative3 } from "path";
9989
10342
  // package.json
9990
10343
  var package_default = {
9991
10344
  name: "@hasna/skills",
9992
- version: "0.1.63",
10345
+ version: "0.1.66",
9993
10346
  description: "Skills library for AI coding agents",
9994
10347
  type: "module",
9995
10348
  bin: {
@@ -10063,6 +10416,7 @@ var package_default = {
10063
10416
  license: "Apache-2.0",
10064
10417
  devDependencies: {
10065
10418
  "@types/bun": "latest",
10419
+ "@types/node": "25.2.3",
10066
10420
  "@types/react": "^18.2.0",
10067
10421
  "bun-types": "1.3.14",
10068
10422
  "react-devtools-core": "^7.0.1",
@@ -10071,7 +10425,7 @@ var package_default = {
10071
10425
  dependencies: {
10072
10426
  "@aws-sdk/client-ecs": "^3.1079.0",
10073
10427
  "@aws-sdk/client-s3": "^3.1079.0",
10074
- "@hasna/events": "0.1.7",
10428
+ "@hasna/events": "0.1.16",
10075
10429
  "@modelcontextprotocol/sdk": "^1.26.0",
10076
10430
  chalk: "^5.3.0",
10077
10431
  commander: "^12.1.0",
@@ -10156,8 +10510,8 @@ function createRegistrySyncArtifact(options = {}) {
10156
10510
  };
10157
10511
  }
10158
10512
  function writeRegistrySyncArtifact(path, artifact) {
10159
- mkdirSync11(dirname7(path), { recursive: true });
10160
- writeFileSync11(path, `${JSON.stringify(artifact, null, 2)}
10513
+ mkdirSync10(dirname8(path), { recursive: true });
10514
+ writeFileSync10(path, `${JSON.stringify(artifact, null, 2)}
10161
10515
  `);
10162
10516
  }
10163
10517
  function buildDocs(name) {
@@ -10996,17 +11350,17 @@ function clone2(value) {
10996
11350
  return JSON.parse(JSON.stringify(value));
10997
11351
  }
10998
11352
  // src/lib/feedback.ts
10999
- import { existsSync as existsSync16, mkdirSync as mkdirSync12 } from "fs";
11000
- import { dirname as dirname8, join as join16 } from "path";
11353
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11 } from "fs";
11354
+ import { dirname as dirname9, join as join16 } from "path";
11001
11355
  import { Database } from "bun:sqlite";
11002
11356
  function getFeedbackDbPath() {
11003
11357
  return join16(getDataDir(), "skills.db");
11004
11358
  }
11005
11359
  function getFeedbackDb() {
11006
11360
  const dbPath = getFeedbackDbPath();
11007
- const dir = dirname8(dbPath);
11361
+ const dir = dirname9(dbPath);
11008
11362
  if (!existsSync16(dir))
11009
- mkdirSync12(dir, { recursive: true });
11363
+ mkdirSync11(dir, { recursive: true });
11010
11364
  const db = new Database(dbPath);
11011
11365
  db.exec("PRAGMA journal_mode = WAL");
11012
11366
  db.exec([
@@ -11040,16 +11394,16 @@ function saveFeedback(input) {
11040
11394
  return { saved: true, category, path: getFeedbackDbPath() };
11041
11395
  }
11042
11396
  // src/lib/native-storage.ts
11043
- import { createHash as createHash4, createHmac as createHmac2 } from "crypto";
11397
+ import { createHash as createHash5, createHmac as createHmac2 } from "crypto";
11044
11398
  import {
11045
11399
  existsSync as existsSync17,
11046
- mkdirSync as mkdirSync13,
11047
- readFileSync as readFileSync13,
11400
+ mkdirSync as mkdirSync12,
11401
+ readFileSync as readFileSync15,
11048
11402
  readdirSync as readdirSync9,
11049
11403
  statSync as statSync9,
11050
- writeFileSync as writeFileSync12
11404
+ writeFileSync as writeFileSync11
11051
11405
  } from "fs";
11052
- import { dirname as dirname9, join as join17, normalize as normalize3, relative as relative4, sep as sep2 } from "path";
11406
+ import { dirname as dirname10, join as join17, normalize as normalize3, relative as relative4, sep as sep2 } from "path";
11053
11407
  var SKILLS_STORAGE_TABLES = [
11054
11408
  "skills_sync_records",
11055
11409
  "skills_sync_cursors"
@@ -11087,6 +11441,55 @@ var SKILLS_NATIVE_STORAGE_FALLBACK_ENV = {
11087
11441
  };
11088
11442
  var SKILLS_STORAGE_ENV = SKILLS_NATIVE_STORAGE_ENV;
11089
11443
  var SKILLS_STORAGE_FALLBACK_ENV = SKILLS_NATIVE_STORAGE_FALLBACK_ENV;
11444
+ var storageCapabilities = {
11445
+ version: 1,
11446
+ values: [
11447
+ "SKILLS_NATIVE_STORAGE_ENV",
11448
+ "SKILLS_NATIVE_STORAGE_FALLBACK_ENV",
11449
+ "SKILLS_STORAGE_ENV",
11450
+ "SKILLS_STORAGE_FALLBACK_ENV",
11451
+ "SKILLS_STORAGE_TABLES",
11452
+ "STORAGE_TABLES",
11453
+ "SkillsPostgresSyncStore",
11454
+ "SkillsS3ObjectStore",
11455
+ "buildSkillsS3ObjectUrl",
11456
+ "createSkillsPostgresSyncStore",
11457
+ "createSkillsS3ObjectStore",
11458
+ "createSkillsSnapshotSyncRecord",
11459
+ "exportSkillsLocalSnapshot",
11460
+ "getSkillsNativeStorageStatus",
11461
+ "getSkillsStorageDatabaseEnv",
11462
+ "getSkillsStorageDatabaseUrl",
11463
+ "getSkillsStorageStatus",
11464
+ "getStorageDatabaseEnv",
11465
+ "getStorageDatabaseUrl",
11466
+ "getStorageStatus",
11467
+ "importSkillsLocalSnapshot",
11468
+ "planSkillsS3SnapshotUpload",
11469
+ "resolveSkillsNativeStorageConfig",
11470
+ "resolveStorageConfig",
11471
+ "signSkillsAwsV4Request",
11472
+ "skillsPostgresSyncSchemaSql",
11473
+ "storageCapabilities",
11474
+ "uploadSkillsSnapshotFilesToS3"
11475
+ ],
11476
+ types: [
11477
+ "AwsCredentials",
11478
+ "SignSkillsAwsV4RequestOptions",
11479
+ "SkillsFetch",
11480
+ "SkillsLocalSnapshot",
11481
+ "SkillsNativeStorageConfig",
11482
+ "SkillsNativeStorageStatus",
11483
+ "SkillsPostgresQueryClient",
11484
+ "SkillsS3ObjectStoreOptions",
11485
+ "SkillsS3PutObjectOptions",
11486
+ "SkillsS3SnapshotPlanEntry",
11487
+ "SkillsS3StoredObject",
11488
+ "SkillsSnapshotFile",
11489
+ "SkillsStorageTable",
11490
+ "SkillsSyncRecord"
11491
+ ]
11492
+ };
11090
11493
  var SKILLS_ENV_NAMESPACE = "SKILLS";
11091
11494
  function resolveSkillsNativeStorageConfig(env = process.env) {
11092
11495
  assertNoRetiredModeEnvVars(env, {
@@ -11128,7 +11531,7 @@ function getSkillsNativeStorageStatus(options = {}) {
11128
11531
  const s3BucketEnv = readStorageEnv(env, "s3Bucket");
11129
11532
  const targetDir = options.targetDir ?? process.cwd();
11130
11533
  return {
11131
- package: "open-skills",
11534
+ package: "skills",
11132
11535
  tables: [...SKILLS_STORAGE_TABLES],
11133
11536
  env: {
11134
11537
  databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
@@ -11162,12 +11565,12 @@ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
11162
11565
  const files = [];
11163
11566
  if (existsSync17(projectStateDir)) {
11164
11567
  for (const filePath of walkFiles2(projectStateDir)) {
11165
- const bytes = readFileSync13(filePath);
11568
+ const bytes = readFileSync15(filePath);
11166
11569
  const relativePath = toPosix(relative4(targetDir, filePath));
11167
11570
  files.push({
11168
11571
  path: relativePath,
11169
11572
  sizeBytes: bytes.byteLength,
11170
- sha256: createHash4("sha256").update(bytes).digest("hex"),
11573
+ sha256: createHash5("sha256").update(bytes).digest("hex"),
11171
11574
  ...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
11172
11575
  });
11173
11576
  }
@@ -11192,12 +11595,12 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
11192
11595
  continue;
11193
11596
  }
11194
11597
  const bytes = Buffer.from(file.contentBase64, "base64");
11195
- const hash = createHash4("sha256").update(bytes).digest("hex");
11598
+ const hash = createHash5("sha256").update(bytes).digest("hex");
11196
11599
  if (hash !== file.sha256) {
11197
11600
  throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
11198
11601
  }
11199
- mkdirSync13(dirname9(absolutePath), { recursive: true });
11200
- writeFileSync12(absolutePath, bytes);
11602
+ mkdirSync12(dirname10(absolutePath), { recursive: true });
11603
+ writeFileSync11(absolutePath, bytes);
11201
11604
  written += 1;
11202
11605
  }
11203
11606
  return { written, skipped };
@@ -11300,7 +11703,7 @@ function createSkillsSnapshotSyncRecord(snapshot, options = {}) {
11300
11703
  kind: "local-snapshot",
11301
11704
  id: options.id ?? "project-state",
11302
11705
  updatedAt: snapshot.exportedAt,
11303
- source: options.source ?? "open-skills",
11706
+ source: options.source ?? "skills",
11304
11707
  payload: snapshot
11305
11708
  };
11306
11709
  }
@@ -11524,7 +11927,7 @@ function toIsoString(value) {
11524
11927
  return Number.isNaN(date.getTime()) ? value : date.toISOString();
11525
11928
  }
11526
11929
  function sha256Hex2(value) {
11527
- return createHash4("sha256").update(value).digest("hex");
11930
+ return createHash5("sha256").update(value).digest("hex");
11528
11931
  }
11529
11932
  function normalizeHeaders(headers) {
11530
11933
  const result = {};
@@ -11593,6 +11996,7 @@ export {
11593
11996
  unpinProjectSkill,
11594
11997
  syncSkillsToAgents,
11595
11998
  summarizeMcpToolContract,
11999
+ storageCapabilities,
11596
12000
  skillsPostgresSyncSchemaSql,
11597
12001
  skillExists,
11598
12002
  signSkillsAwsV4Request,
@@ -11747,6 +12151,8 @@ export {
11747
12151
  SKILLS_CLI_MCP_PARITY,
11748
12152
  SKILLS,
11749
12153
  RemoteSkillsClient,
12154
+ RemoteRouteUnsupportedError,
12155
+ RemoteRequestError,
11750
12156
  REMOTE_SKILL_RUN_CONTRACT_VERSION,
11751
12157
  PullSkillError,
11752
12158
  PROJECT_CONFIG_FILE,