@hasna/skills 0.10.91 → 0.10.92

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
@@ -91998,7 +91998,7 @@ var init_event_sink = __esm(() => {
91998
91998
  init_dist();
91999
91999
  });
92000
92000
  // package.json
92001
- var version = "0.10.91";
92001
+ var version = "0.10.92";
92002
92002
  var package_default = {
92003
92003
  name: "@hasna/skills",
92004
92004
  version,
@@ -102865,6 +102865,234 @@ if (failed) process.exitCode = 2;
102865
102865
  `;
102866
102866
  }
102867
102867
 
102868
+ // src/lib/agent-opencode.ts
102869
+ function renderOpenCodePlugin(command, profile) {
102870
+ return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
102871
+ import { spawn } from "node:child_process";
102872
+ import { createHash } from "node:crypto";
102873
+ const command = ${JSON.stringify(command)};
102874
+ const profile = ${JSON.stringify(profile)};
102875
+ const bridgeBodySha256 = ${JSON.stringify(OPENCODE_BRIDGE_BODY_SHA256)};
102876
+ const bridgeDescriptionSha256 = ${JSON.stringify(OPENCODE_BRIDGE_DESCRIPTION_SHA256)};
102877
+ function digest(text) { return createHash("sha256").update(text).digest("hex"); }
102878
+ function canonicalSkill(skill) {
102879
+ return skill?.id === "skills-cli" && skill.name === "skills-cli" && typeof skill.path === "string"
102880
+ && typeof skill.content === "string" && digest(skill.content) === bridgeBodySha256
102881
+ && typeof skill.description === "string" && digest(skill.description) === bridgeDescriptionSha256
102882
+ && (skill.autoinvoke === undefined || skill.autoinvoke === false);
102883
+ }
102884
+ function refusal() {
102885
+ const error = new Error("Skills verification blocked this request. Review the OpenCode Skills hook configuration, then retry.");
102886
+ error.name = "SkillsHookRefusal";
102887
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
102888
+ return error;
102889
+ }
102890
+ export default {
102891
+ id: "local.hasna-skills-cli",
102892
+ async setup(ctx) {
102893
+ if (!ctx?.session?.hook || !ctx?.session?.get || !ctx?.skill?.transform || !ctx?.skill?.list || !ctx?.tool?.hook) throw refusal();
102894
+ const registrations = [], pending = new Set(), sessions = new Set(), prompts = new Map();
102895
+ let disposed = false;
102896
+ function context(input) {
102897
+ if (disposed) return Promise.reject(refusal());
102898
+ return new Promise((resolve, reject) => {
102899
+ // Availability failures may omit optional model context. Native skill
102900
+ // consumption and explicit policy refusals always remain fail-closed.
102901
+ const optional = input.delivery_phase === "context";
102902
+ const chunks = []; let bytes = 0, settled = false, child, timer;
102903
+ const finish = () => { clearTimeout(timer); pending.delete(cancel); };
102904
+ const deny = () => { if (settled) return; settled = true; finish(); reject(refusal()); };
102905
+ const unavailable = code => {
102906
+ if (settled) return;
102907
+ if (!optional || disposed) return deny();
102908
+ settled = true; finish();
102909
+ resolve({ delivered: false, text: "Skills context is unavailable [SKILLS_HOOK_" + code + "]. No Skills instructions were delivered by this hook. Ordinary work may continue. Do not perform actions that depend on unavailable Skills instructions, including previously loaded instructions. Explicit skills load, context and run still require verification. Do not substitute native, unmanaged or stale payloads." });
102910
+ };
102911
+ const stop = code => { unavailable(code); child?.kill("SIGKILL"); };
102912
+ const cancel = () => { deny(); child?.kill("SIGKILL"); };
102913
+ try {
102914
+ child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
102915
+ } catch { unavailable("SPAWN_FAILED"); return; }
102916
+ pending.add(cancel);
102917
+ timer = setTimeout(() => stop("TIMEOUT"), 15000);
102918
+ child.stdout.on("data", chunk => { if (settled) return; bytes += chunk.length; if (bytes > 1048576) stop("OUTPUT_LIMIT"); else chunks.push(chunk); });
102919
+ child.stdout.on("error", () => stop("TRANSPORT_FAILED"));
102920
+ child.stderr.on("data", () => {});
102921
+ child.stderr.on("error", () => stop("TRANSPORT_FAILED"));
102922
+ child.stdin.on("error", () => stop("TRANSPORT_FAILED"));
102923
+ child.on("error", () => unavailable("SPAWN_FAILED"));
102924
+ child.on("close", code => {
102925
+ if (settled) return;
102926
+ if (disposed) return deny();
102927
+ if (code !== 0) return unavailable("CHILD_FAILED");
102928
+ let result;
102929
+ try {
102930
+ result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
102931
+ } catch { unavailable("INVALID_RESPONSE"); return; }
102932
+ if (!result || typeof result !== "object" || Array.isArray(result)) return unavailable("INVALID_RESPONSE");
102933
+ if (result.decision === "block" || result.continue === false) return deny();
102934
+ // hookContextOutput returns exactly {} for a successful empty
102935
+ // context. It never attests native bridge verification.
102936
+ const output = optional && Object.keys(result).length === 0
102937
+ ? { hookEventName: input.hook_event_name, additionalContext: "" }
102938
+ : result.hookSpecificOutput;
102939
+ if (!output || typeof output !== "object" || Array.isArray(output) || output.hookEventName !== input.hook_event_name || typeof output.additionalContext !== "string") return unavailable("INVALID_RESPONSE");
102940
+ if (input.hook_event_name === "BeforeTool" && result.nativeBridgeVerified !== true) return deny();
102941
+ settled = true; finish();
102942
+ resolve({ delivered: true, text: output.additionalContext });
102943
+ });
102944
+ try { child.stdin.end(JSON.stringify(input)); } catch { stop("TRANSPORT_FAILED"); }
102945
+ });
102946
+ }
102947
+ async function input(sessionID, prompt, event) {
102948
+ if (disposed) throw refusal();
102949
+ let session;
102950
+ try { session = await ctx.session.get({ sessionID }); } catch { throw refusal(); }
102951
+ if (disposed || !session || session.id !== sessionID || typeof session.location?.directory !== "string" || (session.parentID !== undefined && typeof session.parentID !== "string") || session.parentID === sessionID) throw refusal();
102952
+ return { hook_event_name: event ?? (!sessions.has(sessionID) ? session.parentID ? "SubagentStart" : "SessionStart" : "UserPromptSubmit"), delivery_phase: event ? "native" : "context", cwd: session.location.directory, session_id: sessionID, parent_session_id: session.parentID ?? null, prompt, restore: true };
102953
+ }
102954
+ async function dispose() {
102955
+ if (disposed) return;
102956
+ disposed = true;
102957
+ for (const cancel of pending) cancel();
102958
+ sessions.clear();
102959
+ prompts.clear();
102960
+ const results = await Promise.allSettled(registrations.reverse().map(registration => Promise.resolve().then(() => registration.dispose())));
102961
+ if (results.some(result => result.status === "rejected")) throw refusal();
102962
+ }
102963
+ try {
102964
+ registrations.push(await ctx.skill.transform(editor => {
102965
+ for (const skill of editor.list()) if (!canonicalSkill(skill)) editor.remove(skill.id);
102966
+ }));
102967
+ async function verifyNativeBridge(sessionID) {
102968
+ let skills;
102969
+ try { skills = await ctx.skill.list(); } catch { throw refusal(); }
102970
+ if (!Array.isArray(skills)) throw refusal();
102971
+ const matches = skills.filter(skill => skill?.id === "skills-cli");
102972
+ if (matches.length !== 1) throw refusal();
102973
+ const skill = matches[0];
102974
+ if (!canonicalSkill(skill)) throw refusal();
102975
+ const metadata = { id: skill.id, name: skill.name, path: skill.path, contentSha256: digest(skill.content), descriptionSha256: digest(skill.description) };
102976
+ await context({ ...await input(sessionID, "", "BeforeTool"), tool_input: { id: "skills-cli" }, native_skill: metadata });
102977
+ }
102978
+ registrations.push(await ctx.tool.hook("execute.before", async event => {
102979
+ if (disposed) throw refusal();
102980
+ if (event.tool !== "skill") return;
102981
+ if (event.input?.id !== "skills-cli") throw refusal();
102982
+ await verifyNativeBridge(event.sessionID);
102983
+ }));
102984
+ registrations.push(await ctx.session.hook("prompt", async event => {
102985
+ if (disposed || typeof event.prompt?.text !== "string" || event.prompt.text.length > 128 * 1024) throw refusal();
102986
+ if (event.prompt.skills !== undefined && (!Array.isArray(event.prompt.skills) || event.prompt.skills.some(skill => skill?.id !== "skills-cli"))) throw refusal();
102987
+ // Attachments materialize immediately after this hook. Check the actual
102988
+ // current registry object and on-disk bridge before native consumption.
102989
+ if (event.prompt.skills?.length) await verifyNativeBridge(event.sessionID);
102990
+ if (disposed) throw refusal();
102991
+ const queued = prompts.get(event.sessionID) ?? [];
102992
+ if (queued.reduce((size, entry) => size + entry.text.length + 1, 0) + event.prompt.text.length > 128 * 1024) throw refusal();
102993
+ queued.push({ text: event.prompt.text });
102994
+ prompts.set(event.sessionID, queued);
102995
+ }));
102996
+ registrations.push(await ctx.session.hook("context", async event => {
102997
+ // Only prompt-hook input has owner-prompt provenance. Keep it transient
102998
+ // until the one delivery-bearing CLI call, including an empty prompt.
102999
+ // Checkpoints, synthetic user-role history and resumes never match.
103000
+ const queued = [...(prompts.get(event.sessionID) ?? [])];
103001
+ const result = await context(await input(event.sessionID, queued.map(entry => entry.text).join("\\n")));
103002
+ if (disposed) throw refusal();
103003
+ if (result.text) event.system.push({ type: "text", text: result.text });
103004
+ // Remove only the prompt events this request consumed. New admission
103005
+ // while the CLI was running belongs to the next model context.
103006
+ const current = prompts.get(event.sessionID);
103007
+ if (current) {
103008
+ const remaining = current.filter(entry => !queued.includes(entry));
103009
+ if (remaining.length) prompts.set(event.sessionID, remaining);
103010
+ else prompts.delete(event.sessionID);
103011
+ }
103012
+ // Retry SessionStart on the next context after an unavailable child,
103013
+ // but never replay prompt text already admitted to this model turn.
103014
+ if (result.delivered) sessions.add(event.sessionID);
103015
+ }));
103016
+ return dispose;
103017
+ } catch {
103018
+ await dispose();
103019
+ throw refusal();
103020
+ }
103021
+ },
103022
+ };
103023
+ `;
103024
+ }
103025
+
103026
+ // src/lib/agent-adapters.ts
103027
+ var INTEGRATION_AGENTS = ["claude", "codex", "gemini", "opencode", "cursor", "hermes", "sumi"];
103028
+ function renderAgentHookCommand(command, agent, profileId, event) {
103029
+ if (typeof profileId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) || profileId.includes(".."))
103030
+ throw new Error("Invalid selection profile id");
103031
+ return `'${command.replace(/'/g, `'\\''`)}' hook user-prompt --agent ${agent} --selection-profile ${profileId} --event ${event}`;
103032
+ }
103033
+ var AGENT_ADAPTERS = {
103034
+ claude: { root: ".claude/skills", config: ".claude/settings.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
103035
+ codex: { root: ".codex/skills", config: ".codex/hooks.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
103036
+ gemini: { root: ".gemini/skills", config: ".gemini/settings.json", events: ["BeforeAgent", "SessionStart"], promptContext: true },
103037
+ opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["prompt", "context"], promptContext: true },
103038
+ sumi: { root: null, config: null, events: ["prompt", "context"], promptContext: true },
103039
+ hermes: { root: ".hermes/skills", config: ".hermes/config.yaml", events: ["pre_llm_call", "pre_tool_call"], promptContext: true, promptFailureMode: "open", toolFailureMode: "supervised-child-errors-block", requiresNativeTrust: true },
103040
+ cursor: { root: ".cursor/skills", config: ".cursor/hooks.json", events: ["beforeSubmitPrompt", "sessionStart"], promptContext: false }
103041
+ };
103042
+ function renderLegacyOpenCodePlugin(command, profileId) {
103043
+ return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
103044
+ import { spawn } from "node:child_process";
103045
+ import { randomUUID } from "node:crypto";
103046
+ const command = ${JSON.stringify(command)};
103047
+ const profile = ${JSON.stringify(profileId)};
103048
+ function context(input) {
103049
+ return new Promise((resolve, reject) => {
103050
+ const child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
103051
+ const chunks = []; let bytes = 0, failure;
103052
+ const timer = setTimeout(() => { failure = new Error("Skills prompt hook timed out"); child.kill("SIGKILL"); }, 15000);
103053
+ child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > 1048576) { failure = new Error("Skills prompt hook output exceeded its limit"); child.kill("SIGKILL"); } else chunks.push(chunk); });
103054
+ child.stderr.on("data", () => {});
103055
+ child.stdin.on("error", () => {});
103056
+ child.on("error", error => { clearTimeout(timer); reject(error); });
103057
+ child.on("close", code => {
103058
+ clearTimeout(timer);
103059
+ if (failure || code !== 0) return reject(failure ?? new Error("Skills prompt hook failed"));
103060
+ try {
103061
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
103062
+ if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("Invalid Skills hook response");
103063
+ if (result.decision === "block" || result.continue === false) throw new Error(result.reason ?? result.stopReason ?? "Skills prompt hook refused");
103064
+ if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw new Error("Invalid Skills hook response");
103065
+ const text = result.hookSpecificOutput?.additionalContext ?? "";
103066
+ if (typeof text !== "string") throw new Error("Invalid Skills prompt context");
103067
+ resolve(text);
103068
+ } catch (error) { reject(error); }
103069
+ });
103070
+ child.stdin.end(JSON.stringify(input));
103071
+ });
103072
+ }
103073
+ export default async ({ directory }) => {
103074
+ const sessions = new Set();
103075
+ return {
103076
+ "chat.message": async (input, output) => {
103077
+ const parts = output.parts.filter(part => part.type === "text" && typeof part.text === "string");
103078
+ const first = !sessions.has(input.sessionID);
103079
+ const text = await context({ hook_event_name: first ? "SessionStart" : "UserPromptSubmit", cwd: directory, session_id: input.sessionID, prompt: parts.map(part => part.text).join("\\n") });
103080
+ if (text && parts.length) parts[0].text += "\\n\\n" + text;
103081
+ else if (text) output.parts.push({ id: "prt_" + randomUUID().replaceAll("-", ""), sessionID: input.sessionID, messageID: output.message.id, type: "text", text, synthetic: true });
103082
+ if (sessions.size >= 1000) sessions.clear();
103083
+ sessions.add(input.sessionID);
103084
+ },
103085
+ "tool.execute.before": async (input, output) => {
103086
+ if (input.tool === "skill" && output.args?.name !== "skills-cli") throw new Error("Load instructions through the Skills CLI bridge");
103087
+ },
103088
+ };
103089
+ };
103090
+ `;
103091
+ }
103092
+ function isManagedOpenCodePlugin(text2, command, profileId) {
103093
+ return text2 === renderOpenCodePlugin(command, profileId) || text2 === renderLegacyOpenCodePlugin(command, profileId);
103094
+ }
103095
+
102868
103096
  // src/lib/plugin-projection-store.ts
102869
103097
  import { createHash as createHash24, randomUUID as randomUUID3 } from "crypto";
102870
103098
  import { constants as constants15, closeSync as closeSync17, fchmodSync, fstatSync as fstatSync17, fsyncSync as fsyncSync3, linkSync as linkSync2, lstatSync as lstatSync22, mkdirSync as mkdirSync6, openSync as openSync17, opendirSync as opendirSync3, readSync as readSync11, renameSync as renameSync4, rmdirSync as rmdirSync2, rmSync as rmSync2, unlinkSync as unlinkSync5, writeSync as writeSync2 } from "fs";
@@ -106400,32 +106628,119 @@ function nativeDiscoveryAliasTargets(roots, home) {
106400
106628
  }
106401
106629
  return [...targets];
106402
106630
  }
106403
- function resolveDiscovery(options, exclusion) {
106404
- const canonical9 = options.canonical ?? resolve27, home = resolve27(options.home), agent = options.agent;
106405
- const sources = [], roots = new Set, builtinNames = [];
106406
- const budget3 = discoveryByteBudget();
106407
- let remainingEntries = AGENT_POLICY_LIMITS.discoveryDirectoryEntries;
106408
- const entries = (path) => {
106409
- if (!exclusion)
106410
- return readdirSync11(path);
106411
- const directory2 = opendirSync5(path), names = [];
106631
+ function hooksAreManagedBridge(agent, hooks, managed) {
106632
+ const table = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
106633
+ const exactKeys4 = (value, keys3) => Object.keys(value).sort().join("\x00") === [...keys3].sort().join("\x00");
106634
+ if (!table(hooks) || typeof managed.command !== "string" || !isAbsolute26(managed.command))
106635
+ return false;
106636
+ const events3 = agent === "gemini" ? ["BeforeAgent", "SessionStart", "BeforeTool"] : ["beforeSubmitPrompt", "sessionStart"];
106637
+ for (const [event, entries] of Object.entries(hooks)) {
106638
+ if (!events3.includes(event) || !Array.isArray(entries) || entries.length !== 1)
106639
+ return false;
106640
+ let expected;
106412
106641
  try {
106413
- for (let entry;(entry = directory2.readSync()) !== null; ) {
106414
- if (--remainingEntries < 0)
106415
- throw new Error("Native discovery exclusion enumeration exceeds its bound");
106416
- names.push(entry.name);
106417
- }
106418
- } finally {
106419
- directory2.closeSync();
106642
+ expected = renderAgentHookCommand(managed.command, agent, managed.profile, event);
106643
+ } catch {
106644
+ return false;
106420
106645
  }
106421
- return names;
106422
- };
106423
- const witness = (path, format, fields2, hashMode) => {
106424
- const source = { path: canonical9(path), sha256: null, ...format ? { format, fields: fields2 } : {}, ...hashMode ? { hashMode } : {} };
106425
- if (exclusion && sources.length >= AGENT_POLICY_LIMITS.discoverySources)
106426
- throw new Error("Native discovery exclusion sources exceed their bound");
106427
- if (exclusion)
106428
- safe6(source.path);
106646
+ const entry = entries[0];
106647
+ if (!table(entry))
106648
+ return false;
106649
+ if (agent === "cursor") {
106650
+ if (!exactKeys4(entry, ["command", "timeout"]) || entry.timeout !== 15 || entry.command !== expected)
106651
+ return false;
106652
+ continue;
106653
+ }
106654
+ if (!exactKeys4(entry, event === "BeforeTool" ? ["hooks", "matcher"] : ["hooks"]) || event === "BeforeTool" && entry.matcher !== "activate_skill" || !Array.isArray(entry.hooks) || entry.hooks.length !== 1)
106655
+ return false;
106656
+ const hook = entry.hooks[0];
106657
+ if (!table(hook) || !exactKeys4(hook, ["command", "timeout", "type"]) || hook.type !== "command" || hook.timeout !== 15000 || hook.command !== expected)
106658
+ return false;
106659
+ }
106660
+ return true;
106661
+ }
106662
+ function sumiPluginLoadsOnlyBuiltins(text2) {
106663
+ for (const match of text2.matchAll(/\b(?:import|require)\s*\(\s*(.?)/g)) {
106664
+ const rest = text2.slice(match.index + match[0].length - match[1].length);
106665
+ if (!/^(['"])node:[A-Za-z0-9_/.-]+\1/.test(rest))
106666
+ return false;
106667
+ }
106668
+ for (const match of text2.matchAll(/\b(?:from|import)\s*(['"])([^'"]*)\1/g))
106669
+ if (!match[2].startsWith("node:"))
106670
+ return false;
106671
+ return true;
106672
+ }
106673
+ var CODEX_MARKETPLACE_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
106674
+ function codexRootFreeControls(config) {
106675
+ const table = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date));
106676
+ const adapted = [];
106677
+ const servers = config.mcp_servers;
106678
+ if (table(servers) && Object.keys(servers).length <= AGENT_POLICY_LIMITS.discoveryRoots && Object.entries(servers).every(([name, row]) => name !== "codex_apps" && name !== "_codex_apps" && isCodexLocalStdioMcpServer(name, row)))
106679
+ adapted.push("mcp_servers");
106680
+ const agents = config.agents;
106681
+ if (table(agents) && Object.keys(agents).length <= AGENT_POLICY_LIMITS.fields && Object.values(agents).every((value) => typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)))
106682
+ adapted.push("agents");
106683
+ return adapted;
106684
+ }
106685
+ var CODEX_HOOK_EVENTS = new Set(["PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart", "SubagentStop", "Stop", "Interrupt"]);
106686
+ function codexPluginHookHandlers(value) {
106687
+ const table = (item) => Boolean(item && typeof item === "object" && !Array.isArray(item));
106688
+ const identifier7 = (item) => typeof item === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(item);
106689
+ const text2 = (item) => typeof item === "string" && item.length <= AGENT_POLICY_LIMITS.pathCharacters && !item.includes("\x00");
106690
+ if (!table(value) || !Object.keys(value).every((key2) => key2 === "hooks" || key2 === "description") || value.description !== undefined && !text2(value.description) || !table(value.hooks) || Object.keys(value.hooks).length > 32)
106691
+ return null;
106692
+ const result = { commands: 0, servers: new Set };
106693
+ for (const [event, groups] of Object.entries(value.hooks)) {
106694
+ if (!CODEX_HOOK_EVENTS.has(event) || !Array.isArray(groups) || groups.length > 32)
106695
+ return null;
106696
+ for (const group of groups) {
106697
+ if (!table(group) || !Object.keys(group).every((key2) => key2 === "hooks" || key2 === "matcher") || group.matcher !== undefined && !text2(group.matcher) || !Array.isArray(group.hooks) || group.hooks.length > 32)
106698
+ return null;
106699
+ for (const handler of group.hooks) {
106700
+ if (!table(handler) || handler.timeout !== undefined && !(Number.isSafeInteger(handler.timeout) && handler.timeout >= 0) || handler.statusMessage !== undefined && !text2(handler.statusMessage))
106701
+ return null;
106702
+ if (handler.type === "command") {
106703
+ if (!text2(handler.command) || !handler.command || !Object.keys(handler).every((key2) => ["type", "command", "commandWindows", "command_windows", "timeout", "async", "statusMessage", "additionalContextLimit"].includes(key2)))
106704
+ return null;
106705
+ result.commands++;
106706
+ } else if (handler.type === "mcp_tool") {
106707
+ if (!identifier7(handler.server) || !identifier7(handler.tool) || handler.input !== undefined && !table(handler.input) || !Object.keys(handler).every((key2) => ["type", "server", "tool", "input", "timeout", "statusMessage"].includes(key2)))
106708
+ return null;
106709
+ result.servers.add(handler.server);
106710
+ } else
106711
+ return null;
106712
+ }
106713
+ }
106714
+ }
106715
+ return result;
106716
+ }
106717
+ function resolveDiscovery(options, exclusion) {
106718
+ const canonical9 = options.canonical ?? resolve27, home = resolve27(options.home), agent = options.agent;
106719
+ const sources = [], roots = new Set, builtinNames = [];
106720
+ const sumiLocalPluginTexts = new Map;
106721
+ const budget3 = discoveryByteBudget();
106722
+ let remainingEntries = AGENT_POLICY_LIMITS.discoveryDirectoryEntries;
106723
+ const entries = (path) => {
106724
+ if (!exclusion)
106725
+ return readdirSync11(path);
106726
+ const directory2 = opendirSync5(path), names = [];
106727
+ try {
106728
+ for (let entry;(entry = directory2.readSync()) !== null; ) {
106729
+ if (--remainingEntries < 0)
106730
+ throw new Error("Native discovery exclusion enumeration exceeds its bound");
106731
+ names.push(entry.name);
106732
+ }
106733
+ } finally {
106734
+ directory2.closeSync();
106735
+ }
106736
+ return names;
106737
+ };
106738
+ const witness = (path, format, fields2, hashMode) => {
106739
+ const source = { path: canonical9(path), sha256: null, ...format ? { format, fields: fields2 } : {}, ...hashMode ? { hashMode } : {} };
106740
+ if (exclusion && sources.length >= AGENT_POLICY_LIMITS.discoverySources)
106741
+ throw new Error("Native discovery exclusion sources exceed their bound");
106742
+ if (exclusion)
106743
+ safe6(source.path);
106429
106744
  const raw = exclusion && lstatSync30(source.path, { throwIfNoEntry: false }) ? readNativeDiscoveryWitnessFile(source.path, budget3) : null;
106430
106745
  if (exclusion && raw !== null && (format === "json" || source.path.endsWith(".json")))
106431
106746
  parseNativeJsonObject(raw);
@@ -106479,12 +106794,38 @@ function resolveDiscovery(options, exclusion) {
106479
106794
  const controls = agent === "claude" ? claudeDiscoveryControlKeys(configText ?? "{}") : agent === "codex" ? codexDiscoveryControlKeys(config) : Object.keys(config).filter((key2) => /skill|plugin|hook|marketplace|extension|instruction/i.test(key2));
106480
106795
  const fieldKey = (key2) => /^[A-Za-z_$][A-Za-z0-9_$.-]{0,22}$/.test(key2) && !/(?:sk|rk|pk)_(?:live|test)_|whsec_/i.test(key2) ? JSON.stringify(key2) : "[withheld field key]";
106481
106796
  const unsupportedFields = (names, label) => unresolved(`unknown discovery controls have no root-exclusion adapter for ${label}${names.slice(0, 8).map(fieldKey).join(", ")}${names.length > 8 ? ", additional keys withheld" : ""} in ${quotedPath(canonical9(configPath))}`);
106482
- const unsupportedControls = controls.filter((key2) => !supported[agent].includes(key2));
106797
+ const adapted = agent === "codex" ? codexRootFreeControls(config) : agent === "opencode" && Array.isArray(config.instructions) && config.instructions.length === 0 ? ["instructions"] : [];
106798
+ const unsupportedControls = controls.filter((key2) => !supported[agent].includes(key2) && !adapted.includes(key2));
106483
106799
  if (unsupportedControls.length)
106484
106800
  unsupportedFields(unsupportedControls, "");
106485
106801
  if (config.hooks !== undefined && !object6(config.hooks))
106486
106802
  unresolved("hooks must be an object");
106487
- const hasHooks = controls.includes("hooks") && config.hooks !== undefined && Object.keys(config.hooks).length > 0;
106803
+ const managedSupervisorOnly = () => {
106804
+ const supervisor = exclusion.managedHook?.supervisor;
106805
+ if (agent !== "hermes" || !supervisor || typeof supervisor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(supervisor.sha256) || !object6(config.hooks))
106806
+ return false;
106807
+ let expected;
106808
+ try {
106809
+ expected = hermesHookDefinitions(supervisor);
106810
+ } catch {
106811
+ return false;
106812
+ }
106813
+ for (const [event, entries2] of Object.entries(config.hooks)) {
106814
+ const want = expected[event];
106815
+ if (!want || !Array.isArray(entries2) || entries2.length !== 1 || !object6(entries2[0]))
106816
+ return false;
106817
+ const entry = entries2[0];
106818
+ if (Object.keys(entry).sort().join("\x00") !== Object.keys(want).sort().join("\x00") || Object.keys(want).some((key2) => entry[key2] !== want[key2]))
106819
+ return false;
106820
+ }
106821
+ try {
106822
+ return hashRawDiscoveryFile(canonical9(supervisor.path), budget3) === supervisor.sha256;
106823
+ } catch {
106824
+ return false;
106825
+ }
106826
+ };
106827
+ const managedBridgeOnly = (agent === "gemini" || agent === "cursor") && exclusion.managedHook !== undefined && hooksAreManagedBridge(agent, config.hooks, exclusion.managedHook) || managedSupervisorOnly();
106828
+ const hasHooks = controls.includes("hooks") && config.hooks !== undefined && Object.keys(config.hooks).length > 0 && !managedBridgeOnly;
106488
106829
  let hasPluginConfigs = false, hasFunctionHookEnvironment = false;
106489
106830
  const verifyCurrentOpaqueSettings = (label) => {
106490
106831
  const retained = exclusion.retained;
@@ -106589,7 +106930,9 @@ function resolveDiscovery(options, exclusion) {
106589
106930
  unresolved("malformed Sumi plugins");
106590
106931
  }
106591
106932
  if (agent === "hermes" && config.skills !== undefined) {
106592
- keys3(config.skills, ["external_dirs"], "Hermes skills");
106933
+ keys3(config.skills, ["external_dirs", "creation_nudge_interval"], "Hermes skills");
106934
+ if (config.skills.creation_nudge_interval !== undefined && !(Number.isSafeInteger(config.skills.creation_nudge_interval) && config.skills.creation_nudge_interval >= 0))
106935
+ unresolved("malformed Hermes skills creation_nudge_interval");
106593
106936
  if (config.skills.external_dirs !== undefined)
106594
106937
  paths(typeof config.skills.external_dirs === "string" ? [config.skills.external_dirs] : config.skills.external_dirs, "Hermes external directories");
106595
106938
  }
@@ -106609,10 +106952,21 @@ function resolveDiscovery(options, exclusion) {
106609
106952
  unresolved(`malformed Codex ${key2}`);
106610
106953
  for (const value of Object.values(config.plugins ?? {}))
106611
106954
  keys3(value, ["enabled"], "Codex plugin registration");
106612
- for (const value of Object.values(config.marketplaces ?? {}))
106613
- keys3(value, ["source_type", "source"], "Codex marketplace registration");
106955
+ for (const value of Object.values(config.marketplaces ?? {})) {
106956
+ keys3(value, ["source_type", "source", "last_updated"], "Codex marketplace registration");
106957
+ if (value.last_updated !== undefined && (typeof value.last_updated !== "string" || !CODEX_MARKETPLACE_TIMESTAMP.test(value.last_updated)))
106958
+ unresolved("malformed Codex marketplace refresh time");
106959
+ }
106614
106960
  if (config.skills !== undefined)
106615
- keys3(config.skills, ["config", "bundled"], "Codex skills");
106961
+ keys3(config.skills, ["config", "bundled", "native_allowed_paths"], "Codex skills");
106962
+ if (config.skills?.native_allowed_paths !== undefined) {
106963
+ for (const value of paths(config.skills.native_allowed_paths, "Codex native allowed skill paths")) {
106964
+ if (!isAbsolute26(value))
106965
+ unresolved("relative Codex native allowed skill path");
106966
+ const path = canonical9(value);
106967
+ roots.add(path.endsWith(`${sep13}SKILL.md`) ? dirname26(path) : path);
106968
+ }
106969
+ }
106616
106970
  if (config.skills?.bundled !== undefined) {
106617
106971
  keys3(config.skills.bundled, ["enabled"], "Codex bundled skills");
106618
106972
  if (typeof config.skills.bundled.enabled !== "boolean")
@@ -106670,7 +107024,9 @@ function resolveDiscovery(options, exclusion) {
106670
107024
  const target = join34(path, name);
106671
107025
  if (!lstatSync30(target).isFile())
106672
107026
  unresolved("local plugin packages need a dedicated discovery adapter");
106673
- witness(target);
107027
+ const text2 = witness(target);
107028
+ if (exclusion && text2 !== null)
107029
+ sumiLocalPluginTexts.set(canonical9(target), text2);
106674
107030
  }
106675
107031
  }
106676
107032
  for (const root2 of roots) {
@@ -106804,16 +107160,88 @@ function resolveDiscovery(options, exclusion) {
106804
107160
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual8(candidate2, source)) === index);
106805
107161
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
106806
107162
  }
106807
- function plugin(root2) {
107163
+ const reviewedManifestAbsence = (root2, manifestPath) => {
107164
+ const retained = exclusion?.retained;
107165
+ if (agent !== "claude" || retained?.method !== "reviewed" || ![join34(root2, "skills"), root2].some((path) => retained.roots.includes(path)))
107166
+ return false;
107167
+ const saved = retained.sources.filter((source) => source.path === canonical9(manifestPath) && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined);
107168
+ return saved.length > 0 && saved.every((source) => projected(source, undefined, budget3) === source.sha256);
107169
+ };
107170
+ const reviewedInstalledCopy = (root2, path, identity5) => {
107171
+ const retained = exclusion?.retained;
107172
+ if (agent !== "codex" || retained?.method !== "reviewed")
107173
+ return false;
107174
+ const inside = relative8(root2, canonical9(path));
107175
+ if (!inside || inside === ".." || inside.startsWith(`..${sep13}`) || isAbsolute26(inside))
107176
+ return false;
107177
+ const parent = canonical9(join34(nativeHome, "plugins/cache", identity5.marketplace, identity5.name));
107178
+ const current = hashRawDiscoveryFile(canonical9(path), budget3);
107179
+ if (current === null)
107180
+ return false;
107181
+ return retained.sources.some((source) => {
107182
+ if (source.sha256 !== current || source.format !== undefined || source.fields !== undefined || ![undefined, "bytes"].includes(source.hashMode) || source.managedPlugins !== undefined || source.reviewArtifact !== undefined || !source.path.startsWith(parent + sep13))
107183
+ return false;
107184
+ const [version2, ...rest] = source.path.slice(parent.length + 1).split(sep13);
107185
+ return Boolean(version2) && version2 !== "." && version2 !== ".." && rest.join(sep13) === inside;
107186
+ });
107187
+ };
107188
+ const verifyCodexPluginHookClosure = (root2, identity5, marketplaceSource, servers, declarations) => {
107189
+ const retained = exclusion.retained;
107190
+ if (retained.codexSemanticCache || retained.codexInstallationInputs || retained.codexRetiredMaterializations)
107191
+ return verifyOpaqueClosure(declarations);
107192
+ const within2 = (base, path) => path === base || path.startsWith(base + sep13);
107193
+ const executables = [];
107194
+ for (const server of servers) {
107195
+ const row = config.mcp_servers?.[server];
107196
+ if (server === "codex_apps" || server === "_codex_apps" || !isCodexLocalStdioMcpServer(server, row))
107197
+ unresolved("plugin hook MCP server is not a configured local stdio server");
107198
+ for (const value of [row.command, ...row.args ?? []])
107199
+ for (const candidate2 of [value, value.slice(value.indexOf("=") + 1)]) {
107200
+ if (!isAbsolute26(candidate2) || /[\0$]/.test(candidate2) || executables.length >= AGENT_POLICY_LIMITS.discoveryRoots)
107201
+ continue;
107202
+ const path = canonical9(candidate2);
107203
+ if (lstatSync30(path, { throwIfNoEntry: false })?.isFile())
107204
+ executables.push(path);
107205
+ }
107206
+ }
107207
+ const cacheRoot = canonical9(join34(nativeHome, "plugins/cache"));
107208
+ const ownCache = canonical9(join34(nativeHome, "plugins/cache", identity5.marketplace, identity5.name));
107209
+ const ownCatalog = canonical9(join34(marketplaceSource, ".agents/plugins/marketplace.json"));
107210
+ const account = canonical9(home);
107211
+ const marketplaceRoots = Object.values(config.marketplaces ?? {}).filter((market) => market?.source_type === "local" && typeof market.source === "string" && isAbsolute26(market.source)).map((market) => canonical9(market.source)).filter((path) => !within2(path, account));
107212
+ const otherPlugin = (path) => !within2(root2, path) && !within2(ownCache, path) && path !== ownCatalog && !executables.includes(path) && (within2(cacheRoot, path) || marketplaceRoots.some((base) => within2(base, path)));
107213
+ const sources2 = retained.sources.filter((source) => source.path !== canonical9(configPath) && !otherPlugin(source.path));
107214
+ if (sources2.some((source) => source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || source.reviewArtifact !== undefined || ![undefined, "bytes", "path-bytes", "executable-v1"].includes(source.hashMode)))
107215
+ return verifyOpaqueClosure(declarations);
107216
+ const declared = declarations.map((path) => relative8(root2, path));
107217
+ const declaration = (path) => declarations.includes(path) || path.startsWith(ownCache + sep13) && declared.includes(path.slice(ownCache.length + 1).split(sep13).slice(1).join(sep13));
107218
+ if (!sources2.some((source) => !declaration(source.path) && source.sha256 !== null))
107219
+ unresolved("opaque executable dependency closure is unavailable");
107220
+ for (const source of sources2)
107221
+ if (projected(source, undefined, budget3) !== source.sha256) {
107222
+ throw new NativeDiscoveryWitnessChange("input", source.path, `Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
107223
+ }
107224
+ verifyDiscoveryDirectories((retained.directories ?? []).filter((directory2) => !otherPlugin(directory2.path)));
107225
+ };
107226
+ const readCodexHookDeclaration = (text2, path) => {
107227
+ if (text2 === null)
107228
+ return null;
107229
+ try {
107230
+ return codexPluginHookHandlers(parseConfig(text2, path));
107231
+ } catch {
107232
+ return null;
107233
+ }
107234
+ };
107235
+ function plugin(root2, codex) {
106808
107236
  root2 = canonical9(root2);
106809
107237
  safe6(root2);
106810
107238
  if (exclusion)
106811
107239
  roots.add(root2);
106812
107240
  const manifestPath = join34(root2, agent === "claude" ? ".claude-plugin/plugin.json" : ".codex-plugin/plugin.json");
106813
107241
  const raw = witness(manifestPath, undefined, undefined, agent === "claude" ? "claude-plugin-manifest-v1" : undefined);
106814
- if (raw === null)
107242
+ if (raw === null && !reviewedManifestAbsence(root2, manifestPath))
106815
107243
  unresolved("plugin manifest missing");
106816
- const manifest = parseConfig(raw, manifestPath);
107244
+ const manifest = raw === null ? {} : parseConfig(raw, manifestPath);
106817
107245
  if (exclusion && Object.keys(manifest).some((key2) => /skill|plugin|hook|marketplace|extension|instruction/i.test(key2) && !["skills", "hooks"].includes(key2)))
106818
107246
  unresolved("unknown plugin discovery controls");
106819
107247
  const hooks = witness(join34(root2, "hooks/hooks.json"));
@@ -106821,20 +107249,35 @@ function resolveDiscovery(options, exclusion) {
106821
107249
  if (!exclusion || exclusion.retained?.method !== "reviewed")
106822
107250
  unresolved("plugin hooks require a separate no-skill-injection review");
106823
107251
  const required = [manifestPath, ...hooks !== null ? [join34(root2, "hooks/hooks.json")] : []];
107252
+ const scoped2 = agent === "codex" && codex !== undefined ? [] : undefined;
106824
107253
  if (manifest.hooks !== undefined) {
106825
- if (typeof manifest.hooks !== "string")
106826
- unresolved("unsupported plugin hooks target");
106827
- const hook = resolve27(root2, manifest.hooks);
106828
- if (!hook.startsWith(root2 + sep13))
106829
- unresolved("plugin hooks target escapes its root");
106830
- required.push(hook);
106831
- }
107254
+ if (scoped2 && manifest.hooks && typeof manifest.hooks === "object" && !Array.isArray(manifest.hooks)) {
107255
+ const inline = codexPluginHookHandlers(manifest.hooks);
107256
+ if (!inline)
107257
+ unresolved("unsupported inline plugin hooks");
107258
+ scoped2.push(inline);
107259
+ } else {
107260
+ if (typeof manifest.hooks !== "string")
107261
+ unresolved("unsupported plugin hooks target");
107262
+ const hook = resolve27(root2, manifest.hooks);
107263
+ if (!hook.startsWith(root2 + sep13))
107264
+ unresolved("plugin hooks target escapes its root");
107265
+ required.push(hook);
107266
+ if (scoped2)
107267
+ scoped2.push(readCodexHookDeclaration(witness(hook), hook));
107268
+ }
107269
+ }
107270
+ if (scoped2 && hooks !== null)
107271
+ scoped2.push(readCodexHookDeclaration(hooks, join34(root2, "hooks/hooks.json")));
106832
107272
  for (const path of required) {
106833
107273
  const saved = exclusion.retained.sources.filter((source) => source.path === canonical9(path) && source.sha256 !== null && source.format === undefined && source.fields === undefined);
106834
- if (!saved.length || saved.some((source) => projected(source, undefined, budget3) !== source.sha256))
107274
+ if (saved.length ? saved.some((source) => projected(source, undefined, budget3) !== source.sha256) : !(scoped2 && reviewedInstalledCopy(root2, path, codex)))
106835
107275
  unresolved("plugin hooks require current independently reviewed sources");
106836
107276
  }
106837
- verifyOpaqueClosure(required.map((path) => canonical9(path)));
107277
+ if (scoped2 && scoped2.every((item) => item !== null && item.commands === 0)) {
107278
+ verifyCodexPluginHookClosure(root2, codex, codex.source, new Set(scoped2.flatMap((item) => [...item.servers])), required.map((path) => canonical9(path)));
107279
+ } else
107280
+ verifyOpaqueClosure(required.map((path) => canonical9(path)));
106838
107281
  }
106839
107282
  const commands = join34(root2, "commands");
106840
107283
  safe6(commands);
@@ -106890,7 +107333,7 @@ function resolveDiscovery(options, exclusion) {
106890
107333
  const catalog = parseConfig(catalogText, join34(market.source, ".agents/plugins/marketplace.json")), matches = Array.isArray(catalog.plugins) ? catalog.plugins.filter((item) => item.name === name) : [];
106891
107334
  if (matches.length !== 1 || matches[0]?.source?.source !== "local" || typeof matches[0]?.source?.path !== "string")
106892
107335
  unresolved("plugin catalog source is missing or ambiguous");
106893
- plugin(resolve27(market.source, matches[0].source.path));
107336
+ plugin(resolve27(market.source, matches[0].source.path), exclusion ? { marketplace, name, source: market.source } : undefined);
106894
107337
  }
106895
107338
  } else if (agent === "opencode") {
106896
107339
  if (witness(join34(home, ".config/opencode/opencode.jsonc")) !== null || witness(join34(home, ".config/opencode/config.json")) !== null)
@@ -106907,7 +107350,11 @@ function resolveDiscovery(options, exclusion) {
106907
107350
  if (lstatSync30(localPlugins, { throwIfNoEntry: false }) && entries(localPlugins).some((name) => name !== "skills-cli.js"))
106908
107351
  unresolved("additional local plugin hooks require review");
106909
107352
  } else if (agent === "sumi") {
106910
- if ((config.plugin?.length ?? 0) > 0 || (config.plugins?.length ?? 0) > 0 || sources.some((source) => source.path !== canonical9(configPath) && source.path !== canonical9(join34(sumiConfigDirectory(home), "sumi.jsonc")) && source.sha256 !== null))
107353
+ const pinnedLocalPlugin = (source) => {
107354
+ const retained = exclusion?.retained, text2 = sumiLocalPluginTexts.get(source.path);
107355
+ return retained?.method === "reviewed" && text2 !== undefined && sumiPluginLoadsOnlyBuiltins(text2) && retained.sources.some((saved) => saved.path === source.path && saved.sha256 === source.sha256 && saved.sha256 !== null && [undefined, "bytes"].includes(saved.hashMode) && saved.format === undefined && saved.fields === undefined && saved.managedPlugins === undefined && saved.reviewArtifact === undefined);
107356
+ };
107357
+ if ((config.plugin?.length ?? 0) > 0 || (config.plugins?.length ?? 0) > 0 || sources.some((source) => source.path !== canonical9(configPath) && source.path !== canonical9(join34(sumiConfigDirectory(home), "sumi.jsonc")) && source.sha256 !== null && !pinnedLocalPlugin(source)))
106911
107358
  unresolved("external or foreign local plugins require review");
106912
107359
  } else if (agent === "hermes") {
106913
107360
  for (const directory2 of [join34(home, ".hermes/plugins"), join34(home, ".hermes/hermes-agent")]) {
@@ -106942,234 +107389,6 @@ function resolveDiscovery(options, exclusion) {
106942
107389
  return { agent, roots: [...roots].sort(), sources, method: "automatic", ...agent === "gemini" ? { builtinNames } : {}, ...agent === "hermes" ? { directories: captureDiscoveryDirectories([join34(home, ".hermes/plugins"), join34(home, ".hermes/hermes-agent")].map((path) => canonical9(path))) } : {} };
106943
107390
  }
106944
107391
 
106945
- // src/lib/agent-opencode.ts
106946
- function renderOpenCodePlugin(command, profile) {
106947
- return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
106948
- import { spawn } from "node:child_process";
106949
- import { createHash } from "node:crypto";
106950
- const command = ${JSON.stringify(command)};
106951
- const profile = ${JSON.stringify(profile)};
106952
- const bridgeBodySha256 = ${JSON.stringify(OPENCODE_BRIDGE_BODY_SHA256)};
106953
- const bridgeDescriptionSha256 = ${JSON.stringify(OPENCODE_BRIDGE_DESCRIPTION_SHA256)};
106954
- function digest(text) { return createHash("sha256").update(text).digest("hex"); }
106955
- function canonicalSkill(skill) {
106956
- return skill?.id === "skills-cli" && skill.name === "skills-cli" && typeof skill.path === "string"
106957
- && typeof skill.content === "string" && digest(skill.content) === bridgeBodySha256
106958
- && typeof skill.description === "string" && digest(skill.description) === bridgeDescriptionSha256
106959
- && (skill.autoinvoke === undefined || skill.autoinvoke === false);
106960
- }
106961
- function refusal() {
106962
- const error = new Error("Skills verification blocked this request. Review the OpenCode Skills hook configuration, then retry.");
106963
- error.name = "SkillsHookRefusal";
106964
- error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
106965
- return error;
106966
- }
106967
- export default {
106968
- id: "local.hasna-skills-cli",
106969
- async setup(ctx) {
106970
- if (!ctx?.session?.hook || !ctx?.session?.get || !ctx?.skill?.transform || !ctx?.skill?.list || !ctx?.tool?.hook) throw refusal();
106971
- const registrations = [], pending = new Set(), sessions = new Set(), prompts = new Map();
106972
- let disposed = false;
106973
- function context(input) {
106974
- if (disposed) return Promise.reject(refusal());
106975
- return new Promise((resolve, reject) => {
106976
- // Availability failures may omit optional model context. Native skill
106977
- // consumption and explicit policy refusals always remain fail-closed.
106978
- const optional = input.delivery_phase === "context";
106979
- const chunks = []; let bytes = 0, settled = false, child, timer;
106980
- const finish = () => { clearTimeout(timer); pending.delete(cancel); };
106981
- const deny = () => { if (settled) return; settled = true; finish(); reject(refusal()); };
106982
- const unavailable = code => {
106983
- if (settled) return;
106984
- if (!optional || disposed) return deny();
106985
- settled = true; finish();
106986
- resolve({ delivered: false, text: "Skills context is unavailable [SKILLS_HOOK_" + code + "]. No Skills instructions were delivered by this hook. Ordinary work may continue. Do not perform actions that depend on unavailable Skills instructions, including previously loaded instructions. Explicit skills load, context and run still require verification. Do not substitute native, unmanaged or stale payloads." });
106987
- };
106988
- const stop = code => { unavailable(code); child?.kill("SIGKILL"); };
106989
- const cancel = () => { deny(); child?.kill("SIGKILL"); };
106990
- try {
106991
- child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
106992
- } catch { unavailable("SPAWN_FAILED"); return; }
106993
- pending.add(cancel);
106994
- timer = setTimeout(() => stop("TIMEOUT"), 15000);
106995
- child.stdout.on("data", chunk => { if (settled) return; bytes += chunk.length; if (bytes > 1048576) stop("OUTPUT_LIMIT"); else chunks.push(chunk); });
106996
- child.stdout.on("error", () => stop("TRANSPORT_FAILED"));
106997
- child.stderr.on("data", () => {});
106998
- child.stderr.on("error", () => stop("TRANSPORT_FAILED"));
106999
- child.stdin.on("error", () => stop("TRANSPORT_FAILED"));
107000
- child.on("error", () => unavailable("SPAWN_FAILED"));
107001
- child.on("close", code => {
107002
- if (settled) return;
107003
- if (disposed) return deny();
107004
- if (code !== 0) return unavailable("CHILD_FAILED");
107005
- let result;
107006
- try {
107007
- result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
107008
- } catch { unavailable("INVALID_RESPONSE"); return; }
107009
- if (!result || typeof result !== "object" || Array.isArray(result)) return unavailable("INVALID_RESPONSE");
107010
- if (result.decision === "block" || result.continue === false) return deny();
107011
- // hookContextOutput returns exactly {} for a successful empty
107012
- // context. It never attests native bridge verification.
107013
- const output = optional && Object.keys(result).length === 0
107014
- ? { hookEventName: input.hook_event_name, additionalContext: "" }
107015
- : result.hookSpecificOutput;
107016
- if (!output || typeof output !== "object" || Array.isArray(output) || output.hookEventName !== input.hook_event_name || typeof output.additionalContext !== "string") return unavailable("INVALID_RESPONSE");
107017
- if (input.hook_event_name === "BeforeTool" && result.nativeBridgeVerified !== true) return deny();
107018
- settled = true; finish();
107019
- resolve({ delivered: true, text: output.additionalContext });
107020
- });
107021
- try { child.stdin.end(JSON.stringify(input)); } catch { stop("TRANSPORT_FAILED"); }
107022
- });
107023
- }
107024
- async function input(sessionID, prompt, event) {
107025
- if (disposed) throw refusal();
107026
- let session;
107027
- try { session = await ctx.session.get({ sessionID }); } catch { throw refusal(); }
107028
- if (disposed || !session || session.id !== sessionID || typeof session.location?.directory !== "string" || (session.parentID !== undefined && typeof session.parentID !== "string") || session.parentID === sessionID) throw refusal();
107029
- return { hook_event_name: event ?? (!sessions.has(sessionID) ? session.parentID ? "SubagentStart" : "SessionStart" : "UserPromptSubmit"), delivery_phase: event ? "native" : "context", cwd: session.location.directory, session_id: sessionID, parent_session_id: session.parentID ?? null, prompt, restore: true };
107030
- }
107031
- async function dispose() {
107032
- if (disposed) return;
107033
- disposed = true;
107034
- for (const cancel of pending) cancel();
107035
- sessions.clear();
107036
- prompts.clear();
107037
- const results = await Promise.allSettled(registrations.reverse().map(registration => Promise.resolve().then(() => registration.dispose())));
107038
- if (results.some(result => result.status === "rejected")) throw refusal();
107039
- }
107040
- try {
107041
- registrations.push(await ctx.skill.transform(editor => {
107042
- for (const skill of editor.list()) if (!canonicalSkill(skill)) editor.remove(skill.id);
107043
- }));
107044
- async function verifyNativeBridge(sessionID) {
107045
- let skills;
107046
- try { skills = await ctx.skill.list(); } catch { throw refusal(); }
107047
- if (!Array.isArray(skills)) throw refusal();
107048
- const matches = skills.filter(skill => skill?.id === "skills-cli");
107049
- if (matches.length !== 1) throw refusal();
107050
- const skill = matches[0];
107051
- if (!canonicalSkill(skill)) throw refusal();
107052
- const metadata = { id: skill.id, name: skill.name, path: skill.path, contentSha256: digest(skill.content), descriptionSha256: digest(skill.description) };
107053
- await context({ ...await input(sessionID, "", "BeforeTool"), tool_input: { id: "skills-cli" }, native_skill: metadata });
107054
- }
107055
- registrations.push(await ctx.tool.hook("execute.before", async event => {
107056
- if (disposed) throw refusal();
107057
- if (event.tool !== "skill") return;
107058
- if (event.input?.id !== "skills-cli") throw refusal();
107059
- await verifyNativeBridge(event.sessionID);
107060
- }));
107061
- registrations.push(await ctx.session.hook("prompt", async event => {
107062
- if (disposed || typeof event.prompt?.text !== "string" || event.prompt.text.length > 128 * 1024) throw refusal();
107063
- if (event.prompt.skills !== undefined && (!Array.isArray(event.prompt.skills) || event.prompt.skills.some(skill => skill?.id !== "skills-cli"))) throw refusal();
107064
- // Attachments materialize immediately after this hook. Check the actual
107065
- // current registry object and on-disk bridge before native consumption.
107066
- if (event.prompt.skills?.length) await verifyNativeBridge(event.sessionID);
107067
- if (disposed) throw refusal();
107068
- const queued = prompts.get(event.sessionID) ?? [];
107069
- if (queued.reduce((size, entry) => size + entry.text.length + 1, 0) + event.prompt.text.length > 128 * 1024) throw refusal();
107070
- queued.push({ text: event.prompt.text });
107071
- prompts.set(event.sessionID, queued);
107072
- }));
107073
- registrations.push(await ctx.session.hook("context", async event => {
107074
- // Only prompt-hook input has owner-prompt provenance. Keep it transient
107075
- // until the one delivery-bearing CLI call, including an empty prompt.
107076
- // Checkpoints, synthetic user-role history and resumes never match.
107077
- const queued = [...(prompts.get(event.sessionID) ?? [])];
107078
- const result = await context(await input(event.sessionID, queued.map(entry => entry.text).join("\\n")));
107079
- if (disposed) throw refusal();
107080
- if (result.text) event.system.push({ type: "text", text: result.text });
107081
- // Remove only the prompt events this request consumed. New admission
107082
- // while the CLI was running belongs to the next model context.
107083
- const current = prompts.get(event.sessionID);
107084
- if (current) {
107085
- const remaining = current.filter(entry => !queued.includes(entry));
107086
- if (remaining.length) prompts.set(event.sessionID, remaining);
107087
- else prompts.delete(event.sessionID);
107088
- }
107089
- // Retry SessionStart on the next context after an unavailable child,
107090
- // but never replay prompt text already admitted to this model turn.
107091
- if (result.delivered) sessions.add(event.sessionID);
107092
- }));
107093
- return dispose;
107094
- } catch {
107095
- await dispose();
107096
- throw refusal();
107097
- }
107098
- },
107099
- };
107100
- `;
107101
- }
107102
-
107103
- // src/lib/agent-adapters.ts
107104
- var INTEGRATION_AGENTS = ["claude", "codex", "gemini", "opencode", "cursor", "hermes", "sumi"];
107105
- function renderAgentHookCommand(command, agent, profileId, event) {
107106
- if (typeof profileId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) || profileId.includes(".."))
107107
- throw new Error("Invalid selection profile id");
107108
- return `'${command.replace(/'/g, `'\\''`)}' hook user-prompt --agent ${agent} --selection-profile ${profileId} --event ${event}`;
107109
- }
107110
- var AGENT_ADAPTERS = {
107111
- claude: { root: ".claude/skills", config: ".claude/settings.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
107112
- codex: { root: ".codex/skills", config: ".codex/hooks.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
107113
- gemini: { root: ".gemini/skills", config: ".gemini/settings.json", events: ["BeforeAgent", "SessionStart"], promptContext: true },
107114
- opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["prompt", "context"], promptContext: true },
107115
- sumi: { root: null, config: null, events: ["prompt", "context"], promptContext: true },
107116
- hermes: { root: ".hermes/skills", config: ".hermes/config.yaml", events: ["pre_llm_call", "pre_tool_call"], promptContext: true, promptFailureMode: "open", toolFailureMode: "supervised-child-errors-block", requiresNativeTrust: true },
107117
- cursor: { root: ".cursor/skills", config: ".cursor/hooks.json", events: ["beforeSubmitPrompt", "sessionStart"], promptContext: false }
107118
- };
107119
- function renderLegacyOpenCodePlugin(command, profileId) {
107120
- return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
107121
- import { spawn } from "node:child_process";
107122
- import { randomUUID } from "node:crypto";
107123
- const command = ${JSON.stringify(command)};
107124
- const profile = ${JSON.stringify(profileId)};
107125
- function context(input) {
107126
- return new Promise((resolve, reject) => {
107127
- const child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
107128
- const chunks = []; let bytes = 0, failure;
107129
- const timer = setTimeout(() => { failure = new Error("Skills prompt hook timed out"); child.kill("SIGKILL"); }, 15000);
107130
- child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > 1048576) { failure = new Error("Skills prompt hook output exceeded its limit"); child.kill("SIGKILL"); } else chunks.push(chunk); });
107131
- child.stderr.on("data", () => {});
107132
- child.stdin.on("error", () => {});
107133
- child.on("error", error => { clearTimeout(timer); reject(error); });
107134
- child.on("close", code => {
107135
- clearTimeout(timer);
107136
- if (failure || code !== 0) return reject(failure ?? new Error("Skills prompt hook failed"));
107137
- try {
107138
- const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
107139
- if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("Invalid Skills hook response");
107140
- if (result.decision === "block" || result.continue === false) throw new Error(result.reason ?? result.stopReason ?? "Skills prompt hook refused");
107141
- if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw new Error("Invalid Skills hook response");
107142
- const text = result.hookSpecificOutput?.additionalContext ?? "";
107143
- if (typeof text !== "string") throw new Error("Invalid Skills prompt context");
107144
- resolve(text);
107145
- } catch (error) { reject(error); }
107146
- });
107147
- child.stdin.end(JSON.stringify(input));
107148
- });
107149
- }
107150
- export default async ({ directory }) => {
107151
- const sessions = new Set();
107152
- return {
107153
- "chat.message": async (input, output) => {
107154
- const parts = output.parts.filter(part => part.type === "text" && typeof part.text === "string");
107155
- const first = !sessions.has(input.sessionID);
107156
- const text = await context({ hook_event_name: first ? "SessionStart" : "UserPromptSubmit", cwd: directory, session_id: input.sessionID, prompt: parts.map(part => part.text).join("\\n") });
107157
- if (text && parts.length) parts[0].text += "\\n\\n" + text;
107158
- else if (text) output.parts.push({ id: "prt_" + randomUUID().replaceAll("-", ""), sessionID: input.sessionID, messageID: output.message.id, type: "text", text, synthetic: true });
107159
- if (sessions.size >= 1000) sessions.clear();
107160
- sessions.add(input.sessionID);
107161
- },
107162
- "tool.execute.before": async (input, output) => {
107163
- if (input.tool === "skill" && output.args?.name !== "skills-cli") throw new Error("Load instructions through the Skills CLI bridge");
107164
- },
107165
- };
107166
- };
107167
- `;
107168
- }
107169
- function isManagedOpenCodePlugin(text2, command, profileId) {
107170
- return text2 === renderOpenCodePlugin(command, profileId) || text2 === renderLegacyOpenCodePlugin(command, profileId);
107171
- }
107172
-
107173
107392
  // src/lib/agent-codex.ts
107174
107393
  import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
107175
107394
  var CODEX_SKILL_CONFIG_SECTIONS = /^\[\[skills\.config\]\][^\n]*(?:\n(?!\s*\[)[^\n]*)*/gm;
@@ -111594,7 +111813,10 @@ function configuredRoots(home, explicitHome) {
111594
111813
  if (recorded && (!executable2 || executable2.target !== recorded.target || onPath && onPath.target !== recorded.target) || !executable2 && (binding.builtinNames?.length || binding.agent === "gemini" && binding.sources.some((source) => basename17(source.path) === "package.json"))) {
111595
111814
  throw new Error("Native discovery runtime target is unresolved or changed");
111596
111815
  }
111597
- roots.push(...resolveAgentDiscoveryExclusionRoots({ home, agent: binding.agent, canonical: physicalPath, retained: binding, executable: executable2, narrowed }));
111816
+ const managedCommand = current.commands?.[agent], managedProfile = current.profiles?.[agent];
111817
+ const supervisor = current.supervisors?.[agent];
111818
+ const managedHook = (binding.agent === "gemini" || binding.agent === "cursor") && typeof managedCommand === "string" && typeof managedProfile === "string" ? { command: managedCommand, profile: managedProfile } : binding.agent === "hermes" && typeof managedCommand === "string" && typeof managedProfile === "string" && supervisor && typeof supervisor === "object" && typeof supervisor.path === "string" && typeof supervisor.runtime === "string" && typeof supervisor.sha256 === "string" ? { command: managedCommand, profile: managedProfile, supervisor: { path: supervisor.path, runtime: supervisor.runtime, sha256: supervisor.sha256 } } : undefined;
111819
+ roots.push(...resolveAgentDiscoveryExclusionRoots({ home, agent: binding.agent, canonical: physicalPath, retained: binding, executable: executable2, narrowed, ...managedHook ? { managedHook } : {} }));
111598
111820
  }
111599
111821
  };
111600
111822
  if (homePolicy)