@hasna/skills 0.10.55 → 0.10.57

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/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.55",
20022
+ version: "0.10.57",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -50788,6 +50788,97 @@ function read(path, changes) {
50788
50788
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
50789
50789
  return readFileSync15(path, "utf8");
50790
50790
  }
50791
+ function assertReviewedClaudeCacheClosures(home, config2, review) {
50792
+ const enabledIds = Object.entries(config2.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id]) => id);
50793
+ if (enabledIds.length === 0)
50794
+ return;
50795
+ const registryPath = join26(home, ".claude/plugins/installed_plugins.json");
50796
+ const cacheRoot = join26(home, ".claude/plugins/cache");
50797
+ const registryText = read(registryPath);
50798
+ if (registryText === null)
50799
+ return;
50800
+ const registry2 = parseConfig(registryText, registryPath);
50801
+ if (registry2.version !== 2 || !registry2.plugins || typeof registry2.plugins !== "object" || Array.isArray(registry2.plugins))
50802
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
50803
+ const cachePath = (path) => typeof path === "string" && isAbsolute15(path) && resolve17(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep6));
50804
+ const activeRoots = new Set;
50805
+ for (const id of enabledIds) {
50806
+ const rows = registry2.plugins[id];
50807
+ if (rows === undefined)
50808
+ continue;
50809
+ if (!Array.isArray(rows))
50810
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
50811
+ for (const row of rows) {
50812
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
50813
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
50814
+ }
50815
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep6))) {
50816
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename9(row.installPath) !== row.version)) {
50817
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
50818
+ }
50819
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute15(row.projectPath) && resolve17(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
50820
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
50821
+ activeRoots.add(row.installPath);
50822
+ }
50823
+ }
50824
+ }
50825
+ if (activeRoots.size === 0)
50826
+ return;
50827
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
50828
+ if (!registrySource)
50829
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
50830
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
50831
+ const requireSource = (path, description, manifest = false) => {
50832
+ if (!reviewedFile(path, manifest))
50833
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
50834
+ };
50835
+ for (const root of activeRoots) {
50836
+ safe4(root);
50837
+ const rootStat = lstatSync18(root, { throwIfNoEntry: false });
50838
+ if (!rootStat?.isDirectory() || realpathSync5(root) !== root)
50839
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
50840
+ const manifestPath = join26(root, ".claude-plugin/plugin.json");
50841
+ const manifestStat = lstatSync18(manifestPath, { throwIfNoEntry: false });
50842
+ let manifest;
50843
+ if (manifestStat) {
50844
+ if (!manifestStat.isFile())
50845
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
50846
+ requireSource(manifestPath, "manifest", true);
50847
+ const raw = read(manifestPath);
50848
+ if (raw === null)
50849
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
50850
+ manifest = JSON.parse(raw);
50851
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
50852
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
50853
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
50854
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
50855
+ }
50856
+ const roots = new Set([join26(root, "skills")]);
50857
+ if (lstatSync18(join26(root, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
50858
+ roots.add(root);
50859
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
50860
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
50861
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
50862
+ for (const value of declared) {
50863
+ const path = resolve17(root, value);
50864
+ if (path !== root && !path.startsWith(root + sep6))
50865
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
50866
+ roots.add(path.endsWith(`${sep6}SKILL.md`) ? dirname16(path) : path);
50867
+ }
50868
+ for (const skillRoot of roots)
50869
+ if (!review.roots.includes(skillRoot))
50870
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
50871
+ const hooksPath = join26(root, "hooks/hooks.json");
50872
+ const hooksStat = lstatSync18(hooksPath, { throwIfNoEntry: false });
50873
+ if (hooksStat) {
50874
+ if (!hooksStat.isFile())
50875
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
50876
+ requireSource(hooksPath, "hook manifest");
50877
+ }
50878
+ if (typeof manifest?.hooks === "string")
50879
+ requireSource(resolve17(root, manifest.hooks), "manifest-declared hook source");
50880
+ }
50881
+ }
50791
50882
  function projectNativeDiscoveryFields(text2, format, fields2, path) {
50792
50883
  const object7 = format === "yaml" ? parseHermesConfig(text2) : parseConfig(text2, path, format === "toml");
50793
50884
  if (!object7 || typeof object7 !== "object" || Array.isArray(object7))
@@ -51254,6 +51345,8 @@ function resolveAgentDiscovery(options) {
51254
51345
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
51255
51346
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
51256
51347
  });
51348
+ if (agent === "claude")
51349
+ assertReviewedClaudeCacheClosures(home, config2, review);
51257
51350
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
51258
51351
  const unique = combined.filter((source, index) => combined.findIndex((candidate) => isDeepStrictEqual4(candidate, source)) === index);
51259
51352
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
package/bin/migrate.js CHANGED
@@ -25,7 +25,7 @@ import { join as join5 } from "path";
25
25
  // package.json
26
26
  var package_default = {
27
27
  name: "@hasna/skills",
28
- version: "0.10.55",
28
+ version: "0.10.57",
29
29
  description: "Skills library for AI coding agents",
30
30
  type: "module",
31
31
  bin: {
package/bin/server.js CHANGED
@@ -24140,7 +24140,7 @@ restoreLaunchCwd();
24140
24140
  // package.json
24141
24141
  var package_default = {
24142
24142
  name: "@hasna/skills",
24143
- version: "0.10.55",
24143
+ version: "0.10.57",
24144
24144
  description: "Skills library for AI coding agents",
24145
24145
  type: "module",
24146
24146
  bin: {
package/bin/worker.js CHANGED
@@ -24143,7 +24143,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24143
24143
  // package.json
24144
24144
  var package_default = {
24145
24145
  name: "@hasna/skills",
24146
- version: "0.10.55",
24146
+ version: "0.10.57",
24147
24147
  description: "Skills library for AI coding agents",
24148
24148
  type: "module",
24149
24149
  bin: {
package/dist/index.d.ts CHANGED
@@ -65,6 +65,8 @@ export type { ClaudeManagedHookProjection, ClaudeManagedHookProjectionOptions }
65
65
  export { captureDiscoveryDirectories, captureDiscoveryByteSources, captureDiscoveryPathSources, DISCOVERY_ROOT_UNRESOLVED, isDiscoveryRootUnresolved, type DiscoveryDirectory, type DiscoveryExecutable, type DiscoverySource } from "./lib/agent-discovery.js";
66
66
  export { captureClaudeMarketplaceRegistry, captureClaudeMarketplaceRegistryV2 } from "./lib/claude-marketplace-registry.js";
67
67
  export { captureClaudeMarketplaceEntry, hashClaudeMarketplaceEntry, CLAUDE_MARKETPLACE_ENTRY_HASH_MODE, type ClaudeMarketplaceEntrySelector, type ClaudeMarketplaceEntryWitness } from "./lib/claude-marketplace-entry-witness.js";
68
+ export { captureClaudeProspectiveCandidateClosure } from "./lib/claude-prospective-review.js";
69
+ export { hashClaudePluginManifest } from "./lib/claude-plugin-manifest-witness.js";
68
70
  export { captureClaudeSettings, captureClaudeSettingsV2, captureClaudeSettingsV3, captureClaudeSettingsV4, upgradeClaudeSettingsWitness, upgradeClaudeSettingsWitnessV4 } from "./lib/claude-settings-witness.js";
69
71
  export { buildPluginProjection, pluginTreeDigest, type PluginProjectionManifest, type PluginPayloadMapping, type PluginFileWitness } from "./lib/plugin-projection.js";
70
72
  export { planPluginAdmission, admitPlugin, resolveAdmittedPlugin, pluginResolverCommand, type PluginAdmissionTarget, type PluginAdmissionBinding, type PluginAdmissionPrincipal, type PluginAdmissionPlan, type PluginAdmissionIdentity, type PluginBundleIdentity, type PluginRoutingIdentity, type PluginAdmissionReceipt } from "./lib/plugin-admission.js";
package/dist/index.js CHANGED
@@ -11833,7 +11833,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
11833
11833
  // package.json
11834
11834
  var package_default = {
11835
11835
  name: "@hasna/skills",
11836
- version: "0.10.55",
11836
+ version: "0.10.57",
11837
11837
  description: "Skills library for AI coding agents",
11838
11838
  type: "module",
11839
11839
  bin: {
@@ -20693,6 +20693,97 @@ function read2(path, changes) {
20693
20693
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
20694
20694
  return readFileSync20(path, "utf8");
20695
20695
  }
20696
+ function assertReviewedClaudeCacheClosures(home, config, review) {
20697
+ const enabledIds = Object.entries(config.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id]) => id);
20698
+ if (enabledIds.length === 0)
20699
+ return;
20700
+ const registryPath = join32(home, ".claude/plugins/installed_plugins.json");
20701
+ const cacheRoot = join32(home, ".claude/plugins/cache");
20702
+ const registryText = read2(registryPath);
20703
+ if (registryText === null)
20704
+ return;
20705
+ const registry = parseConfig(registryText, registryPath);
20706
+ if (registry.version !== 2 || !registry.plugins || typeof registry.plugins !== "object" || Array.isArray(registry.plugins))
20707
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
20708
+ const cachePath = (path) => typeof path === "string" && isAbsolute21(path) && resolve20(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep8));
20709
+ const activeRoots = new Set;
20710
+ for (const id of enabledIds) {
20711
+ const rows = registry.plugins[id];
20712
+ if (rows === undefined)
20713
+ continue;
20714
+ if (!Array.isArray(rows))
20715
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
20716
+ for (const row of rows) {
20717
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
20718
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
20719
+ }
20720
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep8))) {
20721
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename11(row.installPath) !== row.version)) {
20722
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
20723
+ }
20724
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute21(row.projectPath) && resolve20(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
20725
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
20726
+ activeRoots.add(row.installPath);
20727
+ }
20728
+ }
20729
+ }
20730
+ if (activeRoots.size === 0)
20731
+ return;
20732
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
20733
+ if (!registrySource)
20734
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
20735
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
20736
+ const requireSource = (path, description, manifest = false) => {
20737
+ if (!reviewedFile(path, manifest))
20738
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
20739
+ };
20740
+ for (const root2 of activeRoots) {
20741
+ safe4(root2);
20742
+ const rootStat = lstatSync25(root2, { throwIfNoEntry: false });
20743
+ if (!rootStat?.isDirectory() || realpathSync11(root2) !== root2)
20744
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
20745
+ const manifestPath = join32(root2, ".claude-plugin/plugin.json");
20746
+ const manifestStat = lstatSync25(manifestPath, { throwIfNoEntry: false });
20747
+ let manifest;
20748
+ if (manifestStat) {
20749
+ if (!manifestStat.isFile())
20750
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
20751
+ requireSource(manifestPath, "manifest", true);
20752
+ const raw = read2(manifestPath);
20753
+ if (raw === null)
20754
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
20755
+ manifest = JSON.parse(raw);
20756
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
20757
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
20758
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
20759
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
20760
+ }
20761
+ const roots = new Set([join32(root2, "skills")]);
20762
+ if (lstatSync25(join32(root2, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
20763
+ roots.add(root2);
20764
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
20765
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
20766
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
20767
+ for (const value of declared) {
20768
+ const path = resolve20(root2, value);
20769
+ if (path !== root2 && !path.startsWith(root2 + sep8))
20770
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
20771
+ roots.add(path.endsWith(`${sep8}SKILL.md`) ? dirname20(path) : path);
20772
+ }
20773
+ for (const skillRoot of roots)
20774
+ if (!review.roots.includes(skillRoot))
20775
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
20776
+ const hooksPath = join32(root2, "hooks/hooks.json");
20777
+ const hooksStat = lstatSync25(hooksPath, { throwIfNoEntry: false });
20778
+ if (hooksStat) {
20779
+ if (!hooksStat.isFile())
20780
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
20781
+ requireSource(hooksPath, "hook manifest");
20782
+ }
20783
+ if (typeof manifest?.hooks === "string")
20784
+ requireSource(resolve20(root2, manifest.hooks), "manifest-declared hook source");
20785
+ }
20786
+ }
20696
20787
  function projectNativeDiscoveryFields(text2, format, fields2, path) {
20697
20788
  const object6 = format === "yaml" ? parseHermesConfig(text2) : parseConfig(text2, path, format === "toml");
20698
20789
  if (!object6 || typeof object6 !== "object" || Array.isArray(object6))
@@ -21281,6 +21372,8 @@ function resolveAgentDiscovery(options) {
21281
21372
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
21282
21373
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
21283
21374
  });
21375
+ if (agent === "claude")
21376
+ assertReviewedClaudeCacheClosures(home, config, review);
21284
21377
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
21285
21378
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual7(candidate2, source)) === index);
21286
21379
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
@@ -48250,6 +48343,89 @@ async function reconcileSkillSession(input2, options = {}) {
48250
48343
  const result = replaceSkillSession(skillSessionSnapshotBinding(snapshot2), replacement, { ...plan, planDigest }, options, () => assertPlanWindow(issuedAt, expiresAt, (options.now ?? Date.now)()));
48251
48344
  return { applied: true, plan, planDigest, ...result };
48252
48345
  }
48346
+ // src/lib/claude-prospective-review.ts
48347
+ import { createHash as createHash35 } from "crypto";
48348
+ import { lstatSync as lstatSync28, readFileSync as readFileSync30, readdirSync as readdirSync17, realpathSync as realpathSync15 } from "fs";
48349
+ import { basename as basename14, dirname as dirname30, isAbsolute as isAbsolute26, join as join44, relative as relative12, resolve as resolve27, sep as sep15 } from "path";
48350
+ var MAX_CANDIDATE_FILES = 16384;
48351
+ var MAX_CANDIDATE_BYTES = 256 * 1024 * 1024;
48352
+ var sha6 = (data) => createHash35("sha256").update(data).digest("hex");
48353
+ function refuse4(reason2) {
48354
+ throw new Error(`CLAUDE_CANDIDATE_REVIEW_REFUSED: ${reason2}`);
48355
+ }
48356
+ var GENERATED_MARKETPLACE_TIME = Object.freeze({ $generated: "claude-utc-timestamp", field: "known-marketplaces.lastUpdated" });
48357
+ var GENERATED_FRESH_PLUGIN_INSTALLED_AT = Object.freeze({ $generated: "claude-utc-timestamp", field: "installed_plugins.fresh-row.installedAt" });
48358
+ var GENERATED_PLUGIN_UPDATE_TIME = Object.freeze({ $generated: "claude-utc-timestamp", field: "installed_plugins.selected-row.lastUpdated" });
48359
+ function abs(path, name) {
48360
+ if (typeof path !== "string" || !path || path.length > 4096 || /[\x00-\x1f\x7f]/.test(path) || !isAbsolute26(path) || resolve27(path) !== path)
48361
+ refuse4(`${name} must be a normalized absolute path`);
48362
+ }
48363
+ function safeFile(path, name) {
48364
+ abs(path, name);
48365
+ for (let at = dirname30(path);; at = dirname30(at)) {
48366
+ const stat3 = lstatSync28(at, { throwIfNoEntry: false });
48367
+ if (!stat3?.isDirectory() || stat3.isSymbolicLink())
48368
+ refuse4(`${name} traverses a symlink or non-directory`);
48369
+ if (at === dirname30(at))
48370
+ break;
48371
+ }
48372
+ const stat2 = lstatSync28(path, { throwIfNoEntry: false });
48373
+ if (!stat2?.isFile() || stat2.isSymbolicLink() || stat2.size > MAX_CANDIDATE_BYTES)
48374
+ refuse4(`${name} is not a bounded regular file`);
48375
+ return readFileSync30(path);
48376
+ }
48377
+ function canonicalDir(path, name) {
48378
+ abs(path, name);
48379
+ if (realpathSync15(path) !== path || !lstatSync28(path).isDirectory())
48380
+ refuse4(`${name} is not a canonical directory`);
48381
+ for (let at = path;; at = dirname30(at)) {
48382
+ const stat2 = lstatSync28(at);
48383
+ if (!stat2.isDirectory() || stat2.isSymbolicLink())
48384
+ refuse4(`${name} traverses a symlink`);
48385
+ if (at === dirname30(at))
48386
+ break;
48387
+ }
48388
+ }
48389
+ function captureClaudeProspectiveCandidateClosure(root2) {
48390
+ canonicalDir(root2, "candidate root");
48391
+ const entries = [];
48392
+ let bytes = 0;
48393
+ const walk2 = (directory2) => {
48394
+ const beforeDirectory = lstatSync28(directory2);
48395
+ const names = readdirSync17(directory2).sort();
48396
+ if (new Set(names).size !== names.length)
48397
+ refuse4("candidate contains ambiguous directory entries");
48398
+ for (const name of names) {
48399
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\") || /[\x00-\x1f\x7f]/.test(name))
48400
+ refuse4("candidate contains an unsupported path member");
48401
+ const path = join44(directory2, name), rel = relative12(root2, path).split(sep15).join("/");
48402
+ if (entries.length >= MAX_CANDIDATE_FILES)
48403
+ refuse4("candidate closure exceeds its member limit");
48404
+ const stat2 = lstatSync28(path);
48405
+ if (stat2.isSymbolicLink())
48406
+ refuse4("candidate closure contains a symlink");
48407
+ if (stat2.isDirectory()) {
48408
+ entries.push([rel, "directory", null]);
48409
+ walk2(path);
48410
+ } else if (stat2.isFile()) {
48411
+ bytes += stat2.size;
48412
+ if (bytes > MAX_CANDIDATE_BYTES)
48413
+ refuse4("candidate closure exceeds its byte limit");
48414
+ const content = safeFile(path, "candidate member"), after = lstatSync28(path);
48415
+ if (!after.isFile() || after.dev !== stat2.dev || after.ino !== stat2.ino || after.size !== stat2.size || after.mtimeMs !== stat2.mtimeMs || after.ctimeMs !== stat2.ctimeMs)
48416
+ refuse4("candidate member changed while being read");
48417
+ entries.push([rel, "file", sha6(content)]);
48418
+ } else
48419
+ refuse4("candidate closure contains a special file");
48420
+ }
48421
+ const afterDirectory = lstatSync28(directory2);
48422
+ if (!afterDirectory.isDirectory() || afterDirectory.dev !== beforeDirectory.dev || afterDirectory.ino !== beforeDirectory.ino || afterDirectory.mtimeMs !== beforeDirectory.mtimeMs || afterDirectory.ctimeMs !== beforeDirectory.ctimeMs)
48423
+ refuse4("candidate directory changed while being read");
48424
+ };
48425
+ walk2(root2);
48426
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
48427
+ return sha6(JSON.stringify(entries));
48428
+ }
48253
48429
  export {
48254
48430
  writeStationSnapshot,
48255
48431
  writeStationHydration,
@@ -48390,6 +48566,7 @@ export {
48390
48566
  inspectPrivatePublication,
48391
48567
  importSkillsLocalSnapshot,
48392
48568
  homePathFor,
48569
+ hashClaudePluginManifest,
48393
48570
  hashClaudeMarketplaceEntry,
48394
48571
  getToolPrimitive,
48395
48572
  getStorageStatus,
@@ -48473,6 +48650,7 @@ export {
48473
48650
  captureClaudeSettingsV3,
48474
48651
  captureClaudeSettingsV2,
48475
48652
  captureClaudeSettings,
48653
+ captureClaudeProspectiveCandidateClosure,
48476
48654
  captureClaudeMarketplaceRegistryV2,
48477
48655
  captureClaudeMarketplaceRegistry,
48478
48656
  captureClaudeMarketplaceEntry,
@@ -99,6 +99,8 @@ export interface AgentIntegrationPlan {
99
99
  }
100
100
  /** Normalize only the verified OS prefix, never hide user-controlled links. */
101
101
  export declare function canonicalSystemPath(path: string): string;
102
+ export declare function canonicalAgentPath(path: string, aliases: AgentRootAlias[]): string;
103
+ export declare function recheckRootAliases(aliases: AgentRootAlias[]): void;
102
104
  /** Parse the reviewed exact-target binding used by native migration. Paths are
103
105
  * deliberately project-relative so a manifest cannot silently select another
104
106
  * checkout when a project root changes. */
@@ -0,0 +1,140 @@
1
+ import { type ReviewedDiscoveryInputs } from "./agent-discovery.js";
2
+ export declare const CLAUDE_PROSPECTIVE_REVIEW_SCHEMA: "skills.claude-plugin-prospective-review/v1";
3
+ export interface ClaudeProspectiveReviewRequest {
4
+ schema: typeof CLAUDE_PROSPECTIVE_REVIEW_SCHEMA;
5
+ scope: {
6
+ kind: "user" | "project";
7
+ projectPath: string;
8
+ };
9
+ native: {
10
+ home: string;
11
+ executable: {
12
+ path: string;
13
+ target: string;
14
+ version: string;
15
+ sha256: string;
16
+ };
17
+ configRoot: string;
18
+ pluginRoot: string;
19
+ };
20
+ skills: {
21
+ dataDir: string;
22
+ policySha256: string;
23
+ discoverySha256: string;
24
+ discoveryInputs: ReviewedDiscoveryInputs;
25
+ };
26
+ candidate: {
27
+ marketplace: string;
28
+ root: string;
29
+ catalogPath: string;
30
+ catalogSha256: string;
31
+ closureSha256: string;
32
+ plugins: Array<{
33
+ plugin: string;
34
+ manifestPath: string;
35
+ manifestSha256: string;
36
+ optionsPatch: Record<string, unknown>;
37
+ }>;
38
+ releaseManifest?: {
39
+ path: string;
40
+ sha256: string;
41
+ };
42
+ functionHooksEnv?: "1";
43
+ };
44
+ preimages: {
45
+ settings: {
46
+ path: string;
47
+ sha256: string | null;
48
+ };
49
+ userSettings: {
50
+ path: string;
51
+ sha256: string | null;
52
+ };
53
+ projectSettings: {
54
+ path: string;
55
+ sha256: string | null;
56
+ };
57
+ projectSettingsLocal: {
58
+ path: string;
59
+ sha256: string | null;
60
+ };
61
+ marketplaces: {
62
+ path: string;
63
+ sha256: string | null;
64
+ };
65
+ installedPlugins: {
66
+ path: string;
67
+ sha256: string | null;
68
+ };
69
+ };
70
+ /** Complete semantic documents expected after native adoption. The one
71
+ * timestamps generated by Claude for the owned marketplace and registration
72
+ * rows use exact typed field markers documented below; every other field is
73
+ * concrete. */
74
+ delta: {
75
+ settingsAfter: Record<string, unknown>;
76
+ knownMarketplacesAfter: Record<string, unknown>;
77
+ installedPluginsAfter: Record<string, unknown>;
78
+ };
79
+ review: {
80
+ pluginHooks: "reviewed-no-skill-injection";
81
+ };
82
+ }
83
+ export interface ClaudeProspectiveReviewReceipt {
84
+ schema: typeof CLAUDE_PROSPECTIVE_REVIEW_SCHEMA;
85
+ accepted: true;
86
+ applied: false;
87
+ scope: ClaudeProspectiveReviewRequest["scope"];
88
+ pluginIds: string[];
89
+ native: {
90
+ executablePath: string;
91
+ executableTarget: string;
92
+ executableVersion: string;
93
+ executableSha256: string;
94
+ configRoot: string;
95
+ pluginRoot: string;
96
+ };
97
+ candidate: {
98
+ marketplace: string;
99
+ plugins: Array<{
100
+ plugin: string;
101
+ pluginId: string;
102
+ manifestSha256: string;
103
+ optionsPatchSha256: string;
104
+ }>;
105
+ closureSha256: string;
106
+ catalogSha256: string;
107
+ releaseManifest?: {
108
+ path: string;
109
+ sha256: string;
110
+ };
111
+ };
112
+ targetSettingsPath: string;
113
+ skillsDiscoverySha256: string;
114
+ requestFileSha256: string;
115
+ semanticDocumentsSha256: string;
116
+ operationSha256: string;
117
+ expectedPostStateSha256: string;
118
+ preimages: Array<{
119
+ name: string;
120
+ path: string;
121
+ sha256: string | null;
122
+ }>;
123
+ ownedDelta: {
124
+ settingsEnabledPlugins: string[];
125
+ selectedPluginOptions: string[];
126
+ functionHooksEnv: string | null;
127
+ knownMarketplace: string;
128
+ installedPlugins: string[];
129
+ unrelatedGraph: "preserved-by-complete-semantic-documents";
130
+ };
131
+ identityPrecondition: "Harnesses must resolve the installed Claude home/config/plugin roots and executable path/version/hash plus the installed Skills data directory through their package-owned consumers, then compare every value before trusting this receipt; this validator checks supplied paths and current bytes only and does not establish actual consumer identity";
132
+ reviewPrecondition: "The caller's no-skill-injection assessment is bound to the raw request hash; structural checks are not semantic hook review, and Harnesses must retain its existing review record and verify the exact candidate binding";
133
+ limitation: "validation-only; native installer atomicity and crash recovery are not established";
134
+ }
135
+ /** Fingerprint every file byte and directory name under the frozen candidate.
136
+ * Symlinks and special files refuse; the digest binds the complete closure. */
137
+ export declare function captureClaudeProspectiveCandidateClosure(root: string): string;
138
+ /** Validate a frozen candidate review against the current filesystem. This is
139
+ * intentionally a planner only: its receipt is not native-install authority. */
140
+ export declare function reviewClaudeProspectiveCandidate(request: ClaudeProspectiveReviewRequest, requestFileSha256: string): ClaudeProspectiveReviewReceipt;
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.55",
32376
+ version: "0.10.57",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -43533,6 +43533,97 @@ function read2(path, changes) {
43533
43533
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
43534
43534
  return readFileSync15(path, "utf8");
43535
43535
  }
43536
+ function assertReviewedClaudeCacheClosures(home, config, review) {
43537
+ const enabledIds = Object.entries(config.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id2]) => id2);
43538
+ if (enabledIds.length === 0)
43539
+ return;
43540
+ const registryPath = join26(home, ".claude/plugins/installed_plugins.json");
43541
+ const cacheRoot = join26(home, ".claude/plugins/cache");
43542
+ const registryText = read2(registryPath);
43543
+ if (registryText === null)
43544
+ return;
43545
+ const registry = parseConfig(registryText, registryPath);
43546
+ if (registry.version !== 2 || !registry.plugins || typeof registry.plugins !== "object" || Array.isArray(registry.plugins))
43547
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
43548
+ const cachePath = (path) => typeof path === "string" && isAbsolute20(path) && resolve19(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep6));
43549
+ const activeRoots = new Set;
43550
+ for (const id2 of enabledIds) {
43551
+ const rows = registry.plugins[id2];
43552
+ if (rows === undefined)
43553
+ continue;
43554
+ if (!Array.isArray(rows))
43555
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
43556
+ for (const row of rows) {
43557
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
43558
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
43559
+ }
43560
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep6))) {
43561
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename9(row.installPath) !== row.version)) {
43562
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
43563
+ }
43564
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute20(row.projectPath) && resolve19(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
43565
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
43566
+ activeRoots.add(row.installPath);
43567
+ }
43568
+ }
43569
+ }
43570
+ if (activeRoots.size === 0)
43571
+ return;
43572
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
43573
+ if (!registrySource)
43574
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
43575
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
43576
+ const requireSource = (path, description, manifest = false) => {
43577
+ if (!reviewedFile(path, manifest))
43578
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
43579
+ };
43580
+ for (const root2 of activeRoots) {
43581
+ safe4(root2);
43582
+ const rootStat = lstatSync21(root2, { throwIfNoEntry: false });
43583
+ if (!rootStat?.isDirectory() || realpathSync9(root2) !== root2)
43584
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
43585
+ const manifestPath = join26(root2, ".claude-plugin/plugin.json");
43586
+ const manifestStat = lstatSync21(manifestPath, { throwIfNoEntry: false });
43587
+ let manifest;
43588
+ if (manifestStat) {
43589
+ if (!manifestStat.isFile())
43590
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
43591
+ requireSource(manifestPath, "manifest", true);
43592
+ const raw = read2(manifestPath);
43593
+ if (raw === null)
43594
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
43595
+ manifest = JSON.parse(raw);
43596
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
43597
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
43598
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
43599
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
43600
+ }
43601
+ const roots = new Set([join26(root2, "skills")]);
43602
+ if (lstatSync21(join26(root2, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
43603
+ roots.add(root2);
43604
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
43605
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
43606
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
43607
+ for (const value of declared) {
43608
+ const path = resolve19(root2, value);
43609
+ if (path !== root2 && !path.startsWith(root2 + sep6))
43610
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
43611
+ roots.add(path.endsWith(`${sep6}SKILL.md`) ? dirname17(path) : path);
43612
+ }
43613
+ for (const skillRoot of roots)
43614
+ if (!review.roots.includes(skillRoot))
43615
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
43616
+ const hooksPath = join26(root2, "hooks/hooks.json");
43617
+ const hooksStat = lstatSync21(hooksPath, { throwIfNoEntry: false });
43618
+ if (hooksStat) {
43619
+ if (!hooksStat.isFile())
43620
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
43621
+ requireSource(hooksPath, "hook manifest");
43622
+ }
43623
+ if (typeof manifest?.hooks === "string")
43624
+ requireSource(resolve19(root2, manifest.hooks), "manifest-declared hook source");
43625
+ }
43626
+ }
43536
43627
  function projectNativeDiscoveryFields(text2, format, fields2, path) {
43537
43628
  const object6 = format === "yaml" ? parseHermesConfig(text2) : parseConfig(text2, path, format === "toml");
43538
43629
  if (!object6 || typeof object6 !== "object" || Array.isArray(object6))
@@ -44121,6 +44212,8 @@ function resolveAgentDiscovery(options) {
44121
44212
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
44122
44213
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
44123
44214
  });
44215
+ if (agent === "claude")
44216
+ assertReviewedClaudeCacheClosures(home, config, review);
44124
44217
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
44125
44218
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual6(candidate2, source)) === index);
44126
44219
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };