@hasna/skills 0.9.13 → 0.9.15

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
@@ -31310,17 +31310,17 @@ class JsonEventsStore {
31310
31310
  }
31311
31311
  async appendEvent(event) {
31312
31312
  await this.init();
31313
- const events = await this.readJson(this.eventsPath, []);
31314
- events.push(event);
31315
- await this.writeJson(this.eventsPath, events);
31313
+ const events2 = await this.readJson(this.eventsPath, []);
31314
+ events2.push(event);
31315
+ await this.writeJson(this.eventsPath, events2);
31316
31316
  return event;
31317
31317
  }
31318
31318
  async appendEventOnce(event, options = {}) {
31319
31319
  await this.init();
31320
- const events = await this.readJson(this.eventsPath, []);
31320
+ const events2 = await this.readJson(this.eventsPath, []);
31321
31321
  const dedupe = options.dedupe !== false;
31322
31322
  if (dedupe) {
31323
- const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
31323
+ const existing = findEventByIdentity(events2, { id: event.id, dedupeKey: event.dedupeKey });
31324
31324
  if (existing) {
31325
31325
  return {
31326
31326
  event: existing,
@@ -31330,8 +31330,8 @@ class JsonEventsStore {
31330
31330
  };
31331
31331
  }
31332
31332
  }
31333
- events.push(event);
31334
- await this.writeJson(this.eventsPath, events);
31333
+ events2.push(event);
31334
+ await this.writeJson(this.eventsPath, events2);
31335
31335
  return {
31336
31336
  event,
31337
31337
  stored: true,
@@ -31341,13 +31341,13 @@ class JsonEventsStore {
31341
31341
  }
31342
31342
  async listEvents(options = {}) {
31343
31343
  await this.init();
31344
- const events = await this.readJson(this.eventsPath, []);
31345
- return queryEvents(events, options);
31344
+ const events2 = await this.readJson(this.eventsPath, []);
31345
+ return queryEvents(events2, options);
31346
31346
  }
31347
31347
  async listEventsPage(options = {}) {
31348
31348
  await this.init();
31349
- const events = await this.readJson(this.eventsPath, []);
31350
- const queried = queryEvents(events, {
31349
+ const events2 = await this.readJson(this.eventsPath, []);
31350
+ const queried = queryEvents(events2, {
31351
31351
  eventId: options.eventId,
31352
31352
  source: options.source,
31353
31353
  type: options.type
@@ -31365,8 +31365,8 @@ class JsonEventsStore {
31365
31365
  };
31366
31366
  }
31367
31367
  async findEventByIdentity(identity3) {
31368
- const events = await this.listEvents();
31369
- return findEventByIdentity(events, identity3);
31368
+ const events2 = await this.listEvents();
31369
+ return findEventByIdentity(events2, identity3);
31370
31370
  }
31371
31371
  async appendDelivery(result) {
31372
31372
  await this.init();
@@ -31471,8 +31471,8 @@ function normalizeEventPageLimit(limit) {
31471
31471
  throw new Error(`Event page limit must be a positive integer, got ${limit}`);
31472
31472
  return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
31473
31473
  }
31474
- function queryEvents(events, options) {
31475
- let rows = events;
31474
+ function queryEvents(events2, options) {
31475
+ let rows = events2;
31476
31476
  if (options.eventId)
31477
31477
  rows = rows.filter((event) => event.id === options.eventId);
31478
31478
  if (options.source)
@@ -31491,8 +31491,8 @@ function assertCursorFilter(name, cursorValue, optionValue) {
31491
31491
  if (cursorValue !== optionValue)
31492
31492
  throw new Error(`Local JSON event cursor ${name} filter mismatch`);
31493
31493
  }
31494
- function findEventByIdentity(events, identity3) {
31495
- return events.find((event) => identity3.id !== undefined && event.id === identity3.id || identity3.dedupeKey !== undefined && event.dedupeKey === identity3.dedupeKey);
31494
+ function findEventByIdentity(events2, identity3) {
31495
+ return events2.find((event) => identity3.id !== undefined && event.id === identity3.id || identity3.dedupeKey !== undefined && event.dedupeKey === identity3.dedupeKey);
31496
31496
  }
31497
31497
  function buildSignatureBase(timestamp, body4) {
31498
31498
  return `${timestamp}.${body4}`;
@@ -32125,16 +32125,16 @@ class EventsClient {
32125
32125
  async listEventsPage(options = {}) {
32126
32126
  if (this.store.listEventsPage)
32127
32127
  return this.store.listEventsPage(options);
32128
- const events = queryClientEvents(await this.store.listEvents(), {
32128
+ const events2 = queryClientEvents(await this.store.listEvents(), {
32129
32129
  eventId: options.eventId,
32130
32130
  source: options.source,
32131
32131
  type: options.type
32132
32132
  });
32133
32133
  const offset = decodeLocalJsonEventCursor(options.cursor, options);
32134
32134
  const limit = normalizeEventPageLimit(options.limit);
32135
- const pageEvents = events.slice(offset, offset + limit);
32135
+ const pageEvents = events2.slice(offset, offset + limit);
32136
32136
  const nextOffset = offset + pageEvents.length;
32137
- const hasMore = nextOffset < events.length;
32137
+ const hasMore = nextOffset < events2.length;
32138
32138
  return {
32139
32139
  events: pageEvents,
32140
32140
  cursor: options.cursor,
@@ -32265,8 +32265,8 @@ class EventsClient {
32265
32265
  return createDeliveryResult(event, channel, attempts);
32266
32266
  }
32267
32267
  }
32268
- function queryClientEvents(events, options) {
32269
- let rows = events;
32268
+ function queryClientEvents(events2, options) {
32269
+ let rows = events2;
32270
32270
  if (options.eventId)
32271
32271
  rows = rows.filter((event) => event.id === options.eventId);
32272
32272
  if (options.source)
@@ -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.13",
32376
+ version: "0.9.15",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -40126,32 +40126,37 @@ function canonical3(value) {
40126
40126
  return { kind: "array", items: value.items.map(canonical3) };
40127
40127
  return value;
40128
40128
  }
40129
- function assertClaudeStopHookReplacement(before, replacement) {
40129
+ var CLAUDE_COORDINATED_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "Stop", "Notification", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart"];
40130
+ function assertClaudeHookEventsReplacement(before, replacement, events) {
40131
+ need3(Array.isArray(events) && events.length > 0 && events.length <= CLAUDE_COORDINATED_HOOK_EVENTS.length && new Set(events).size === events.length && events.every((event) => CLAUDE_COORDINATED_HOOK_EVENTS.includes(event)), "requires explicit supported hook events");
40132
+ const label = events.join(",");
40130
40133
  function bounded(text2) {
40131
- need3(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "Stop update exceeds its byte limit");
40134
+ need3(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, `${label} update exceeds its byte limit`);
40132
40135
  return parse6(text2);
40133
40136
  }
40134
40137
  const original = bounded(before ?? "{}"), next = bounded(replacement);
40135
40138
  const hadHooks = original.entries.some(([key]) => key === "hooks");
40136
- function withoutStop(value) {
40139
+ function withoutOwnedEvents(value) {
40137
40140
  const hooks = value.entries.find(([key]) => key === "hooks")?.[1];
40138
40141
  if (hooks !== undefined) {
40139
- need3(hooks.kind === "object", "Stop update requires an object of hooks");
40140
- const stop = hooks.entries.find(([key]) => key === "Stop")?.[1];
40141
- need3(stop === undefined || stop.kind === "array", "Stop update requires a Stop array");
40142
- hooks.entries = hooks.entries.filter(([key]) => key !== "Stop");
40142
+ need3(hooks.kind === "object", `${label} update requires an object of hooks`);
40143
+ for (const event of events) {
40144
+ const selected = hooks.entries.find(([key]) => key === event)?.[1];
40145
+ need3(selected === undefined || selected.kind === "array", `${event} update requires a ${event} array`);
40146
+ }
40147
+ hooks.entries = hooks.entries.filter(([key]) => !events.includes(key));
40143
40148
  if (!hadHooks && hooks.entries.length === 0)
40144
40149
  value.entries = value.entries.filter(([key]) => key !== "hooks");
40145
40150
  }
40146
40151
  return canonical3(value);
40147
40152
  }
40148
- need3(JSON.stringify(withoutStop(original)) === JSON.stringify(withoutStop(next)), "Stop update changed settings outside hooks.Stop");
40153
+ need3(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
40149
40154
  }
40150
40155
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40151
40156
  function inferencePreference(key, child) {
40152
40157
  if (key === "skipDangerousModePermissionPrompt") {
40153
40158
  need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
40154
- return true;
40159
+ return false;
40155
40160
  }
40156
40161
  if (key === "effortLevel") {
40157
40162
  need3(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
@@ -40262,14 +40267,20 @@ function unchanged2(before) {
40262
40267
  need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
40263
40268
  }
40264
40269
 
40270
+ // src/lib/codex-hook-additions-review.ts
40271
+ var events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
40272
+ var matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
40273
+ var proofs = new WeakMap;
40274
+
40265
40275
  // src/lib/codex-hook-discovery-recovery.ts
40266
40276
  var sha = (text2) => createHash14("sha256").update(text2).digest("hex");
40267
- var proofs = new WeakMap;
40277
+ var proofs2 = new WeakMap;
40268
40278
  function assertCodexHookDiscoveryRecovery(binding, witness) {
40269
- const proof = proofs.get(witness);
40279
+ const proof = proofs2.get(witness);
40270
40280
  need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
40271
40281
  unchanged2(proof.before);
40272
40282
  unchanged2(proof.current);
40283
+ proof.recheckAdditions();
40273
40284
  }
40274
40285
  function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
40275
40286
  if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
@@ -41301,6 +41312,34 @@ function planAgentIntegration(options) {
41301
41312
  return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
41302
41313
  }
41303
41314
  function planClaudeStopHookUpdate(options) {
41315
+ return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
41316
+ }
41317
+ function planClaudePreToolUseHookUpdate(options) {
41318
+ return planClaudeHookEventsUpdate({ ...options, events: ["PreToolUse"] });
41319
+ }
41320
+ function claudeSettingsConsumers(home, path, discoveries, aliases) {
41321
+ const canonical4 = (value) => canonicalAgentPath(value, aliases);
41322
+ const containsSettings = (root2) => {
41323
+ const distance = relative3(canonical4(root2), path);
41324
+ return distance === "" || distance !== ".." && !distance.startsWith(`..${sep4}`) && !isAbsolute15(distance);
41325
+ };
41326
+ return discoveries.filter((binding) => binding.agent === "claude" || binding.agent === "opencode" || canonical4(join21(home, AGENT_ADAPTERS[binding.agent].config)) === path || binding.sources.some((source) => canonical4(source.path) === path) || binding.roots.some(containsSettings) || binding.directories?.some((directory) => containsSettings(directory.path))).map((binding) => binding.agent);
41327
+ }
41328
+ function verifyCoordinatedDiscovery(binding, home, aliases) {
41329
+ try {
41330
+ if (binding.method !== "automatic" && binding.method !== "reviewed")
41331
+ throw new Error("Invalid managed discovery method");
41332
+ verifyAgentDiscovery(binding);
41333
+ if (binding.method === "automatic") {
41334
+ const current = resolveAgentDiscovery({ home, agent: binding.agent, canonical: (path) => canonicalAgentPath(path, aliases) });
41335
+ if (JSON.stringify(current) !== JSON.stringify(binding))
41336
+ throw new Error("Configured native discovery roots changed during Claude update");
41337
+ }
41338
+ } catch (error) {
41339
+ throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
41340
+ }
41341
+ }
41342
+ function planClaudeHookEventsUpdate(options) {
41304
41343
  const home = resolve14(options.home ?? homedir3()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
41305
41344
  const policyPath = join21(dataDir2, "agent-policy.json");
41306
41345
  assertSafePath(policyPath);
@@ -41318,31 +41357,42 @@ function planClaudeStopHookUpdate(options) {
41318
41357
  const before = lstatSync18(path, { throwIfNoEntry: false }) ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readNativeBytes(path, 1024 * 1024)) : null;
41319
41358
  if ((before === null ? "absent" : sha2(before)) !== options.expectedSettingsSha256)
41320
41359
  throw new Error("Claude settings preimage changed");
41321
- assertClaudeStopHookReplacement(before, options.replacement);
41360
+ assertClaudeHookEventsReplacement(before, options.replacement, options.events);
41322
41361
  const agents = bridge.agents;
41323
41362
  const discoveries = Object.entries(bridge.discovery).map(([agent, binding]) => {
41324
- if (!agents.includes(agent) || binding?.agent !== agent)
41325
- throw new Error("Invalid managed discovery owner");
41363
+ if (!agents.includes(agent) || binding?.agent !== agent || typeof bridge.commands?.[agent] !== "string" || typeof bridge.profiles?.[agent] !== "string")
41364
+ throw new Error("Invalid managed discovery owner or command/profile binding");
41326
41365
  return binding;
41327
41366
  });
41328
- if (discoveries.length !== agents.length)
41329
- throw new Error("Missing managed discovery coverage");
41330
- for (const agent of agents)
41367
+ if (discoveries.length !== agents.length || !agents.includes("claude"))
41368
+ throw new Error("Missing managed Claude discovery coverage");
41369
+ for (const binding of discoveries)
41370
+ verifyCoordinatedDiscovery(binding, home, aliases);
41371
+ const consumers = claudeSettingsConsumers(home, path, discoveries, aliases);
41372
+ for (const agent of consumers)
41331
41373
  assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: home });
41332
41374
  const replacements = new Map(before === options.replacement ? [] : [[path, options.replacement]]);
41333
- const discoveryAfter = discoveries.map((binding) => rebindAgentDiscovery(binding, replacements));
41375
+ const discoveryAfter = discoveries.map((binding) => {
41376
+ const next = rebindAgentDiscovery(binding, replacements);
41377
+ for (const [index, source] of binding.sources.entries()) {
41378
+ if (source.path !== path && JSON.stringify(next.sources[index]) !== JSON.stringify(source)) {
41379
+ throw new Error(`Unrelated native discovery input changed during Claude update: ${source.path}`);
41380
+ }
41381
+ }
41382
+ return JSON.stringify(next) === JSON.stringify(binding) ? binding : next;
41383
+ });
41334
41384
  const nextPolicy = { ...policy, bridge: { ...bridge, discovery: Object.fromEntries(discoveryAfter.map((binding) => [binding.agent, binding])) } };
41335
41385
  const changes = before === options.replacement ? [] : [{ path, before, after: options.replacement }];
41336
41386
  if (JSON.stringify(nextPolicy) !== JSON.stringify(policy))
41337
41387
  changes.push({ path: policyPath, before: snapshot2.text, after: serializeManagedSkillPolicy(nextPolicy) });
41338
41388
  recheckRootAliases(aliases);
41339
41389
  const nativeSkills = inventoryNativeSkills(home, {
41340
- agents,
41390
+ agents: consumers,
41341
41391
  projectDir: home,
41342
41392
  includeVendor: true,
41343
- guardHermes: agents.includes("hermes"),
41393
+ guardHermes: consumers.includes("hermes"),
41344
41394
  allowRootAliases: aliases.length > 0,
41345
- agentRoots: discoveries.flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
41395
+ agentRoots: discoveries.filter((binding) => consumers.includes(binding.agent)).flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
41346
41396
  });
41347
41397
  return {
41348
41398
  dataDir: dataDir2,
@@ -41353,7 +41403,7 @@ function planClaudeStopHookUpdate(options) {
41353
41403
  discoveryBefore: discoveries,
41354
41404
  discoveryAfter,
41355
41405
  rootAliases: aliases,
41356
- managedAgentChecks: { home, agents: [...agents] }
41406
+ managedAgentChecks: { home, agents: consumers }
41357
41407
  };
41358
41408
  }
41359
41409
  function atomicWrite2(path, content) {
@@ -41380,8 +41430,12 @@ function applyAgentIntegration(plan) {
41380
41430
  recheckRootAliases(aliases);
41381
41431
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41382
41432
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41383
- for (const binding of plan.discoveryBefore ?? [])
41384
- verifyAgentDiscovery(binding);
41433
+ for (const binding of plan.discoveryBefore ?? []) {
41434
+ if (plan.managedAgentChecks)
41435
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
41436
+ else
41437
+ verifyAgentDiscovery(binding);
41438
+ }
41385
41439
  if (plan.observedPolicy && currentText(plan.observedPolicy.path) !== plan.observedPolicy.before)
41386
41440
  throw new Error("Agent policy changed after planning");
41387
41441
  for (const change of plan.changes)
@@ -41413,8 +41467,12 @@ function applyAgentIntegration(plan) {
41413
41467
  written.push({ path: change.path, before: change.before, after });
41414
41468
  }
41415
41469
  recheckRootAliases(aliases);
41416
- for (const binding of plan.discoveryAfter ?? [])
41417
- verifyAgentDiscovery(binding);
41470
+ for (const binding of plan.discoveryAfter ?? []) {
41471
+ if (plan.managedAgentChecks)
41472
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
41473
+ else
41474
+ verifyAgentDiscovery(binding);
41475
+ }
41418
41476
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41419
41477
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41420
41478
  atomicWrite2(join21(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: written.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha2(change.before), afterHash: sha2(change.after) })), backups, ...aliases.length ? { rootAliases: aliases } : {} }) + `
@@ -294,9 +294,11 @@ addition to the v1 display/model selections, it tolerates the documented
294
294
  persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
295
295
  level and in known built-in model entries whose only property is `effortLevel`.
296
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.
297
+ The boolean `skipDangerousModePermissionPrompt` remains bound: Claude also uses
298
+ this saved acceptance in bypass-mode and pending project MCP approval decisions.
299
+ Permission settings, plugins, hooks, environment, ultracode and unknown
300
+ configuration remain bound. Version 0.9.14 corrects the 0.9.13 normalization of
301
+ this flag; an existing v2 review containing it must be explicitly reviewed again.
300
302
  Invalid recognized preference types are refused.
301
303
 
302
304
  `captureClaudeMarketplaceRegistryV2(path)` creates a
@@ -322,3 +324,25 @@ preference: its owning installer must coordinate a guarded settings/policy
322
324
  update, or it needs a fresh explicit review.
323
325
 
324
326
  The effort schema follows [Claude's model configuration documentation](https://code.claude.com/docs/en/model-config#adjust-effort-level).
327
+
328
+ Cooperating installers can use `planClaudeHookEventsUpdate` with an explicit
329
+ selection of supported Claude hook events. `planClaudePreToolUseHookUpdate` and
330
+ `planClaudeStopHookUpdate` are single-event convenience wrappers. Supported events
331
+ are `PreToolUse`, `PostToolUse`, `Stop`, `Notification`, `SessionStart`, `SessionEnd`
332
+ `UserPromptSubmit` and `SubagentStart`. The transaction requires the exact settings SHA-256
333
+ preimage and preserves every field outside the selected events, including other
334
+ hooks. Apply the returned plan through `applyAgentIntegration`; this validates
335
+ existing managed discovery and native guards, writes settings and policy with
336
+ precondition checks, and retains recovery backups. An invalid policy or existing
337
+ drift is a refusal, never permission to use an uncoordinated writer. A null plan
338
+ means no managed Skills policy exists. These APIs do not authorize the hook's
339
+ behavior or turn a configuration-changing hook into a preference.
340
+
341
+ The transaction verifies every agent's discovery sources, directories and
342
+ automatic discovery closure. Native runtime and trust checks cover Claude and
343
+ the declared consumers of the settings path, including containing roots or
344
+ directories and OpenCode's Claude compatibility surface. Unrelated agents keep
345
+ their exact witnesses and trust state; they must still pass their own consumer
346
+ checks before use. The plan's `managedAgentChecks.agents` and `nativeSkills`
347
+ describe this scoped runtime check, not a fleet health result. A path-identity
348
+ witness intersecting the write still refuses instead of adopting a new inode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {