@hasna/skills 0.10.3 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -561,6 +561,18 @@ skills pull --all --selection-profile default --json
561
561
  skills pull pdf-generate@0.5.2 --selection-profile default --json
562
562
  ```
563
563
 
564
+ To inspect a published version that is not selected, export its immutable bundle
565
+ to an existing owner-only directory outside Git and agent-native discovery:
566
+
567
+ ```bash
568
+ skills review-export example-skill@1.0.0 --output /absolute/private/review/example-skill-1.0.0.tar.gz --json
569
+ ```
570
+
571
+ The command verifies the version metadata, response headers, byte count, and
572
+ SHA-256 before creating a mode-0600 file. It prints a metadata-only receipt and
573
+ does not install or select the skill. Review and scan the private archive before
574
+ reading its content.
575
+
564
576
  Use `sync --selection-profile default --station station-example` when you also
565
577
  need a station receipt. Native-folder migration options belong to the older
566
578
  sync mode and cannot be mixed with profile sync.
@@ -945,6 +957,7 @@ of app folders, and `XDG_CONFIG_HOME` is not consulted at all.
945
957
  | `skills hook agents --json` | | Report maintained adapters and explicit coverage limits |
946
958
  | `skills migrate native` | | Inventory native copies; `--apply` archives managed copies, with explicit `--include-unmanaged`, `--include-vendor`, or reviewed `--target-manifest` selection |
947
959
  | `skills pull --all --selection-profile <id>` | | With CLI loading active, refresh the selected profile into the verified cache |
960
+ | `skills review-export <slug@version> --output <path>` | | Export one immutable hosted bundle to a private archive for review without selecting it |
948
961
  | `skills sync --selection-profile <id> [--check] [--station <id>]` | | Sync or check the selected profile/cache; optionally record station state |
949
962
  | `skills station-state <id>` | | Read a station's sync receipt in the authenticated workspace |
950
963
  | `skills run <name> [args]` | | Execute a skill directly |
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.10.3",
36871
+ version: "0.10.5",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -51946,12 +51946,20 @@ function assertManagedAgentBridge(agent, options = {}) {
51946
51946
  const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join20(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
51947
51947
  if (agent === "codex" && [...setting(expected), ...(codexConfig.skills?.config ?? []).filter((entry) => typeof entry.name === "string" && entry.name.trim() === CLI_BRIDGE_NAME)].some((entry) => entry.enabled === false))
51948
51948
  throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
51949
- const disabledBuiltin = (entry) => {
51950
- if (agent !== "codex" || !entry.vendor || !entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "skills", ".system"), aliases) + sep4))
51949
+ const disabledVendorSkill = (entry) => {
51950
+ if (agent !== "codex" || !entry.vendor)
51951
51951
  return false;
51952
- const matched = Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
51953
51952
  const settings = setting(entry.path);
51954
- return matched && settings.length === 1 && settings[0]?.enabled === false;
51953
+ if (settings.length !== 1 || settings[0]?.enabled !== false)
51954
+ return false;
51955
+ if (entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "plugins", "cache"), aliases) + sep4))
51956
+ return true;
51957
+ const runtimeCache = canonicalAgentPath(join20(home, ".cache", "codex-runtimes"), aliases);
51958
+ if (entry.path.startsWith(runtimeCache + sep4) && discovery?.method === "automatic" && discovery.roots.some((root) => entry.path.startsWith(root + sep4)))
51959
+ return true;
51960
+ if (!entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "skills", ".system"), aliases) + sep4))
51961
+ return false;
51962
+ return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
51955
51963
  };
51956
51964
  if (!discovery || discovery.agent !== agent)
51957
51965
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
@@ -51968,7 +51976,7 @@ function assertManagedAgentBridge(agent, options = {}) {
51968
51976
  throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
51969
51977
  }
51970
51978
  const inventory = inventoryNativeSkills(home, { includeVendor: true, guardHermes: agent === "hermes", agents: [agent], projectDirs: [...roots], agentRoots: discovery.roots.map((path) => ({ agent, path })), allowRootAliases: aliases.length > 0 });
51971
- const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledBuiltin(entry));
51979
+ const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledVendorSkill(entry));
51972
51980
  if (unexpected.length) {
51973
51981
  const paths = unexpected.slice(0, 3).map((entry) => {
51974
51982
  const escaped = JSON.stringify(entry.path).replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -111804,10 +111812,100 @@ var init_registry_sync = __esm(() => {
111804
111812
  init_skill_validation();
111805
111813
  });
111806
111814
 
111815
+ // src/lib/review-export.ts
111816
+ import { constants as constants22, closeSync as closeSync25, existsSync as existsSync35, fstatSync as fstatSync25, fsyncSync as fsyncSync7, lstatSync as lstatSync31, openSync as openSync25, realpathSync as realpathSync15, statSync as statSync23, writeFileSync as writeFileSync30 } from "fs";
111817
+ import { basename as basename14, dirname as dirname36, isAbsolute as isAbsolute29, join as join61, parse as parse12, resolve as resolve26, sep as sep13 } from "path";
111818
+ function exactVersion(spec) {
111819
+ const at2 = spec.indexOf("@");
111820
+ const slug2 = spec.slice(0, at2);
111821
+ const version2 = spec.slice(at2 + 1);
111822
+ if (at2 < 1 || slug2.length > 128 || !SLUG2.test(slug2) || !isValidSkillVersion(version2)) {
111823
+ throw new Error("Review export requires an exact valid slug@version.");
111824
+ }
111825
+ return { slug: slug2, version: version2 };
111826
+ }
111827
+ function assertPrivateDestination(output2) {
111828
+ if (!isAbsolute29(output2) || resolve26(output2) !== output2 || !basename14(output2).endsWith(".tar.gz")) {
111829
+ throw new Error("Review output must be an absolute, normalized .tar.gz path.");
111830
+ }
111831
+ const parent = dirname36(output2);
111832
+ if (realpathSync15(parent) !== parent || lstatSync31(parent).isSymbolicLink()) {
111833
+ throw new Error("Review output parent must be a real directory, not a symlink.");
111834
+ }
111835
+ const parentStat = statSync23(parent);
111836
+ if (!parentStat.isDirectory() || (parentStat.mode & 63) !== 0 || process.getuid && parentStat.uid !== process.getuid()) {
111837
+ throw new Error("Review output parent must be an owner-only directory (mode 0700).");
111838
+ }
111839
+ const segments = parent.toLowerCase().split(sep13);
111840
+ if (segments.some((segment) => NATIVE_CONTAINERS.has(segment)) || segments.some((segment, index) => segment === ".config" && segments[index + 1] === "opencode") || segments.some((segment, index) => segment === ".hasna" && segments[index + 1] === "skills")) {
111841
+ throw new Error("Review output cannot be placed in agent-native discovery or Skills installation paths.");
111842
+ }
111843
+ for (let current = parent;; current = dirname36(current)) {
111844
+ if (existsSync35(join61(current, ".git"))) {
111845
+ throw new Error("Review output cannot be placed inside a Git worktree.");
111846
+ }
111847
+ if (current === parse12(current).root)
111848
+ break;
111849
+ }
111850
+ return output2;
111851
+ }
111852
+ async function exportSkillVersionForReview(spec, output2, client2) {
111853
+ const { slug: slug2, version: version2 } = exactVersion(spec);
111854
+ const target = assertPrivateDestination(output2);
111855
+ if (existsSync35(target))
111856
+ throw new Error("Review output already exists; choose a new filename.");
111857
+ const metadata = await client2.getSkillVersion(slug2, version2);
111858
+ if (!metadata)
111859
+ throw new Error(`Published skill version ${slug2}@${version2} was not found.`);
111860
+ if (!/^[a-f0-9]{64}$/i.test(metadata.bundleSha256) || !Number.isSafeInteger(metadata.bundleByteSize) || metadata.bundleByteSize < 1 || metadata.bundleByteSize > MAX_REVIEW_BUNDLE_BYTES) {
111861
+ throw new Error("Published bundle metadata is invalid or exceeds the review size limit.");
111862
+ }
111863
+ const response = await client2.getBundle(slug2, version2);
111864
+ if (!response)
111865
+ throw new Error(`Published bundle for ${slug2}@${version2} is unavailable.`);
111866
+ if (!response.ok) {
111867
+ response.body?.cancel().catch(() => {});
111868
+ throw new Error(`Published bundle request failed: HTTP ${response.status}.`);
111869
+ }
111870
+ const headerSha = response.headers.get("X-Skill-Bundle-Sha256");
111871
+ const headerVersion = response.headers.get("X-Skill-Version");
111872
+ if (!headerSha || headerSha.toLowerCase() !== metadata.bundleSha256.toLowerCase() || headerVersion !== version2) {
111873
+ response.body?.cancel().catch(() => {});
111874
+ throw new Error("Published bundle headers do not match the exact version metadata.");
111875
+ }
111876
+ const bytes = await readBoundedResponse(response, Math.min(metadata.bundleByteSize, MAX_REVIEW_BUNDLE_BYTES));
111877
+ if (bytes.byteLength !== metadata.bundleByteSize || sha256Hex(bytes) !== metadata.bundleSha256.toLowerCase()) {
111878
+ throw new Error("Published bundle bytes do not match the exact version metadata.");
111879
+ }
111880
+ const fd = openSync25(target, constants22.O_WRONLY | constants22.O_CREAT | constants22.O_EXCL | constants22.O_NOFOLLOW, 384);
111881
+ try {
111882
+ writeFileSync30(fd, bytes);
111883
+ fsyncSync7(fd);
111884
+ if (fstatSync25(fd).size !== bytes.byteLength)
111885
+ throw new Error("Incomplete review output write.");
111886
+ } catch (error61) {
111887
+ throw new Error(`Review output write failed. A private partial file may remain at ${target}; use Trash to remove it before reusing this path.`, { cause: error61 });
111888
+ } finally {
111889
+ closeSync25(fd);
111890
+ }
111891
+ return { slug: slug2, version: version2, bundleSha256: metadata.bundleSha256.toLowerCase(), bundleByteSize: bytes.byteLength, output: target };
111892
+ }
111893
+ var MAX_REVIEW_BUNDLE_BYTES, SLUG2, NATIVE_CONTAINERS;
111894
+ var init_review_export = __esm(() => {
111895
+ init_remote_files();
111896
+ init_skill_bundle();
111897
+ init_skill_version();
111898
+ init_native_discovery_roots();
111899
+ MAX_REVIEW_BUNDLE_BYTES = 32 * 1024 * 1024;
111900
+ SLUG2 = /^[a-z0-9][a-z0-9._-]*$/;
111901
+ NATIVE_CONTAINERS = new Set(NATIVE_SKILL_ROOTS.map(([, root2]) => root2.split("/")[0]).filter((root2) => root2 !== ".config"));
111902
+ });
111903
+
111807
111904
  // src/cli/commands/registry.ts
111808
111905
  var exports_registry2 = {};
111809
111906
  __export(exports_registry2, {
111810
111907
  registerVersions: () => registerVersions,
111908
+ registerReviewExport: () => registerReviewExport,
111811
111909
  registerRegistry: () => registerRegistry,
111812
111910
  registerPull: () => registerPull,
111813
111911
  registerLifecycle: () => registerLifecycle
@@ -111819,12 +111917,12 @@ function registerRegistry(parent) {
111819
111917
  async function writeJson2(value, space) {
111820
111918
  const text4 = `${JSON.stringify(value, null, space)}
111821
111919
  `;
111822
- await new Promise((resolve26, reject2) => {
111920
+ await new Promise((resolve27, reject2) => {
111823
111921
  process.stdout.write(text4, (error61) => {
111824
111922
  if (error61)
111825
111923
  reject2(error61);
111826
111924
  else
111827
- resolve26();
111925
+ resolve27();
111828
111926
  });
111829
111927
  });
111830
111928
  }
@@ -111966,6 +112064,26 @@ Versions of ${name}
111966
112064
  }
111967
112065
  });
111968
112066
  }
112067
+ function registerReviewExport(parent) {
112068
+ parent.command("review-export").argument("<slug@version>", "Exact published skill version to review").requiredOption("--output <path>", "New .tar.gz path inside an existing owner-only directory outside Git and agent discovery").option("--json", "Print a metadata-only receipt", false).description("Export an exact hosted bundle for private review without changing skill selection").action(async (spec, options) => {
112069
+ try {
112070
+ const client2 = await createRemoteSkillsClient();
112071
+ if (!client2)
112072
+ throw new Error("No Skills instance credential is configured. Run skills login.");
112073
+ const receipt = await exportSkillVersionForReview(spec, options.output, client2);
112074
+ if (options.json)
112075
+ console.log(JSON.stringify(receipt));
112076
+ else
112077
+ console.log(`Exported ${receipt.slug}@${receipt.version} to ${receipt.output} (${receipt.bundleSha256}).`);
112078
+ } catch (error61) {
112079
+ if (options.json)
112080
+ console.log(JSON.stringify({ error: error61.message }));
112081
+ else
112082
+ console.error(source_default.red(error61.message));
112083
+ process.exitCode = 1;
112084
+ }
112085
+ });
112086
+ }
111969
112087
  function registerLifecycle(parent) {
111970
112088
  parent.command("lifecycle").argument("<name>", "Skill name on the configured instance").requiredOption("--state <state>", "Lifecycle state: active or archived").requiredOption("--revision <revision>", "Exact revision id from skills info/get").option("--reason <reason>", "Bounded archive reason").option("--replacement <slug>", "Optional successor slug").option("--json", "Output as JSON", false).description("Change a hosted skill's active/archive lifecycle with an exact revision guard").action(async (name, options) => {
111971
112089
  if (options.state !== "active" && options.state !== "archived")
@@ -111994,6 +112112,7 @@ var init_registry2 = __esm(() => {
111994
112112
  init_selection_resolver();
111995
112113
  init_context();
111996
112114
  init_selection_cache();
112115
+ init_review_export();
111997
112116
  });
111998
112117
 
111999
112118
  // src/cli/commands/publish.ts
@@ -112007,9 +112126,9 @@ __export(exports_publish, {
112007
112126
  PushSkillError: () => PushSkillError
112008
112127
  });
112009
112128
  import { execFileSync as execFileSync2 } from "child_process";
112010
- import { existsSync as existsSync35, readFileSync as readFileSync43 } from "fs";
112129
+ import { existsSync as existsSync36, readFileSync as readFileSync43 } from "fs";
112011
112130
  import { hostname as hostname5 } from "os";
112012
- import { join as join61 } from "path";
112131
+ import { join as join62 } from "path";
112013
112132
  function registerPublish(parent) {
112014
112133
  parent.command("push").argument("<name>", "Name of a skill in the local corpus (~/.hasna/skills/installed or the migrated ~/.hasna/skills/skills)").option("--version <version>", "Override the version recorded on the instance").option("--force-new-version", "If name@version already exists with different content, publish as the next patch version", false).option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
112015
112134
  try {
@@ -112061,8 +112180,8 @@ async function pushSkill(name, options = {}) {
112061
112180
  const packed = packSkillBundle(skill.path, { maxUnpackedBytes: MAX_UNPACKED_BYTES });
112062
112181
  await verifyPackedCanonicalHash(skill.name, packed);
112063
112182
  const versionManifest = buildVersionManifest(skill.path, packed);
112064
- const skillMdPath = join61(skill.path, "SKILL.md");
112065
- const skillMd = existsSync35(skillMdPath) ? readFileSync43(skillMdPath, "utf-8") : undefined;
112183
+ const skillMdPath = join62(skill.path, "SKILL.md");
112184
+ const skillMd = existsSync36(skillMdPath) ? readFileSync43(skillMdPath, "utf-8") : undefined;
112066
112185
  const base3 = {
112067
112186
  slug: skill.name,
112068
112187
  path: skill.path,
@@ -112300,7 +112419,7 @@ var exports_private_publications = {};
112300
112419
  __export(exports_private_publications, {
112301
112420
  registerPrivatePublications: () => registerPrivatePublications
112302
112421
  });
112303
- import { resolve as resolve26 } from "path";
112422
+ import { resolve as resolve27 } from "path";
112304
112423
  function verifyOptions(options, action) {
112305
112424
  if (options.userId === undefined !== (options.membershipId === undefined) || options.userId !== undefined && (!publicationUuid(options.userId) || !publicationUuid(options.membershipId)))
112306
112425
  throw new PrivatePublicationError("PUBLICATION_CONTEXT_REQUIRED", "Provide both observed --user-id and --membership-id UUIDs.");
@@ -112345,9 +112464,9 @@ function registerPrivatePublications(parent) {
112345
112464
  if (code2 === null)
112346
112465
  return;
112347
112466
  const context2 = options.userId ? { userId: options.userId, membershipId: options.membershipId } : undefined;
112348
- const client2 = await privatePublicationSession(options.email, code2, context2), directory2 = resolve26(options.recoveryDir);
112467
+ const client2 = await privatePublicationSession(options.email, code2, context2), directory2 = resolve27(options.recoveryDir);
112349
112468
  if (action === "publish")
112350
- await preparePrivatePublication(client2, resolve26(args2[0]), directory2, { skillId: options.skillId, expectedCurrentVersionId: options.expectedCurrentVersion ?? null, idempotencyKey: options.idempotencyKey });
112469
+ await preparePrivatePublication(client2, resolve27(args2[0]), directory2, { skillId: options.skillId, expectedCurrentVersionId: options.expectedCurrentVersion ?? null, idempotencyKey: options.idempotencyKey });
112351
112470
  const result2 = action === "publish" || action === "resume" ? await continuePrivatePublication(client2, directory2, { confirm: true, waitMs: Number(options.waitSeconds) * 1000 }) : await inspectPrivatePublication(client2, directory2, action === "cancel");
112352
112471
  print2(result2, options.json);
112353
112472
  } catch (error61) {
@@ -112449,7 +112568,7 @@ __export(exports_auth, {
112449
112568
  import { createInterface as createInterface3 } from "readline";
112450
112569
  function prompt(question) {
112451
112570
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
112452
- return new Promise((resolve27) => {
112571
+ return new Promise((resolve28) => {
112453
112572
  let settled = false;
112454
112573
  const finish = (answer) => {
112455
112574
  if (settled)
@@ -112458,7 +112577,7 @@ function prompt(question) {
112458
112577
  rl.close();
112459
112578
  if (answer === null)
112460
112579
  process.exitCode = 130;
112461
- resolve27(answer);
112580
+ resolve28(answer);
112462
112581
  };
112463
112582
  rl.once("SIGINT", () => finish(null));
112464
112583
  rl.once("close", () => finish(null));
@@ -112636,7 +112755,7 @@ Skipped. Run it any time: skills setup agents`));
112636
112755
  function promptHidden(question) {
112637
112756
  const stdin = process.stdin;
112638
112757
  process.stdout.write(question);
112639
- return new Promise((resolve27) => {
112758
+ return new Promise((resolve28) => {
112640
112759
  let value = "";
112641
112760
  const done = (answer) => {
112642
112761
  stdin.off("data", onData);
@@ -112646,7 +112765,7 @@ function promptHidden(question) {
112646
112765
  `);
112647
112766
  if (answer === null)
112648
112767
  process.exitCode = 130;
112649
- resolve27(answer);
112768
+ resolve28(answer);
112650
112769
  };
112651
112770
  const onData = (chunk2) => {
112652
112771
  for (const character of chunk2.toString()) {
@@ -113192,7 +113311,7 @@ function promptInvitationProof(kind) {
113192
113311
  const label = kind === "token" ? "invitation token" : "recovery code";
113193
113312
  const character = kind === "token" ? /^[A-Za-z0-9_-]$/ : /^\d$/;
113194
113313
  const { stdin, stderr: output2 } = process, wasRaw = stdin.isRaw, wasFlowing = stdin.readableFlowing;
113195
- return new Promise((resolve27) => {
113314
+ return new Promise((resolve28) => {
113196
113315
  let value = "", settled = false, overflow = false;
113197
113316
  const finish = (answer) => {
113198
113317
  if (settled)
@@ -113209,7 +113328,7 @@ function promptInvitationProof(kind) {
113209
113328
  `);
113210
113329
  if (answer === null)
113211
113330
  process.exitCode = 130;
113212
- resolve27(answer);
113331
+ resolve28(answer);
113213
113332
  };
113214
113333
  const cancel = () => finish(null);
113215
113334
  const keypress = (text4, key) => {
@@ -113781,11 +113900,11 @@ var init_storage2 = __esm(() => {
113781
113900
  });
113782
113901
 
113783
113902
  // src/lib/registry-reconcile.ts
113784
- import { existsSync as existsSync36, readFileSync as readFileSync44, statSync as statSync23, writeFileSync as writeFileSync30 } from "fs";
113785
- import { join as join62 } from "path";
113903
+ import { existsSync as existsSync37, readFileSync as readFileSync44, statSync as statSync24, writeFileSync as writeFileSync31 } from "fs";
113904
+ import { join as join63 } from "path";
113786
113905
  function isDirectory2(path) {
113787
113906
  try {
113788
- return statSync23(path).isDirectory();
113907
+ return statSync24(path).isDirectory();
113789
113908
  } catch {
113790
113909
  return false;
113791
113910
  }
@@ -113793,12 +113912,12 @@ function isDirectory2(path) {
113793
113912
  function migrationNeeded(options) {
113794
113913
  if (options.rootDir)
113795
113914
  return false;
113796
- const appDir = options.homeDir ? join62(options.homeDir, ".hasna", "skills") : getDataDir();
113797
- return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join62(appDir, SKILLS_CACHE_DIRNAME)));
113915
+ const appDir = options.homeDir ? join63(options.homeDir, ".hasna", "skills") : getDataDir();
113916
+ return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join63(appDir, SKILLS_CACHE_DIRNAME)));
113798
113917
  }
113799
113918
  function readBaseline(skillDir) {
113800
- const markerPath = join62(skillDir, PULL_MARKER_FILE);
113801
- if (!existsSync36(markerPath))
113919
+ const markerPath = join63(skillDir, PULL_MARKER_FILE);
113920
+ if (!existsSync37(markerPath))
113802
113921
  return;
113803
113922
  try {
113804
113923
  const marker = JSON.parse(readFileSync44(markerPath, "utf-8"));
@@ -113813,8 +113932,8 @@ function readBaseline(skillDir) {
113813
113932
  }
113814
113933
  }
113815
113934
  function readCursor(root2) {
113816
- const path = join62(root2, SYNC_CURSOR_FILE);
113817
- if (!existsSync36(path))
113935
+ const path = join63(root2, SYNC_CURSOR_FILE);
113936
+ if (!existsSync37(path))
113818
113937
  return { runCount: 0 };
113819
113938
  try {
113820
113939
  const cursor2 = JSON.parse(readFileSync44(path, "utf-8"));
@@ -113826,12 +113945,12 @@ function readCursor(root2) {
113826
113945
  function resolveCorpusRootReadOnly(options) {
113827
113946
  if (options.rootDir)
113828
113947
  return { root: options.rootDir, migrationPending: false };
113829
- const appDir = options.homeDir ? join62(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
113830
- const cache3 = join62(appDir, SKILLS_CACHE_DIRNAME);
113948
+ const appDir = options.homeDir ? join63(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
113949
+ const cache3 = join63(appDir, SKILLS_CACHE_DIRNAME);
113831
113950
  if (isOwnerLayoutMigrated(appDir) && isDirectory2(cache3)) {
113832
113951
  return { root: cache3, migrationPending: false };
113833
113952
  }
113834
- return { root: join62(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
113953
+ return { root: join63(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
113835
113954
  }
113836
113955
  function remoteRowToSkill(record11) {
113837
113956
  const slug2 = typeof record11.slug === "string" ? record11.slug : typeof record11.name === "string" ? record11.name : undefined;
@@ -113845,7 +113964,7 @@ function remoteRowToSkill(record11) {
113845
113964
  }
113846
113965
  function recheckLocalSide(plannedLocal, localDir, ops = {
113847
113966
  pack: (dir) => packSkillBundle(dir).sha256,
113848
- exists: existsSync36
113967
+ exists: existsSync37
113849
113968
  }) {
113850
113969
  let localNow;
113851
113970
  try {
@@ -113965,7 +114084,7 @@ async function reconcileRegistry(options = {}) {
113965
114084
  for (const slug2 of allSlugs) {
113966
114085
  const local = locals.get(slug2);
113967
114086
  const remote = remotes.get(slug2);
113968
- const baseline = local ? readBaseline(join62(root2, slug2)) : undefined;
114087
+ const baseline = local ? readBaseline(join63(root2, slug2)) : undefined;
113969
114088
  const { state, reason } = classifySkill(local, remote, baseline);
113970
114089
  let { action, reason: actionReason } = resolveAction(state, direction, conflict);
113971
114090
  if (state === "remote-only" && isDigestless(remote)) {
@@ -114024,7 +114143,7 @@ async function reconcileRegistry(options = {}) {
114024
114143
  try {
114025
114144
  await pushSkill(slug2, { rootDir: root2, client: client2 });
114026
114145
  const pushed = locals.get(slug2);
114027
- writePullMarker(join62(root2, slug2), {
114146
+ writePullMarker(join63(root2, slug2), {
114028
114147
  skill: slug2,
114029
114148
  ...pushed?.version ? { version: pushed.version } : {},
114030
114149
  ...pushed?.sha256 ? { contentHash: pushed.sha256 } : {},
@@ -114096,7 +114215,7 @@ async function reconcileRegistry(options = {}) {
114096
114215
  runCount: readCursor(root2).runCount + 1,
114097
114216
  summary
114098
114217
  };
114099
- writeFileSync30(join62(root2, SYNC_CURSOR_FILE), `${JSON.stringify(cursor2, null, 2)}
114218
+ writeFileSync31(join63(root2, SYNC_CURSOR_FILE), `${JSON.stringify(cursor2, null, 2)}
114100
114219
  `);
114101
114220
  return {
114102
114221
  corpusRoot: root2,
@@ -122402,11 +122521,12 @@ var { registerPortableSkillCommands: registerPortableSkillCommands2 } = await Pr
122402
122521
  registerPortableSkillCommands2(program2);
122403
122522
  var { registerSchedule: registerSchedule2 } = await Promise.resolve().then(() => (init_schedule(), exports_schedule));
122404
122523
  registerSchedule2(program2);
122405
- var { registerRegistry: registerRegistry2, registerPull: registerPull2, registerVersions: registerVersions2, registerLifecycle: registerLifecycle2 } = await Promise.resolve().then(() => (init_registry2(), exports_registry2));
122524
+ var { registerRegistry: registerRegistry2, registerPull: registerPull2, registerVersions: registerVersions2, registerLifecycle: registerLifecycle2, registerReviewExport: registerReviewExport2 } = await Promise.resolve().then(() => (init_registry2(), exports_registry2));
122406
122525
  registerRegistry2(program2);
122407
122526
  registerPull2(program2);
122408
122527
  registerVersions2(program2);
122409
122528
  registerLifecycle2(program2);
122529
+ registerReviewExport2(program2);
122410
122530
  var { registerPublish: registerPublish2 } = await Promise.resolve().then(() => (init_publish(), exports_publish));
122411
122531
  registerPublish2(program2);
122412
122532
  var { registerPrivatePublications: registerPrivatePublications2 } = await Promise.resolve().then(() => (init_private_publications(), exports_private_publications));
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.10.3",
20022
+ version: "0.10.5",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.10.3",
10
+ version: "0.10.5",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.10.3",
24125
+ version: "0.10.5",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.10.3",
24128
+ version: "0.10.5",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -13,4 +13,6 @@ export declare function registerPull(parent: Command): void;
13
13
  * instance, newest first, with the one the instance currently serves marked (hasna/apps#1630).
14
14
  */
15
15
  export declare function registerVersions(parent: Command): void;
16
+ /** Read one immutable bundle into a private review artifact without selecting or installing it. */
17
+ export declare function registerReviewExport(parent: Command): void;
16
18
  export declare function registerLifecycle(parent: Command): void;
package/dist/index.js CHANGED
@@ -9611,7 +9611,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
9611
9611
  // package.json
9612
9612
  var package_default = {
9613
9613
  name: "@hasna/skills",
9614
- version: "0.10.3",
9614
+ version: "0.10.5",
9615
9615
  description: "Skills library for AI coding agents",
9616
9616
  type: "module",
9617
9617
  bin: {
@@ -14125,12 +14125,20 @@ function assertManagedAgentBridge(agent, options = {}) {
14125
14125
  const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join16(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
14126
14126
  if (agent === "codex" && [...setting(expected), ...(codexConfig.skills?.config ?? []).filter((entry) => typeof entry.name === "string" && entry.name.trim() === CLI_BRIDGE_NAME)].some((entry) => entry.enabled === false))
14127
14127
  throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
14128
- const disabledBuiltin = (entry) => {
14129
- if (agent !== "codex" || !entry.vendor || !entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "skills", ".system"), aliases) + sep4))
14128
+ const disabledVendorSkill = (entry) => {
14129
+ if (agent !== "codex" || !entry.vendor)
14130
14130
  return false;
14131
- const matched = Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
14132
14131
  const settings = setting(entry.path);
14133
- return matched && settings.length === 1 && settings[0]?.enabled === false;
14132
+ if (settings.length !== 1 || settings[0]?.enabled !== false)
14133
+ return false;
14134
+ if (entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "plugins", "cache"), aliases) + sep4))
14135
+ return true;
14136
+ const runtimeCache = canonicalAgentPath(join16(home, ".cache", "codex-runtimes"), aliases);
14137
+ if (entry.path.startsWith(runtimeCache + sep4) && discovery?.method === "automatic" && discovery.roots.some((root) => entry.path.startsWith(root + sep4)))
14138
+ return true;
14139
+ if (!entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "skills", ".system"), aliases) + sep4))
14140
+ return false;
14141
+ return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
14134
14142
  };
14135
14143
  if (!discovery || discovery.agent !== agent)
14136
14144
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
@@ -14147,7 +14155,7 @@ function assertManagedAgentBridge(agent, options = {}) {
14147
14155
  throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
14148
14156
  }
14149
14157
  const inventory = inventoryNativeSkills(home, { includeVendor: true, guardHermes: agent === "hermes", agents: [agent], projectDirs: [...roots], agentRoots: discovery.roots.map((path) => ({ agent, path })), allowRootAliases: aliases.length > 0 });
14150
- const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledBuiltin(entry));
14158
+ const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledVendorSkill(entry));
14151
14159
  if (unexpected.length) {
14152
14160
  const paths = unexpected.slice(0, 3).map((entry) => {
14153
14161
  const escaped = JSON.stringify(entry.path).replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -0,0 +1,16 @@
1
+ export interface ReviewExportClient {
2
+ getSkillVersion(slug: string, version: string): Promise<{
3
+ bundleSha256: string;
4
+ bundleByteSize: number;
5
+ } | null>;
6
+ getBundle(slug: string, version: string): Promise<Response | null>;
7
+ }
8
+ export interface ReviewExportReceipt {
9
+ slug: string;
10
+ version: string;
11
+ bundleSha256: string;
12
+ bundleByteSize: number;
13
+ output: string;
14
+ }
15
+ /** Export for human review only. This never changes corpus, cache, profiles, or native discovery. */
16
+ export declare function exportSkillVersionForReview(spec: string, output: string, client: ReviewExportClient): Promise<ReviewExportReceipt>;
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.10.3",
32376
+ version: "0.10.5",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -41996,12 +41996,20 @@ function assertManagedAgentBridge(agent, options = {}) {
41996
41996
  const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join21(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
41997
41997
  if (agent === "codex" && [...setting(expected), ...(codexConfig.skills?.config ?? []).filter((entry) => typeof entry.name === "string" && entry.name.trim() === CLI_BRIDGE_NAME)].some((entry) => entry.enabled === false))
41998
41998
  throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
41999
- const disabledBuiltin = (entry) => {
42000
- if (agent !== "codex" || !entry.vendor || !entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "skills", ".system"), aliases) + sep4))
41999
+ const disabledVendorSkill = (entry) => {
42000
+ if (agent !== "codex" || !entry.vendor)
42001
42001
  return false;
42002
- const matched = Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
42003
42002
  const settings = setting(entry.path);
42004
- return matched && settings.length === 1 && settings[0]?.enabled === false;
42003
+ if (settings.length !== 1 || settings[0]?.enabled !== false)
42004
+ return false;
42005
+ if (entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "plugins", "cache"), aliases) + sep4))
42006
+ return true;
42007
+ const runtimeCache = canonicalAgentPath(join21(home, ".cache", "codex-runtimes"), aliases);
42008
+ if (entry.path.startsWith(runtimeCache + sep4) && discovery?.method === "automatic" && discovery.roots.some((root2) => entry.path.startsWith(root2 + sep4)))
42009
+ return true;
42010
+ if (!entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "skills", ".system"), aliases) + sep4))
42011
+ return false;
42012
+ return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
42005
42013
  };
42006
42014
  if (!discovery || discovery.agent !== agent)
42007
42015
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
@@ -42018,7 +42026,7 @@ function assertManagedAgentBridge(agent, options = {}) {
42018
42026
  throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
42019
42027
  }
42020
42028
  const inventory = inventoryNativeSkills(home, { includeVendor: true, guardHermes: agent === "hermes", agents: [agent], projectDirs: [...roots], agentRoots: discovery.roots.map((path) => ({ agent, path })), allowRootAliases: aliases.length > 0 });
42021
- const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledBuiltin(entry));
42029
+ const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledVendorSkill(entry));
42022
42030
  if (unexpected.length) {
42023
42031
  const paths = unexpected.slice(0, 3).map((entry) => {
42024
42032
  const escaped = JSON.stringify(entry.path).replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {