@hasna/skills 0.9.12 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.9.12",
32376
+ version: "0.9.13",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -37545,10 +37545,10 @@ function assertAgentPolicyCollections(policy) {
37545
37545
  text(source.path);
37546
37546
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
37547
37547
  if (source.hashMode !== undefined)
37548
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37549
- if (source.hashMode === "claude-settings-v1")
37548
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37549
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
37550
37550
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "settings.json");
37551
- if (source.hashMode === "claude-marketplace-registry")
37551
+ if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
37552
37552
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
37553
37553
  if (source.hashMode === "claude-plugin-registry") {
37554
37554
  requireBound(agent === "claude");
@@ -39895,9 +39895,13 @@ function textField(value) {
39895
39895
  need2(value?.kind === "string" && value.value.length > 0 && !/[\x00-\x1f\x7f]/.test(value.value), "contains an invalid recognized field");
39896
39896
  return value.value;
39897
39897
  }
39898
- function normalizeRow(row) {
39899
- if (!exactKeys2(row, ["source", "installLocation", "lastUpdated"]))
39900
- return;
39898
+ function normalizeRow(row, version = 1) {
39899
+ if (!exactKeys2(row, ["source", "installLocation", "lastUpdated"])) {
39900
+ if (version !== 2 || !exactKeys2(row, ["source", "installLocation", "lastUpdated", "autoUpdate"]))
39901
+ return;
39902
+ const autoUpdate = field(row, "autoUpdate");
39903
+ need2(autoUpdate?.kind === "literal" && (autoUpdate.value === "true" || autoUpdate.value === "false"), "contains an invalid automatic-update preference");
39904
+ }
39901
39905
  const source = field(row, "source");
39902
39906
  if (source?.kind !== "object")
39903
39907
  return;
@@ -39924,6 +39928,13 @@ function captureClaudeMarketplaceRegistry(path, budget = { remaining: 256 * 1024
39924
39928
  const sha2562 = createHash11("sha256").update("hasna.skills.claude-marketplace-registry.v1\x00").update(JSON.stringify(value)).digest("hex");
39925
39929
  return { path, hashMode: "claude-marketplace-registry", sha256: sha2562 };
39926
39930
  }
39931
+ function captureClaudeMarketplaceRegistryV2(path, budget = { remaining: 256 * 1024 * 1024 }) {
39932
+ const value = parse5(readRegistry2(path, budget));
39933
+ for (const [, row] of value.entries)
39934
+ normalizeRow(row, 2);
39935
+ const sha2562 = createHash11("sha256").update("hasna.skills.claude-marketplace-registry.v2\x00").update(JSON.stringify(value)).digest("hex");
39936
+ return { path, hashMode: "claude-marketplace-registry-v2", sha256: sha2562 };
39937
+ }
39927
39938
 
39928
39939
  // src/lib/claude-settings-witness.ts
39929
39940
  import { createHash as createHash12 } from "crypto";
@@ -40136,9 +40147,33 @@ function assertClaudeStopHookReplacement(before, replacement) {
40136
40147
  }
40137
40148
  need3(JSON.stringify(withoutStop(original)) === JSON.stringify(withoutStop(next)), "Stop update changed settings outside hooks.Stop");
40138
40149
  }
40139
- function settingsDigest(text2) {
40150
+ var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40151
+ function inferencePreference(key, child) {
40152
+ if (key === "skipDangerousModePermissionPrompt") {
40153
+ need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
40154
+ return true;
40155
+ }
40156
+ if (key === "effortLevel") {
40157
+ need3(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
40158
+ return true;
40159
+ }
40160
+ if (key !== "modelSettings")
40161
+ return false;
40162
+ need3(child.kind === "object", "requires an object of model preferences");
40163
+ child.entries = child.entries.filter(([model, preferences]) => {
40164
+ if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
40165
+ return true;
40166
+ const effort = preferences.entries[0][1];
40167
+ need3(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
40168
+ return false;
40169
+ });
40170
+ return child.entries.length === 0;
40171
+ }
40172
+ function settingsDigest(text2, version = 1) {
40140
40173
  const value = parse6(text2);
40141
40174
  value.entries = value.entries.filter(([key, child]) => {
40175
+ if (version === 2 && inferencePreference(key, child))
40176
+ return false;
40142
40177
  if (key === "model") {
40143
40178
  need3(child.kind === "string", "contains an invalid model selection");
40144
40179
  return !BUILTIN_MODEL_SELECTIONS.has(child.value);
@@ -40153,7 +40188,7 @@ function settingsDigest(text2) {
40153
40188
  }
40154
40189
  return true;
40155
40190
  });
40156
- return createHash12("sha256").update("hasna.skills.claude-settings.v1\x00").update(JSON.stringify(canonical3(value))).digest("hex");
40191
+ return createHash12("sha256").update(`hasna.skills.claude-settings.v${version}\x00`).update(JSON.stringify(canonical3(value))).digest("hex");
40157
40192
  }
40158
40193
  function hashClaudeSettingsReplacement(text2, budget) {
40159
40194
  need3(typeof text2 === "string", "requires settings text");
@@ -40165,6 +40200,16 @@ function hashClaudeSettingsReplacement(text2, budget) {
40165
40200
  function captureClaudeSettings(path, budget = { remaining: 256 * 1024 * 1024 }) {
40166
40201
  return { path, hashMode: "claude-settings-v1", sha256: settingsDigest(readSettings(path, budget)) };
40167
40202
  }
40203
+ function captureClaudeSettingsV2(path, budget = { remaining: 256 * 1024 * 1024 }) {
40204
+ return { path, hashMode: "claude-settings-v2", sha256: settingsDigest(readSettings(path, budget), 2) };
40205
+ }
40206
+ function hashClaudeSettingsReplacementV2(text2, budget) {
40207
+ need3(typeof text2 === "string", "requires settings text");
40208
+ const bytes = Buffer.byteLength(text2);
40209
+ need3(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
40210
+ budget.remaining -= bytes;
40211
+ return settingsDigest(text2, 2);
40212
+ }
40168
40213
 
40169
40214
  // src/lib/codex-hook-discovery-recovery.ts
40170
40215
  import { createHash as createHash14 } from "crypto";
@@ -40268,18 +40313,18 @@ function read2(path, changes) {
40268
40313
  return readFileSync12(path, "utf8");
40269
40314
  }
40270
40315
  function projected(source, changes, budget = discoveryByteBudget()) {
40271
- if (source.hashMode === "claude-settings-v1") {
40316
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
40272
40317
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
40273
40318
  throw new Error("Claude settings witnesses require exact typed metadata");
40274
- const current = captureClaudeSettings(source.path, budget).sha256;
40319
+ const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
40275
40320
  if (current !== source.sha256)
40276
40321
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40277
- return changes?.has(source.path) ? hashClaudeSettingsReplacement(changes.get(source.path), budget) : current;
40322
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
40278
40323
  }
40279
- if (source.hashMode === "claude-marketplace-registry") {
40324
+ if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
40280
40325
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
40281
40326
  throw new Error("Claude marketplace witnesses require an exact reviewed registry without synthetic changes");
40282
- const sha2562 = captureClaudeMarketplaceRegistry(source.path, budget).sha256;
40327
+ const sha2562 = (source.hashMode === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : captureClaudeMarketplaceRegistry)(source.path, budget).sha256;
40283
40328
  if (sha2562 !== source.sha256)
40284
40329
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40285
40330
  return sha2562;
@@ -40336,9 +40381,9 @@ function verifyAgentDiscovery(binding, codexRecovery) {
40336
40381
  safe4(root2);
40337
40382
  }
40338
40383
  function assertMarketplaceBinding(binding, source) {
40339
- if (source.hashMode === "claude-settings-v1" && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40384
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40340
40385
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
40341
- if (source.hashMode === "claude-marketplace-registry" && (binding.agent !== "claude" || binding.method !== "reviewed"))
40386
+ if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
40342
40387
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
40343
40388
  }
40344
40389
  function rebindAgentDiscovery(binding, changes) {
@@ -40479,7 +40524,7 @@ function resolveAgentDiscovery(options) {
40479
40524
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
40480
40525
  throw new Error("Explicit discovery reviews require full source-file hashes");
40481
40526
  verifyAgentDiscovery(supplied);
40482
- if (review.sources.some((source) => source.hashMode === "claude-settings-v1" && source.path !== canonical4(configPath)))
40527
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
40483
40528
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
40484
40529
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
40485
40530
  throw new Error("Discovery review must include the agent configuration source");
@@ -55561,6 +55606,18 @@ function rowToSkill(row) {
55561
55606
  ...row.replacement_slug ? { replacementSlug: String(row.replacement_slug) } : {}
55562
55607
  };
55563
55608
  }
55609
+ function rowToPublishedSelectionState(row) {
55610
+ return {
55611
+ slug: String(row.slug),
55612
+ version: String(row.version),
55613
+ current: row.skill_exists ? {
55614
+ lifecycle: row.lifecycle === "archived" ? "archived" : "active",
55615
+ ...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {}
55616
+ } : null,
55617
+ versionBundleSha256: typeof row.bundle_sha256 === "string" ? row.bundle_sha256 : null,
55618
+ bundleAvailable: Boolean(row.bundle_available)
55619
+ };
55620
+ }
55564
55621
  function rowToPin(row) {
55565
55622
  return {
55566
55623
  orgId: String(row.org_id),
@@ -56335,6 +56392,22 @@ class SqliteSkillsStore {
56335
56392
  const row = this.get("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [principal.orgId, slug, version2]);
56336
56393
  return row ? rowToSkillVersion(row) : null;
56337
56394
  }
56395
+ async getPublishedSelectionStates(principal, selections2) {
56396
+ if (selections2.length > MAX_PROFILE_SELECTIONS)
56397
+ throw new RangeError("Published selection lookup exceeds the profile selection limit");
56398
+ if (!selections2.length)
56399
+ return [];
56400
+ const requested = JSON.stringify(selections2.map(({ slug, version: version2 }) => ({ slug, version: version2 })));
56401
+ return this.all(`SELECT json_extract(q.value, '$.slug') AS slug, json_extract(q.value, '$.version') AS version,
56402
+ r.slug IS NOT NULL AS skill_exists, r.lifecycle, r.tombstoned_at,
56403
+ v.bundle_sha256, b.sha256 IS NOT NULL AS bundle_available
56404
+ FROM json_each(?) q
56405
+ LEFT JOIN skills_registry r ON r.org_id = ? AND r.slug = json_extract(q.value, '$.slug')
56406
+ LEFT JOIN skills_versions v ON v.org_id = ?
56407
+ AND v.slug = json_extract(q.value, '$.slug') AND v.version = json_extract(q.value, '$.version')
56408
+ LEFT JOIN skills_bundles b ON b.org_id = ? AND b.sha256 = v.bundle_sha256
56409
+ ORDER BY CAST(q.key AS INTEGER)`, [requested, principal.orgId, principal.orgId, principal.orgId]).map(rowToPublishedSelectionState);
56410
+ }
56338
56411
  async pinSkill(principal, slug, metadata = {}) {
56339
56412
  const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
56340
56413
  VALUES (?, ?, ?, ?, ?)
@@ -57463,16 +57536,21 @@ function selected(value) {
57463
57536
  return selections2;
57464
57537
  }
57465
57538
  async function validatePublished(store, principal, selections2) {
57466
- for (const selection2 of selections2) {
57467
- const current = await store.getSkill(principal, selection2.slug);
57539
+ const states = await store.getPublishedSelectionStates(principal, selections2);
57540
+ if (states.length !== selections2.length)
57541
+ throw new SkillRequestError(503, "SELECTION_UNAVAILABLE", "Selected publication state is unavailable");
57542
+ for (const [index, selection2] of selections2.entries()) {
57543
+ const state2 = states[index];
57544
+ if (!state2 || state2.slug !== selection2.slug || state2.version !== selection2.version)
57545
+ throw new SkillRequestError(503, "SELECTION_UNAVAILABLE", "Selected publication state is unavailable");
57546
+ const current = state2.current;
57468
57547
  if (!current || current.tombstonedAt)
57469
57548
  throw new SkillRequestError(current?.tombstonedAt ? 410 : 404, "SELECTION_UNAVAILABLE", `Selected skill '${selection2.slug}' is unavailable`);
57470
57549
  if (current.lifecycle === "archived")
57471
57550
  throw new SkillRequestError(409, "SELECTION_ARCHIVED", `Selected skill '${selection2.slug}' is archived`);
57472
- const version2 = await store.getSkillVersion(principal, selection2.slug, selection2.version);
57473
- if (!version2 || `sha256:${version2.bundleSha256}` !== selection2.bundleDigest)
57551
+ if (!state2.versionBundleSha256 || `sha256:${state2.versionBundleSha256}` !== selection2.bundleDigest)
57474
57552
  throw new SkillRequestError(409, "SELECTION_VERSION_MISMATCH", `Selected version of '${selection2.slug}' does not match its published digest`);
57475
- if (!await store.getSkillBundle(principal, version2.bundleSha256))
57553
+ if (!state2.bundleAvailable)
57476
57554
  throw new SkillRequestError(503, "SELECTION_BUNDLE_UNAVAILABLE", "Selected bundle is unavailable");
57477
57555
  }
57478
57556
  }
@@ -63361,6 +63439,25 @@ class MemorySkillsStore {
63361
63439
  const found = this.versions.get(versionKey(principal.orgId, slug, version2));
63362
63440
  return found && found.orgId === principal.orgId ? found : null;
63363
63441
  }
63442
+ async getPublishedSelectionStates(principal, selections2) {
63443
+ if (selections2.length > MAX_PROFILE_SELECTIONS)
63444
+ throw new RangeError("Published selection lookup exceeds the profile selection limit");
63445
+ return selections2.map(({ slug, version: version2 }) => {
63446
+ const current = this.skills.get(skillKey(principal.orgId, slug));
63447
+ const selectedVersion = this.versions.get(versionKey(principal.orgId, slug, version2));
63448
+ const bundle2 = selectedVersion && this.bundles.get(skillKey(principal.orgId, selectedVersion.bundleSha256));
63449
+ return {
63450
+ slug,
63451
+ version: version2,
63452
+ current: current?.orgId === principal.orgId ? {
63453
+ lifecycle: current.lifecycle,
63454
+ ...current.tombstonedAt ? { tombstonedAt: current.tombstonedAt } : {}
63455
+ } : null,
63456
+ versionBundleSha256: selectedVersion?.orgId === principal.orgId ? selectedVersion.bundleSha256 : null,
63457
+ bundleAvailable: bundle2?.orgId === principal.orgId
63458
+ };
63459
+ });
63460
+ }
63364
63461
  async listSkills(principal) {
63365
63462
  return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a4, b4) => a4.slug.localeCompare(b4.slug));
63366
63463
  }
@@ -64169,6 +64266,25 @@ class PostgresSkillsStore {
64169
64266
  `;
64170
64267
  return rows[0] ? rowToSkillVersion(rows[0]) : null;
64171
64268
  }
64269
+ async getPublishedSelectionStates(principal, selections2) {
64270
+ if (selections2.length > MAX_PROFILE_SELECTIONS)
64271
+ throw new RangeError("Published selection lookup exceeds the profile selection limit");
64272
+ if (!selections2.length)
64273
+ return [];
64274
+ const requested = JSON.stringify(selections2.map(({ slug, version: version2 }) => ({ slug, version: version2 })));
64275
+ const rows = await this.sql`
64276
+ SELECT q.value->>'slug' AS slug, q.value->>'version' AS version,
64277
+ r.slug IS NOT NULL AS skill_exists, r.lifecycle, r.tombstoned_at,
64278
+ v.bundle_sha256, b.sha256 IS NOT NULL AS bundle_available
64279
+ FROM jsonb_array_elements(${requested}::text::jsonb) WITH ORDINALITY AS q(value, ordinal)
64280
+ LEFT JOIN skills_registry r ON r.org_id = ${principal.orgId} AND r.slug = q.value->>'slug'
64281
+ LEFT JOIN skills_versions v ON v.org_id = ${principal.orgId}
64282
+ AND v.slug = q.value->>'slug' AND v.version = q.value->>'version'
64283
+ LEFT JOIN skills_bundles b ON b.org_id = ${principal.orgId} AND b.sha256 = v.bundle_sha256
64284
+ ORDER BY q.ordinal
64285
+ `;
64286
+ return rows.map(rowToPublishedSelectionState);
64287
+ }
64172
64288
  async pinSkill(principal, slug, metadata = {}) {
64173
64289
  const rows = await this.sql`
64174
64290
  INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
@@ -1,4 +1,4 @@
1
- import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord } from "./types.js";
1
+ import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, PublishedSkillSelectionState } from "./types.js";
2
2
  export declare function nowIso(): string;
3
3
  /**
4
4
  * Coerce a caller-supplied row limit to a non-negative integer.
@@ -17,6 +17,7 @@ export declare function rowToRun(row: Record<string, unknown>): ServerRunRecord;
17
17
  export declare function rowToLog(row: Record<string, unknown>): ServerRunLog;
18
18
  export declare function rowToArtifact(row: Record<string, unknown>): ServerArtifact;
19
19
  export declare function rowToSkill(row: Record<string, unknown>): ServerSkillRecord;
20
+ export declare function rowToPublishedSelectionState(row: Record<string, unknown>): PublishedSkillSelectionState;
20
21
  export declare function rowToPin(row: Record<string, unknown>): ServerPin;
21
22
  export declare function rowToSkillBundle(row: Record<string, unknown>): ServerSkillBundle;
22
23
  export declare function parseJsonObject(value: unknown): Record<string, unknown>;
@@ -15,7 +15,7 @@ import { SqliteExecutionGrantStore } from "./execution-grant-store.js";
15
15
  */
16
16
  import { Database } from "bun:sqlite";
17
17
  import { SqliteSkillSelectionStore } from "./selection-store.js";
18
- import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
18
+ import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
19
19
  export interface SqliteStoreOptions {
20
20
  /** Apply pending migrations on open. Default true - it is what makes zero-config work. */
21
21
  migrate?: boolean;
@@ -113,6 +113,7 @@ export declare class SqliteSkillsStore implements SkillsProductStore {
113
113
  getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
114
114
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
115
115
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
116
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
116
117
  pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
117
118
  unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
118
119
  listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
@@ -1,6 +1,6 @@
1
1
  import { MemoryExecutionGrantStore, PostgresExecutionGrantStore } from "./execution-grant-store.js";
2
2
  import { MemorySkillSelectionStore, PostgresSkillSelectionStore } from "./selection-store.js";
3
- import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
3
+ import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
4
4
  import { type SkillLifecyclePatch } from "./types.js";
5
5
  import { type SqliteStoreOptions } from "./sqlite-store.js";
6
6
  export declare function createArtifactId(): string;
@@ -79,6 +79,7 @@ export declare class MemorySkillsStore implements SkillsProductStore {
79
79
  publishSkill(input: PublishSkillInput): Promise<ServerSkillRecord>;
80
80
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
81
81
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
82
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
82
83
  listSkills(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
83
84
  getSkill(principal: ApiPrincipal, slug: string): Promise<ServerSkillRecord | null>;
84
85
  setSkillLifecycle(principal: ApiPrincipal, slug: string, patch: SkillLifecyclePatch, expectedRevisionId?: string): Promise<ServerSkillRecord | null>;
@@ -180,6 +181,7 @@ export declare class PostgresSkillsStore implements SkillsProductStore {
180
181
  getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
181
182
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
182
183
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
184
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
183
185
  pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
184
186
  unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
185
187
  listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
@@ -279,6 +279,17 @@ export interface ServerSkillVersion {
279
279
  publishedByUserId?: string;
280
280
  createdAt: string;
281
281
  }
282
+ /** Exact version lookup, independent of the registry's mutable current version. */
283
+ export interface PublishedSkillSelection {
284
+ slug: string;
285
+ version: string;
286
+ }
287
+ /** Metadata only: bundle availability means its tenant-owned storage row exists. */
288
+ export interface PublishedSkillSelectionState extends PublishedSkillSelection {
289
+ current: Pick<ServerSkillRecord, "lifecycle" | "tombstonedAt"> | null;
290
+ versionBundleSha256: string | null;
291
+ bundleAvailable: boolean;
292
+ }
282
293
  export interface PublishSkillInput {
283
294
  /** Internal boot-seed precondition: never revive or replace non-bundled records. */
284
295
  seedBundledOnly?: true;
@@ -441,6 +452,13 @@ export interface SkillsProductStore {
441
452
  /** Every published version of a slug, newest first (hasna/apps#1630). */
442
453
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
443
454
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
455
+ /**
456
+ * Read at most MAX_PROFILE_SELECTIONS exact selections in one database snapshot,
457
+ * scoped to the principal's org at every join. Returns one row per input in input
458
+ * order, including missing selections and duplicates. Never reads bundle bytes or
459
+ * substitutes the latest version; callers retain lifecycle/digest error policy.
460
+ */
461
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
444
462
  pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
445
463
  /** False when this principal has no pin by that slug. */
446
464
  unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
@@ -286,3 +286,39 @@ cache history is bounded to 128 entries and an aggregate 256 MiB per witness.
286
286
  Primary runtime references: [command sources](https://code.claude.com/docs/en/plugin-marketplaces#command-sources),
287
287
  [plugin components](https://code.claude.com/docs/en/plugins-reference), and
288
288
  [exact command acceptance](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md#21271).
289
+
290
+ ### Explicit v2 preference reviews
291
+
292
+ `captureClaudeSettingsV2(path)` creates a `claude-settings-v2` witness. In
293
+ addition to the v1 display/model selections, it tolerates the documented
294
+ persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
295
+ level and in known built-in model entries whose only property is `effortLevel`.
296
+ Custom model entries and entries with any other property remain fully bound.
297
+ The boolean `skipDangerousModePermissionPrompt` is Claude's saved dialog
298
+ acknowledgment; it does not select a permission mode. Permission settings,
299
+ plugins, hooks, environment, ultracode and unknown configuration remain bound.
300
+ Invalid recognized preference types are refused.
301
+
302
+ `captureClaudeMarketplaceRegistryV2(path)` creates a
303
+ `claude-marketplace-registry-v2` witness. It also recognizes a registry row with
304
+ an optional boolean `autoUpdate`. That boolean remains bound; only a strictly
305
+ validated `lastUpdated` timestamp may vary. A new source or installation path,
306
+ a changed automatic-update policy, or an unknown field still requires review.
307
+
308
+ Capture either witness through the CLI:
309
+
310
+ ```sh
311
+ skills hook witness --kind claude-settings-v2 --path /canonical/home/.claude/settings.json --json
312
+ skills hook witness --kind claude-marketplace-registry-v2 --path /canonical/home/.claude/plugins/known_marketplaces.json --json
313
+ ```
314
+
315
+ Capture does not install or authorize a review. Compare the current complete
316
+ configuration with its previous review, put the chosen witness in the reviewed
317
+ discovery inputs, and preview `skills hook install --discovery-inputs <file>`
318
+ before applying. Existing byte and v1 witnesses are never silently converted.
319
+ Runtime support for the selected witness version must be installed before
320
+ policy activation. A real hook installation is a configuration change, not a
321
+ preference: its owning installer must coordinate a guarded settings/policy
322
+ update, or it needs a fresh explicit review.
323
+
324
+ The effort schema follows [Claude's model configuration documentation](https://code.claude.com/docs/en/model-config#adjust-effort-level).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {