opencode-dynamic-skills 1.1.0 → 1.3.0

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/index.js CHANGED
@@ -15889,7 +15889,7 @@ function toolName(skillPath) {
15889
15889
 
15890
15890
  // src/lib/SkillFs.ts
15891
15891
  import { join } from "path";
15892
- import { existsSync } from "fs";
15892
+ import { existsSync, statSync } from "fs";
15893
15893
 
15894
15894
  // node_modules/mime/dist/types/other.js
15895
15895
  var types = {
@@ -17075,13 +17075,24 @@ var Mime_default = Mime;
17075
17075
  var src_default = new Mime_default(standard_default, other_default)._freeze();
17076
17076
 
17077
17077
  // src/lib/SkillFs.ts
17078
+ function isRegularFile(filePath) {
17079
+ try {
17080
+ return statSync(filePath).isFile();
17081
+ } catch {
17082
+ return false;
17083
+ }
17084
+ }
17078
17085
  var readSkillFile = async (path) => {
17079
17086
  const file2 = Bun.file(path);
17080
17087
  return file2.text();
17081
17088
  };
17082
17089
  var listSkillFiles = (skillPath, subdirectory) => {
17083
17090
  const glob = new Bun.Glob(join(subdirectory, "**", "*"));
17084
- return Array.from(glob.scanSync({ cwd: skillPath, absolute: true }));
17091
+ return Array.from(glob.scanSync({ cwd: skillPath, absolute: true })).filter(isRegularFile);
17092
+ };
17093
+ var listAllSkillFiles = (skillPath) => {
17094
+ const glob = new Bun.Glob("**/*");
17095
+ return Array.from(glob.scanSync({ cwd: skillPath, absolute: true })).filter(isRegularFile).filter((filePath) => !filePath.endsWith("/SKILL.md") && !filePath.endsWith("\\SKILL.md"));
17085
17096
  };
17086
17097
  var findSkillPaths = async (basePath) => {
17087
17098
  const glob = new Bun.Glob("**/SKILL.md");
@@ -17682,6 +17693,7 @@ async function parseSkill(args) {
17682
17693
  const scriptPaths = listSkillFiles(skillFullPath, "scripts");
17683
17694
  const referencePaths = listSkillFiles(skillFullPath, "references");
17684
17695
  const assetPaths = listSkillFiles(skillFullPath, "assets");
17696
+ const indexedFilePaths = listAllSkillFiles(skillFullPath);
17685
17697
  return {
17686
17698
  allowedTools: frontmatter.data["allowed-tools"],
17687
17699
  compatibility: frontmatter.data.compatibility,
@@ -17693,6 +17705,7 @@ async function parseSkill(args) {
17693
17705
  metadata: frontmatter.data.metadata,
17694
17706
  name: frontmatter.data.name,
17695
17707
  path: args.skillPath,
17708
+ files: createSkillResourceMap(skillFullPath, indexedFilePaths),
17696
17709
  scripts: createSkillResourceMap(skillFullPath, scriptPaths),
17697
17710
  references: createSkillResourceMap(skillFullPath, referencePaths),
17698
17711
  assets: createSkillResourceMap(skillFullPath, assetPaths)
@@ -17735,6 +17748,9 @@ function createSkillFinder(provider) {
17735
17748
  // src/tools/SkillRecommender.ts
17736
17749
  var DEFAULT_RECOMMENDATION_LIMIT = 5;
17737
17750
  var MIN_TERM_LENGTH = 3;
17751
+ function normalizeSkillSelector(selector) {
17752
+ return selector.trim().toLowerCase().replace(/[/-]/g, "_");
17753
+ }
17738
17754
  function getRecommendationReason(nameMatches, descMatches) {
17739
17755
  const reasons = [];
17740
17756
  if (nameMatches > 0) {
@@ -17748,46 +17764,190 @@ function getRecommendationReason(nameMatches, descMatches) {
17748
17764
  }
17749
17765
  return reasons.join("; ");
17750
17766
  }
17751
- function createSkillRecommender(provider) {
17767
+ function formatResults(provider, task, recommendations) {
17768
+ return {
17769
+ mode: "recommend",
17770
+ query: task,
17771
+ skills: recommendations.map(({ name, description }) => ({
17772
+ name,
17773
+ description
17774
+ })),
17775
+ recommendations,
17776
+ guidance: recommendations.length > 0 ? "Use skill_use to load one of the recommended skills if you want to apply it." : "No strong skill recommendation was found. Try skill_find with a narrower query.",
17777
+ summary: {
17778
+ total: provider.controller.skills.length,
17779
+ matches: recommendations.length,
17780
+ feedback: recommendations.length > 0 ? `Recommended ${recommendations.length} skill(s) for: **${task}**` : `No strong recommendations found for: **${task}**`
17781
+ },
17782
+ debug: provider.debug
17783
+ };
17784
+ }
17785
+ function getHeuristicRecommendations(provider, task, limit) {
17786
+ const parsedQuery = parseQuery(task);
17787
+ const rankingTerms = parsedQuery.include.filter((term) => term.length >= MIN_TERM_LENGTH);
17788
+ const rankedMatches = provider.controller.skills.map((skill) => rankSkill(skill, rankingTerms)).filter((ranking) => ranking.totalScore > 0).sort((left, right) => {
17789
+ if (right.totalScore !== left.totalScore) {
17790
+ return right.totalScore - left.totalScore;
17791
+ }
17792
+ if (right.nameMatches !== left.nameMatches) {
17793
+ return right.nameMatches - left.nameMatches;
17794
+ }
17795
+ return left.skill.name.localeCompare(right.skill.name);
17796
+ });
17797
+ return rankedMatches.slice(0, limit).map((skill) => ({
17798
+ name: skill.skill.toolName,
17799
+ description: skill.skill.description,
17800
+ score: skill.totalScore,
17801
+ reason: getRecommendationReason(skill.nameMatches, skill.descMatches)
17802
+ }));
17803
+ }
17804
+ function buildModelRecommendationPrompt(args) {
17805
+ const catalog = args.skills.map((skill) => `- ${skill.toolName}: ${skill.description} (directory=${skill.name}, aliases=/${skill.name})`).join(`
17806
+ `);
17807
+ return [
17808
+ `User request: ${args.task}`,
17809
+ `Maximum recommendations: ${args.limit}`,
17810
+ "Available skills:",
17811
+ catalog,
17812
+ "",
17813
+ "Return strict JSON only. Do not include markdown fences unless required by the model."
17814
+ ].join(`
17815
+ `);
17816
+ }
17817
+ function extractJsonPayload(text) {
17818
+ const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
17819
+ if (fencedMatch?.[1]) {
17820
+ return fencedMatch[1].trim();
17821
+ }
17822
+ const firstBrace = text.indexOf("{");
17823
+ const lastBrace = text.lastIndexOf("}");
17824
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
17825
+ return text.slice(firstBrace, lastBrace + 1);
17826
+ }
17827
+ throw new Error("Model response did not contain a JSON object");
17828
+ }
17829
+ function parseModelRecommendations(text) {
17830
+ const payload = JSON.parse(extractJsonPayload(text));
17831
+ if (!Array.isArray(payload.recommendations)) {
17832
+ throw new Error("Model response is missing a recommendations array");
17833
+ }
17834
+ return payload.recommendations.filter((recommendation) => recommendation.name?.trim()).map((recommendation) => ({
17835
+ name: recommendation.name.trim(),
17836
+ reason: recommendation.reason?.trim()
17837
+ }));
17838
+ }
17839
+ function getAssistantText(messages) {
17840
+ const assistantMessage = [...messages].reverse().find((message) => message.info.role === "assistant");
17841
+ if (!assistantMessage) {
17842
+ throw new Error("Model session did not return an assistant message");
17843
+ }
17844
+ const text = assistantMessage.parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
17845
+ `).trim();
17846
+ if (!text) {
17847
+ throw new Error("Model session did not return assistant text");
17848
+ }
17849
+ return text;
17850
+ }
17851
+ function mapModelRecommendationsToSkills(skills, recommendations, limit) {
17852
+ const skillByName = new Map;
17853
+ for (const skill of skills) {
17854
+ skillByName.set(normalizeSkillSelector(skill.toolName), skill);
17855
+ skillByName.set(normalizeSkillSelector(skill.name), skill);
17856
+ }
17857
+ const results = [];
17858
+ const seen = new Set;
17859
+ for (const [index, recommendation] of recommendations.entries()) {
17860
+ const skill = skillByName.get(normalizeSkillSelector(recommendation.name));
17861
+ if (!skill || seen.has(skill.toolName)) {
17862
+ continue;
17863
+ }
17864
+ seen.add(skill.toolName);
17865
+ results.push({
17866
+ name: skill.toolName,
17867
+ description: skill.description,
17868
+ score: Math.max(limit - index, 1),
17869
+ reason: recommendation.reason || "Recommended by the configured skill_recommend model"
17870
+ });
17871
+ if (results.length >= limit) {
17872
+ break;
17873
+ }
17874
+ }
17875
+ return results;
17876
+ }
17877
+ function shouldUseModel(config2, client) {
17878
+ return config2.strategy === "model" && Boolean(client) && config2.model.includes("/");
17879
+ }
17880
+ function parseConfiguredModel(model) {
17881
+ const trimmedModel = model.trim();
17882
+ const separatorIndex = trimmedModel.indexOf("/");
17883
+ if (separatorIndex <= 0 || separatorIndex === trimmedModel.length - 1) {
17884
+ throw new Error(`Invalid skillRecommend.model: ${model}`);
17885
+ }
17886
+ return {
17887
+ providerID: trimmedModel.slice(0, separatorIndex).trim(),
17888
+ modelID: trimmedModel.slice(separatorIndex + 1).trim()
17889
+ };
17890
+ }
17891
+ function unwrapClientData(response, action) {
17892
+ if (response.error) {
17893
+ const message = response.error instanceof Error ? response.error.message : String(response.error);
17894
+ throw new Error(`${action} failed: ${message}`);
17895
+ }
17896
+ if (response.data === undefined) {
17897
+ throw new Error(`${action} failed: missing response data`);
17898
+ }
17899
+ return response.data;
17900
+ }
17901
+ function getModelSessionClient(client) {
17902
+ return client;
17903
+ }
17904
+ async function getModelRecommendations(provider, options2, task, limit) {
17905
+ if (!options2.client) {
17906
+ throw new Error("Model-based recommendation requires an OpenCode client");
17907
+ }
17908
+ const client = getModelSessionClient(options2.client);
17909
+ const selectedModel = parseConfiguredModel(options2.config.model);
17910
+ const session = unwrapClientData(await client.session.create(), "session.create");
17911
+ try {
17912
+ unwrapClientData(await client.session.chat(session.id, {
17913
+ providerID: selectedModel.providerID,
17914
+ modelID: selectedModel.modelID,
17915
+ parts: [
17916
+ {
17917
+ type: "text",
17918
+ text: buildModelRecommendationPrompt({
17919
+ task,
17920
+ limit,
17921
+ skills: provider.controller.skills
17922
+ })
17923
+ }
17924
+ ],
17925
+ system: options2.config.systemPrompt,
17926
+ tools: {}
17927
+ }), "session.chat");
17928
+ const messages = unwrapClientData(await client.session.messages(session.id), "session.messages");
17929
+ const modelOutput = getAssistantText(messages);
17930
+ const parsedRecommendations = parseModelRecommendations(modelOutput);
17931
+ return mapModelRecommendationsToSkills(provider.controller.skills, parsedRecommendations, limit);
17932
+ } finally {
17933
+ await client.session.delete(session.id).catch(() => {
17934
+ return;
17935
+ });
17936
+ }
17937
+ }
17938
+ function createSkillRecommender(provider, options2) {
17752
17939
  return async (args) => {
17753
17940
  await provider.controller.ready.whenReady();
17754
17941
  const limit = Math.max(1, Math.min(args.limit ?? DEFAULT_RECOMMENDATION_LIMIT, 10));
17755
- const parsedQuery = parseQuery(args.task);
17756
- const rankingTerms = parsedQuery.include.filter((term) => term.length >= MIN_TERM_LENGTH);
17757
- const rankedMatches = provider.controller.skills.map((skill) => rankSkill(skill, rankingTerms)).filter((ranking) => ranking.totalScore > 0).sort((left, right) => {
17758
- if (right.totalScore !== left.totalScore) {
17759
- return right.totalScore - left.totalScore;
17760
- }
17761
- if (right.nameMatches !== left.nameMatches) {
17762
- return right.nameMatches - left.nameMatches;
17942
+ if (shouldUseModel(options2.config, options2.client)) {
17943
+ try {
17944
+ const modelRecommendations = await getModelRecommendations(provider, options2, args.task, limit);
17945
+ return formatResults(provider, args.task, modelRecommendations);
17946
+ } catch (error45) {
17947
+ provider.logger.warn("Model-based skill_recommend failed; falling back to heuristic ranking.", error45);
17763
17948
  }
17764
- return left.skill.name.localeCompare(right.skill.name);
17765
- });
17766
- const topMatches = rankedMatches.slice(0, limit);
17767
- const recommendations = topMatches.map((skill) => {
17768
- return {
17769
- name: skill.skill.toolName,
17770
- description: skill.skill.description,
17771
- score: skill.totalScore,
17772
- reason: getRecommendationReason(skill.nameMatches, skill.descMatches)
17773
- };
17774
- });
17775
- return {
17776
- mode: "recommend",
17777
- query: args.task,
17778
- skills: recommendations.map(({ name, description }) => ({
17779
- name,
17780
- description
17781
- })),
17782
- recommendations,
17783
- guidance: recommendations.length > 0 ? "Use skill_use to load one of the recommended skills if you want to apply it." : "No strong skill recommendation was found. Try skill_find with a narrower query.",
17784
- summary: {
17785
- total: provider.controller.skills.length,
17786
- matches: recommendations.length,
17787
- feedback: recommendations.length > 0 ? `Recommended ${recommendations.length} skill(s) for: **${args.task}**` : `No strong recommendations found for: **${args.task}**`
17788
- },
17789
- debug: provider.debug
17790
- };
17949
+ }
17950
+ return formatResults(provider, args.task, getHeuristicRecommendations(provider, args.task, limit));
17791
17951
  };
17792
17952
  }
17793
17953
 
@@ -17809,7 +17969,7 @@ function normalizeSkillResourcePath(inputPath) {
17809
17969
  }
17810
17970
  function getSkillResources(skill) {
17811
17971
  const resources = new Map;
17812
- for (const resourceMap of [skill.references, skill.scripts, skill.assets]) {
17972
+ for (const resourceMap of [skill.files, skill.references, skill.scripts, skill.assets]) {
17813
17973
  for (const [relativePath, resource] of resourceMap.entries()) {
17814
17974
  resources.set(normalizeSkillResourcePath(relativePath), resource);
17815
17975
  }
@@ -17899,6 +18059,9 @@ function createSkillResourceReader(provider) {
17899
18059
  };
17900
18060
  }
17901
18061
 
18062
+ // src/lib/formatLoadedSkill.ts
18063
+ import path3 from "path";
18064
+
17902
18065
  // src/lib/SkillLinks.ts
17903
18066
  import path2 from "path";
17904
18067
  var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
@@ -17952,11 +18115,116 @@ function extractSkillLinks(content) {
17952
18115
  }
17953
18116
 
17954
18117
  // src/lib/formatLoadedSkill.ts
17955
- function formatLoadedSkill(args) {
17956
- const linkedResources = extractSkillLinks(args.skill.content);
18118
+ var MAX_REFERENCE_DEPTH = 3;
18119
+ function normalizeSkillResourcePath2(inputPath) {
18120
+ const unixPath = inputPath.replace(/\\/g, "/").trim();
18121
+ if (unixPath.length === 0 || unixPath.startsWith("/")) {
18122
+ return null;
18123
+ }
18124
+ const normalizedPath = path3.posix.normalize(unixPath).replace(/^\.\//, "");
18125
+ if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
18126
+ return null;
18127
+ }
18128
+ return normalizedPath;
18129
+ }
18130
+ function buildSkillResourceIndex(skill) {
18131
+ const resources = new Map;
18132
+ const basenameIndex = new Map;
18133
+ for (const resourceMap of [skill.files, skill.references, skill.scripts, skill.assets]) {
18134
+ for (const [relativePath, resource] of resourceMap.entries()) {
18135
+ const normalizedPath = normalizeSkillResourcePath2(relativePath);
18136
+ if (!normalizedPath) {
18137
+ continue;
18138
+ }
18139
+ const resolvedResource = {
18140
+ relativePath: normalizedPath,
18141
+ absolutePath: resource.absolutePath,
18142
+ mimeType: resource.mimeType
18143
+ };
18144
+ resources.set(normalizedPath, resolvedResource);
18145
+ const basename2 = path3.posix.basename(normalizedPath);
18146
+ const basenameMatches = basenameIndex.get(basename2) ?? [];
18147
+ basenameMatches.push(resolvedResource);
18148
+ basenameIndex.set(basename2, basenameMatches);
18149
+ }
18150
+ }
18151
+ return {
18152
+ resources,
18153
+ basenameIndex
18154
+ };
18155
+ }
18156
+ function resolveReferencedSkillResource(resourceIndex, resourcePath) {
18157
+ const normalizedPath = normalizeSkillResourcePath2(resourcePath);
18158
+ if (!normalizedPath) {
18159
+ return null;
18160
+ }
18161
+ const { resources, basenameIndex } = resourceIndex;
18162
+ const exactMatch = resources.get(normalizedPath);
18163
+ if (exactMatch) {
18164
+ return exactMatch;
18165
+ }
18166
+ const basenameMatches = basenameIndex.get(path3.posix.basename(normalizedPath)) ?? [];
18167
+ return basenameMatches.length === 1 ? basenameMatches[0] : null;
18168
+ }
18169
+ async function collectReferencedSkillFiles(args) {
18170
+ const depth = args.depth ?? 0;
18171
+ const visited = args.visited ?? new Set;
18172
+ if (depth >= MAX_REFERENCE_DEPTH) {
18173
+ return [];
18174
+ }
18175
+ const referencedFiles = [];
18176
+ for (const link of extractSkillLinks(args.content)) {
18177
+ const resolvedResource = resolveReferencedSkillResource(args.resourceIndex, link.resourcePath);
18178
+ if (!resolvedResource || visited.has(resolvedResource.relativePath)) {
18179
+ continue;
18180
+ }
18181
+ visited.add(resolvedResource.relativePath);
18182
+ const fileContent = await readSkillFile(resolvedResource.absolutePath);
18183
+ referencedFiles.push({
18184
+ ...resolvedResource,
18185
+ content: fileContent
18186
+ });
18187
+ const nestedReferences = await collectReferencedSkillFiles({
18188
+ skill: args.skill,
18189
+ content: fileContent,
18190
+ resourceIndex: args.resourceIndex,
18191
+ depth: depth + 1,
18192
+ visited
18193
+ });
18194
+ referencedFiles.push(...nestedReferences);
18195
+ }
18196
+ return referencedFiles;
18197
+ }
18198
+ function formatExpandedReferences(references) {
18199
+ if (references.length === 0) {
18200
+ return [];
18201
+ }
18202
+ const output = ["", "### Expanded referenced skill files", ""];
18203
+ for (const reference of references) {
18204
+ output.push(`#### ${reference.relativePath}`);
18205
+ output.push(`**Absolute path**: ${reference.absolutePath}`);
18206
+ output.push(`**MIME type**: ${reference.mimeType}`);
18207
+ output.push("");
18208
+ output.push("````text");
18209
+ output.push(reference.content.trim());
18210
+ output.push("````");
18211
+ output.push("");
18212
+ }
18213
+ return output;
18214
+ }
18215
+ async function formatLoadedSkill(args) {
18216
+ const invocationName = args.invocationName ?? args.skill.name;
18217
+ const normalizedSkillRoot = `${args.skill.fullPath.replace(/\\/g, "/")}/`;
18218
+ const resourceIndex = buildSkillResourceIndex(args.skill);
18219
+ const referencedFiles = await collectReferencedSkillFiles({
18220
+ skill: args.skill,
18221
+ content: args.skill.content,
18222
+ resourceIndex
18223
+ });
17957
18224
  const output = [
17958
- `## Skill: ${args.skill.name}`,
18225
+ `# /${invocationName} Command`,
17959
18226
  "",
18227
+ `**Description**: ${args.skill.description}`,
17960
18228
  `**Skill identifier**: ${args.skill.toolName}`,
17961
18229
  `**Base directory**: ${args.skill.fullPath}`
17962
18230
  ];
@@ -17966,21 +18234,28 @@ function formatLoadedSkill(args) {
17966
18234
  if (args.userMessage?.trim()) {
17967
18235
  output.push(`**User request**: ${args.userMessage.trim()}`);
17968
18236
  }
17969
- output.push("", "Relative file references in this skill resolve from the skill root directory.");
17970
- output.push("Use skill_resource with the exact root-relative path when you need a linked file.");
17971
- if (linkedResources.length > 0) {
17972
- output.push("", "### Linked files", "");
17973
- for (const link of linkedResources) {
17974
- output.push(`- ${link.originalPath} -> ${link.resourcePath}`);
18237
+ output.push("", "---", "", "## Command Instructions", "");
18238
+ output.push("<skill-instruction>");
18239
+ output.push(`Base directory for this skill: ${normalizedSkillRoot}`);
18240
+ output.push("File references (@path), markdown links, and relative file mentions in this skill resolve from the skill root.");
18241
+ output.push("Use skill_resource with the exact root-relative path if you need additional files beyond the expanded references below.");
18242
+ output.push("", args.skill.content, "</skill-instruction>");
18243
+ if (args.skill.files.size > 0) {
18244
+ output.push("", "### Available files", "");
18245
+ for (const relativePath of Array.from(args.skill.files.keys()).sort()) {
18246
+ output.push(`- ${relativePath}`);
17975
18247
  }
17976
18248
  }
17977
- output.push("", args.skill.content);
18249
+ output.push(...formatExpandedReferences(referencedFiles));
18250
+ if (args.userMessage?.trim()) {
18251
+ output.push("", "<user-request>", args.userMessage.trim(), "</user-request>");
18252
+ }
17978
18253
  return output.join(`
17979
18254
  `);
17980
18255
  }
17981
18256
 
17982
18257
  // src/tools/Skill.ts
17983
- function normalizeSkillSelector(selector) {
18258
+ function normalizeSkillSelector2(selector) {
17984
18259
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
17985
18260
  }
17986
18261
  function findSkill(registry2, selector) {
@@ -17988,9 +18263,9 @@ function findSkill(registry2, selector) {
17988
18263
  if (directMatch) {
17989
18264
  return directMatch;
17990
18265
  }
17991
- const normalizedSelector = normalizeSkillSelector(selector);
18266
+ const normalizedSelector = normalizeSkillSelector2(selector);
17992
18267
  for (const skill of registry2.controller.skills) {
17993
- if (normalizeSkillSelector(skill.name) === normalizedSelector || normalizeSkillSelector(skill.toolName) === normalizedSelector) {
18268
+ if (normalizeSkillSelector2(skill.name) === normalizedSelector || normalizeSkillSelector2(skill.toolName) === normalizedSelector) {
17994
18269
  return skill;
17995
18270
  }
17996
18271
  }
@@ -18010,7 +18285,7 @@ function createSkillTool(registry2) {
18010
18285
  const available = registry2.controller.skills.map((entry) => entry.name).join(", ");
18011
18286
  throw new Error(`Skill "${args.name}" not found. Available: ${available || "none"}`);
18012
18287
  }
18013
- return formatLoadedSkill({
18288
+ return await formatLoadedSkill({
18014
18289
  skill,
18015
18290
  invocationName: args.name.replace(/^\//, ""),
18016
18291
  userMessage: args.user_message
@@ -18020,7 +18295,7 @@ function createSkillTool(registry2) {
18020
18295
  }
18021
18296
 
18022
18297
  // src/tools/SkillUser.ts
18023
- function normalizeSkillSelector2(selector) {
18298
+ function normalizeSkillSelector3(selector) {
18024
18299
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
18025
18300
  }
18026
18301
  function createSkillLoader(provider) {
@@ -18030,8 +18305,8 @@ function createSkillLoader(provider) {
18030
18305
  const loaded = [];
18031
18306
  const notFound = [];
18032
18307
  for (const name of skillNames) {
18033
- const normalizedName = normalizeSkillSelector2(name);
18034
- const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector2(candidate.name) === normalizedName || normalizeSkillSelector2(candidate.toolName) === normalizedName);
18308
+ const normalizedName = normalizeSkillSelector3(name);
18309
+ const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector3(candidate.name) === normalizedName || normalizeSkillSelector3(candidate.toolName) === normalizedName);
18035
18310
  if (skill) {
18036
18311
  loaded.push(skill);
18037
18312
  } else {
@@ -18047,7 +18322,7 @@ function createSkillLoader(provider) {
18047
18322
  }
18048
18323
 
18049
18324
  // src/api.ts
18050
- var createApi = async (config2) => {
18325
+ var createApi = async (config2, client) => {
18051
18326
  const logger = createLogger(config2);
18052
18327
  const registry2 = await createSkillRegistry(config2, logger);
18053
18328
  return {
@@ -18055,7 +18330,10 @@ var createApi = async (config2) => {
18055
18330
  logger,
18056
18331
  config: config2,
18057
18332
  findSkills: createSkillFinder(registry2),
18058
- recommendSkills: createSkillRecommender(registry2),
18333
+ recommendSkills: createSkillRecommender(registry2, {
18334
+ client,
18335
+ config: config2.skillRecommend
18336
+ }),
18059
18337
  readResource: createSkillResourceReader(registry2),
18060
18338
  loadSkill: createSkillLoader(registry2),
18061
18339
  skillTool: createSkillTool(registry2)
@@ -18063,7 +18341,7 @@ var createApi = async (config2) => {
18063
18341
  };
18064
18342
 
18065
18343
  // node_modules/bunfig/dist/index.js
18066
- import { existsSync as existsSync2, statSync } from "fs";
18344
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
18067
18345
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
18068
18346
  import { homedir as homedir2 } from "os";
18069
18347
  import { dirname as dirname3, resolve as resolve7 } from "path";
@@ -18185,7 +18463,7 @@ class ConfigCache {
18185
18463
  if (!existsSync2(configPath)) {
18186
18464
  return true;
18187
18465
  }
18188
- const stats = statSync(configPath);
18466
+ const stats = statSync2(configPath);
18189
18467
  return stats.mtime > cachedTimestamp;
18190
18468
  } catch {
18191
18469
  return true;
@@ -18204,7 +18482,7 @@ class ConfigCache {
18204
18482
  }
18205
18483
  setWithFileCheck(configName, value, configPath, customTtl) {
18206
18484
  try {
18207
- const stats = existsSync2(configPath) ? statSync(configPath) : null;
18485
+ const stats = existsSync2(configPath) ? statSync2(configPath) : null;
18208
18486
  const fileTimestamp = stats ? stats.mtime : new Date;
18209
18487
  this.set(configName, { value, fileTimestamp }, configPath, customTtl);
18210
18488
  } catch {
@@ -18524,10 +18802,10 @@ async function loadConfig({
18524
18802
  var defaultConfigDir = resolve(process3.cwd(), "config");
18525
18803
  var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
18526
18804
  function getProjectRoot(filePath, options2 = {}) {
18527
- let path3 = process2.cwd();
18528
- while (path3.includes("storage"))
18529
- path3 = resolve2(path3, "..");
18530
- const finalPath = resolve2(path3, filePath || "");
18805
+ let path4 = process2.cwd();
18806
+ while (path4.includes("storage"))
18807
+ path4 = resolve2(path4, "..");
18808
+ const finalPath = resolve2(path4, filePath || "");
18531
18809
  if (options2?.relative)
18532
18810
  return relative3(process2.cwd(), finalPath);
18533
18811
  return finalPath;
@@ -19974,10 +20252,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
19974
20252
  return config3;
19975
20253
  const envPrefix = name.toUpperCase().replace(/-/g, "_");
19976
20254
  const result = { ...config3 };
19977
- function processObject(obj, path3 = []) {
20255
+ function processObject(obj, path4 = []) {
19978
20256
  const result2 = { ...obj };
19979
20257
  for (const [key, value] of Object.entries(obj)) {
19980
- const envPath = [...path3, key];
20258
+ const envPath = [...path4, key];
19981
20259
  const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
19982
20260
  const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
19983
20261
  const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
@@ -20159,10 +20437,10 @@ async function loadConfig3({
20159
20437
  var defaultConfigDir2 = resolve3(process6.cwd(), "config");
20160
20438
  var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
20161
20439
  function getProjectRoot2(filePath, options2 = {}) {
20162
- let path3 = process7.cwd();
20163
- while (path3.includes("storage"))
20164
- path3 = resolve4(path3, "..");
20165
- const finalPath = resolve4(path3, filePath || "");
20440
+ let path4 = process7.cwd();
20441
+ while (path4.includes("storage"))
20442
+ path4 = resolve4(path4, "..");
20443
+ const finalPath = resolve4(path4, filePath || "");
20166
20444
  if (options2?.relative)
20167
20445
  return relative2(process7.cwd(), finalPath);
20168
20446
  return finalPath;
@@ -21599,10 +21877,10 @@ class EnvVarError extends BunfigError {
21599
21877
 
21600
21878
  class FileSystemError extends BunfigError {
21601
21879
  code = "FILE_SYSTEM_ERROR";
21602
- constructor(operation, path3, cause) {
21603
- super(`File system ${operation} failed for "${path3}": ${cause.message}`, {
21880
+ constructor(operation, path4, cause) {
21881
+ super(`File system ${operation} failed for "${path4}": ${cause.message}`, {
21604
21882
  operation,
21605
- path: path3,
21883
+ path: path4,
21606
21884
  originalError: cause.name,
21607
21885
  originalMessage: cause.message
21608
21886
  });
@@ -21675,8 +21953,8 @@ var ErrorFactory = {
21675
21953
  envVar(envKey, envValue, expectedType, configName) {
21676
21954
  return new EnvVarError(envKey, envValue, expectedType, configName);
21677
21955
  },
21678
- fileSystem(operation, path3, cause) {
21679
- return new FileSystemError(operation, path3, cause);
21956
+ fileSystem(operation, path4, cause) {
21957
+ return new FileSystemError(operation, path4, cause);
21680
21958
  },
21681
21959
  typeGeneration(configDir, outputPath, cause) {
21682
21960
  return new TypeGenerationError(configDir, outputPath, cause);
@@ -21812,9 +22090,9 @@ class EnvProcessor {
21812
22090
  }
21813
22091
  return key.replace(/([A-Z])/g, "_$1").toUpperCase();
21814
22092
  }
21815
- processObject(obj, path3, envPrefix, options2) {
22093
+ processObject(obj, path4, envPrefix, options2) {
21816
22094
  for (const [key, value] of Object.entries(obj)) {
21817
- const envPath = [...path3, key];
22095
+ const envPath = [...path4, key];
21818
22096
  const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
21819
22097
  const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
21820
22098
  const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
@@ -21899,9 +22177,9 @@ class EnvProcessor {
21899
22177
  return this.formatAsText(envVars, configName);
21900
22178
  }
21901
22179
  }
21902
- extractEnvVarInfo(obj, path3, prefix, envVars) {
22180
+ extractEnvVarInfo(obj, path4, prefix, envVars) {
21903
22181
  for (const [key, value] of Object.entries(obj)) {
21904
- const envPath = [...path3, key];
22182
+ const envPath = [...path4, key];
21905
22183
  const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
21906
22184
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
21907
22185
  this.extractEnvVarInfo(value, envPath, prefix, envVars);
@@ -22307,8 +22585,8 @@ class ConfigFileLoader {
22307
22585
  }
22308
22586
  async getFileModificationTime(filePath) {
22309
22587
  try {
22310
- const { statSync: statSync2 } = await import("fs");
22311
- const stats = statSync2(filePath);
22588
+ const { statSync: statSync22 } = await import("fs");
22589
+ const stats = statSync22(filePath);
22312
22590
  return stats.mtime;
22313
22591
  } catch {
22314
22592
  return null;
@@ -22316,15 +22594,15 @@ class ConfigFileLoader {
22316
22594
  }
22317
22595
  async preloadConfigurations(configPaths, options2 = {}) {
22318
22596
  const preloaded = new Map;
22319
- await Promise.allSettled(configPaths.map(async (path3) => {
22597
+ await Promise.allSettled(configPaths.map(async (path4) => {
22320
22598
  try {
22321
- const result = await this.loadFromPath(path3, {}, options2);
22599
+ const result = await this.loadFromPath(path4, {}, options2);
22322
22600
  if (result) {
22323
- preloaded.set(path3, result.config);
22601
+ preloaded.set(path4, result.config);
22324
22602
  }
22325
22603
  } catch (error45) {
22326
22604
  if (options2.verbose) {
22327
- console.warn(`Failed to preload ${path3}:`, error45);
22605
+ console.warn(`Failed to preload ${path4}:`, error45);
22328
22606
  }
22329
22607
  }
22330
22608
  }));
@@ -22403,13 +22681,13 @@ class ConfigValidator {
22403
22681
  warnings
22404
22682
  };
22405
22683
  }
22406
- validateObjectAgainstSchema(value, schema, path3, errors3, warnings, options2) {
22684
+ validateObjectAgainstSchema(value, schema, path4, errors3, warnings, options2) {
22407
22685
  if (options2.validateTypes && schema.type) {
22408
22686
  const actualType = Array.isArray(value) ? "array" : typeof value;
22409
22687
  const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
22410
22688
  if (!expectedTypes.includes(actualType)) {
22411
22689
  errors3.push({
22412
- path: path3,
22690
+ path: path4,
22413
22691
  message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
22414
22692
  expected: expectedTypes.join(" or "),
22415
22693
  actual: actualType,
@@ -22421,7 +22699,7 @@ class ConfigValidator {
22421
22699
  }
22422
22700
  if (schema.enum && !schema.enum.includes(value)) {
22423
22701
  errors3.push({
22424
- path: path3,
22702
+ path: path4,
22425
22703
  message: `Value must be one of: ${schema.enum.join(", ")}`,
22426
22704
  expected: schema.enum.join(", "),
22427
22705
  actual: value,
@@ -22433,7 +22711,7 @@ class ConfigValidator {
22433
22711
  if (typeof value === "string") {
22434
22712
  if (schema.minLength !== undefined && value.length < schema.minLength) {
22435
22713
  errors3.push({
22436
- path: path3,
22714
+ path: path4,
22437
22715
  message: `String length must be at least ${schema.minLength}`,
22438
22716
  expected: `>= ${schema.minLength}`,
22439
22717
  actual: value.length,
@@ -22442,7 +22720,7 @@ class ConfigValidator {
22442
22720
  }
22443
22721
  if (schema.maxLength !== undefined && value.length > schema.maxLength) {
22444
22722
  errors3.push({
22445
- path: path3,
22723
+ path: path4,
22446
22724
  message: `String length must not exceed ${schema.maxLength}`,
22447
22725
  expected: `<= ${schema.maxLength}`,
22448
22726
  actual: value.length,
@@ -22453,7 +22731,7 @@ class ConfigValidator {
22453
22731
  const regex = new RegExp(schema.pattern);
22454
22732
  if (!regex.test(value)) {
22455
22733
  errors3.push({
22456
- path: path3,
22734
+ path: path4,
22457
22735
  message: `String does not match pattern ${schema.pattern}`,
22458
22736
  expected: schema.pattern,
22459
22737
  actual: value,
@@ -22465,7 +22743,7 @@ class ConfigValidator {
22465
22743
  if (typeof value === "number") {
22466
22744
  if (schema.minimum !== undefined && value < schema.minimum) {
22467
22745
  errors3.push({
22468
- path: path3,
22746
+ path: path4,
22469
22747
  message: `Value must be at least ${schema.minimum}`,
22470
22748
  expected: `>= ${schema.minimum}`,
22471
22749
  actual: value,
@@ -22474,7 +22752,7 @@ class ConfigValidator {
22474
22752
  }
22475
22753
  if (schema.maximum !== undefined && value > schema.maximum) {
22476
22754
  errors3.push({
22477
- path: path3,
22755
+ path: path4,
22478
22756
  message: `Value must not exceed ${schema.maximum}`,
22479
22757
  expected: `<= ${schema.maximum}`,
22480
22758
  actual: value,
@@ -22484,7 +22762,7 @@ class ConfigValidator {
22484
22762
  }
22485
22763
  if (Array.isArray(value) && schema.items) {
22486
22764
  for (let i = 0;i < value.length; i++) {
22487
- const itemPath = path3 ? `${path3}[${i}]` : `[${i}]`;
22765
+ const itemPath = path4 ? `${path4}[${i}]` : `[${i}]`;
22488
22766
  this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
22489
22767
  if (options2.stopOnFirstError && errors3.length > 0)
22490
22768
  return;
@@ -22496,7 +22774,7 @@ class ConfigValidator {
22496
22774
  for (const requiredProp of schema.required) {
22497
22775
  if (!(requiredProp in obj)) {
22498
22776
  errors3.push({
22499
- path: path3 ? `${path3}.${requiredProp}` : requiredProp,
22777
+ path: path4 ? `${path4}.${requiredProp}` : requiredProp,
22500
22778
  message: `Missing required property '${requiredProp}'`,
22501
22779
  expected: "required",
22502
22780
  rule: "required"
@@ -22509,7 +22787,7 @@ class ConfigValidator {
22509
22787
  if (schema.properties) {
22510
22788
  for (const [propName, propSchema] of Object.entries(schema.properties)) {
22511
22789
  if (propName in obj) {
22512
- const propPath = path3 ? `${path3}.${propName}` : propName;
22790
+ const propPath = path4 ? `${path4}.${propName}` : propName;
22513
22791
  this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
22514
22792
  if (options2.stopOnFirstError && errors3.length > 0)
22515
22793
  return;
@@ -22521,7 +22799,7 @@ class ConfigValidator {
22521
22799
  for (const propName of Object.keys(obj)) {
22522
22800
  if (!allowedProps.has(propName)) {
22523
22801
  warnings.push({
22524
- path: path3 ? `${path3}.${propName}` : propName,
22802
+ path: path4 ? `${path4}.${propName}` : propName,
22525
22803
  message: `Additional property '${propName}' is not allowed`,
22526
22804
  rule: "additionalProperties"
22527
22805
  });
@@ -22555,12 +22833,12 @@ class ConfigValidator {
22555
22833
  warnings
22556
22834
  };
22557
22835
  }
22558
- validateWithRule(value, rule, path3) {
22836
+ validateWithRule(value, rule, path4) {
22559
22837
  const errors3 = [];
22560
22838
  if (rule.required && (value === undefined || value === null)) {
22561
22839
  errors3.push({
22562
- path: path3,
22563
- message: rule.message || `Property '${path3}' is required`,
22840
+ path: path4,
22841
+ message: rule.message || `Property '${path4}' is required`,
22564
22842
  expected: "required",
22565
22843
  rule: "required"
22566
22844
  });
@@ -22573,7 +22851,7 @@ class ConfigValidator {
22573
22851
  const actualType = Array.isArray(value) ? "array" : typeof value;
22574
22852
  if (actualType !== rule.type) {
22575
22853
  errors3.push({
22576
- path: path3,
22854
+ path: path4,
22577
22855
  message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
22578
22856
  expected: rule.type,
22579
22857
  actual: actualType,
@@ -22585,7 +22863,7 @@ class ConfigValidator {
22585
22863
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22586
22864
  if (length < rule.min) {
22587
22865
  errors3.push({
22588
- path: path3,
22866
+ path: path4,
22589
22867
  message: rule.message || `Value must be at least ${rule.min}`,
22590
22868
  expected: `>= ${rule.min}`,
22591
22869
  actual: length,
@@ -22597,7 +22875,7 @@ class ConfigValidator {
22597
22875
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22598
22876
  if (length > rule.max) {
22599
22877
  errors3.push({
22600
- path: path3,
22878
+ path: path4,
22601
22879
  message: rule.message || `Value must not exceed ${rule.max}`,
22602
22880
  expected: `<= ${rule.max}`,
22603
22881
  actual: length,
@@ -22608,7 +22886,7 @@ class ConfigValidator {
22608
22886
  if (rule.pattern && typeof value === "string") {
22609
22887
  if (!rule.pattern.test(value)) {
22610
22888
  errors3.push({
22611
- path: path3,
22889
+ path: path4,
22612
22890
  message: rule.message || `Value does not match pattern ${rule.pattern}`,
22613
22891
  expected: rule.pattern.toString(),
22614
22892
  actual: value,
@@ -22618,7 +22896,7 @@ class ConfigValidator {
22618
22896
  }
22619
22897
  if (rule.enum && !rule.enum.includes(value)) {
22620
22898
  errors3.push({
22621
- path: path3,
22899
+ path: path4,
22622
22900
  message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
22623
22901
  expected: rule.enum.join(", "),
22624
22902
  actual: value,
@@ -22629,7 +22907,7 @@ class ConfigValidator {
22629
22907
  const customError = rule.validator(value);
22630
22908
  if (customError) {
22631
22909
  errors3.push({
22632
- path: path3,
22910
+ path: path4,
22633
22911
  message: rule.message || customError,
22634
22912
  rule: "custom"
22635
22913
  });
@@ -22637,10 +22915,10 @@ class ConfigValidator {
22637
22915
  }
22638
22916
  return errors3;
22639
22917
  }
22640
- getValueByPath(obj, path3) {
22641
- if (!path3)
22918
+ getValueByPath(obj, path4) {
22919
+ if (!path4)
22642
22920
  return obj;
22643
- const keys = path3.split(".");
22921
+ const keys = path4.split(".");
22644
22922
  let current = obj;
22645
22923
  for (const key of keys) {
22646
22924
  if (current && typeof current === "object" && key in current) {
@@ -23066,10 +23344,10 @@ async function loadConfig5(options2) {
23066
23344
  function applyEnvVarsToConfig2(name, config4, verbose = false) {
23067
23345
  const _envProcessor = new EnvProcessor;
23068
23346
  const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
23069
- function processConfigLevel(obj, path3 = []) {
23347
+ function processConfigLevel(obj, path4 = []) {
23070
23348
  const result = { ...obj };
23071
23349
  for (const [key, value] of Object.entries(obj)) {
23072
- const currentPath = [...path3, key];
23350
+ const currentPath = [...path4, key];
23073
23351
  const envKeys = [
23074
23352
  `${envPrefix}_${currentPath.join("_").toUpperCase()}`,
23075
23353
  `${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
@@ -23114,8 +23392,47 @@ var defaultConfigDir3 = resolve7(process12.cwd(), "config");
23114
23392
  var defaultGeneratedDir3 = resolve7(process12.cwd(), "src/generated");
23115
23393
 
23116
23394
  // src/config.ts
23395
+ import { access as access3, mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
23117
23396
  import { homedir as homedir3 } from "os";
23118
23397
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join4, normalize, resolve as resolve8 } from "path";
23398
+ var DEFAULT_RESERVED_SLASH_COMMANDS = [
23399
+ "agent",
23400
+ "agents",
23401
+ "compact",
23402
+ "connect",
23403
+ "details",
23404
+ "editor",
23405
+ "exit",
23406
+ "export",
23407
+ "fork",
23408
+ "help",
23409
+ "init",
23410
+ "mcp",
23411
+ "model",
23412
+ "models",
23413
+ "new",
23414
+ "open",
23415
+ "redo",
23416
+ "sessions",
23417
+ "share",
23418
+ "skills",
23419
+ "terminal",
23420
+ "themes",
23421
+ "thinking",
23422
+ "undo",
23423
+ "unshare"
23424
+ ];
23425
+ var DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT = [
23426
+ "You are selecting the most relevant dynamic skills for a user request.",
23427
+ "Return strict JSON only with this shape:",
23428
+ '{"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
23429
+ "Only recommend skills from the provided catalog.",
23430
+ "Prefer the smallest set of high-confidence matches."
23431
+ ].join(" ");
23432
+ var MANAGED_PLUGIN_CONFIG_DIRECTORY = join4(homedir3(), ".config", "opencode");
23433
+ var MANAGED_TUI_COMMANDS_DIRECTORY = join4(MANAGED_PLUGIN_CONFIG_DIRECTORY, "commands");
23434
+ var MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
23435
+ var MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
23119
23436
  function getOpenCodeConfigPaths() {
23120
23437
  const home = homedir3();
23121
23438
  const paths = [];
@@ -23135,14 +23452,14 @@ function getOpenCodeConfigPaths() {
23135
23452
  paths.push(join4(home, ".opencode"));
23136
23453
  return paths;
23137
23454
  }
23138
- function expandTildePath(path3) {
23139
- if (path3 === "~") {
23455
+ function expandTildePath(path4) {
23456
+ if (path4 === "~") {
23140
23457
  return homedir3();
23141
23458
  }
23142
- if (path3.startsWith("~/")) {
23143
- return join4(homedir3(), path3.slice(2));
23459
+ if (path4.startsWith("~/")) {
23460
+ return join4(homedir3(), path4.slice(2));
23144
23461
  }
23145
- return path3;
23462
+ return path4;
23146
23463
  }
23147
23464
  var createPathKey = (absolutePath) => {
23148
23465
  const normalizedPath = normalize(absolutePath);
@@ -23206,34 +23523,303 @@ var defaultSkillBasePaths = [
23206
23523
  join4(homedir3(), ".claude", "skills"),
23207
23524
  join4(homedir3(), ".agents", "skills")
23208
23525
  ];
23209
- var options2 = {
23210
- name: "opencode-dynamic-skills",
23211
- cwd: "./",
23212
- defaultConfig: {
23526
+ function createDefaultSkillRecommendConfig() {
23527
+ return {
23528
+ strategy: "heuristic",
23529
+ model: "",
23530
+ systemPrompt: DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
23531
+ };
23532
+ }
23533
+ function createDefaultNotificationConfig() {
23534
+ return {
23535
+ enabled: false,
23536
+ success: true,
23537
+ errors: true
23538
+ };
23539
+ }
23540
+ function createDefaultTuiCommandMirrorConfig() {
23541
+ return {
23542
+ enabled: true,
23543
+ directory: MANAGED_TUI_COMMANDS_DIRECTORY
23544
+ };
23545
+ }
23546
+ function createDefaultPluginConfig() {
23547
+ return {
23213
23548
  debug: false,
23214
23549
  basePaths: defaultSkillBasePaths,
23215
- promptRenderer: "xml",
23216
- modelRenderers: {},
23217
23550
  slashCommandName: "skill",
23218
- enableSkillAliases: true
23551
+ enableSkillAliases: true,
23552
+ reservedSlashCommands: [...DEFAULT_RESERVED_SLASH_COMMANDS],
23553
+ notifications: createDefaultNotificationConfig(),
23554
+ skillRecommend: createDefaultSkillRecommendConfig(),
23555
+ tuiCommandMirror: createDefaultTuiCommandMirrorConfig()
23556
+ };
23557
+ }
23558
+ function createConfigOptions(defaultConfig3) {
23559
+ return {
23560
+ name: "opencode-dynamic-skills",
23561
+ cwd: "./",
23562
+ defaultConfig: defaultConfig3
23563
+ };
23564
+ }
23565
+ function stripJsonComments(input) {
23566
+ let output = "";
23567
+ let inString = false;
23568
+ let escaped = false;
23569
+ let inLineComment = false;
23570
+ let inBlockComment = false;
23571
+ for (let index = 0;index < input.length; index += 1) {
23572
+ const current = input[index] ?? "";
23573
+ const next = input[index + 1] ?? "";
23574
+ if (inLineComment) {
23575
+ if (current === `
23576
+ ` || current === "\r") {
23577
+ inLineComment = false;
23578
+ output += current;
23579
+ }
23580
+ continue;
23581
+ }
23582
+ if (inBlockComment) {
23583
+ if (current === "*" && next === "/") {
23584
+ inBlockComment = false;
23585
+ index += 1;
23586
+ continue;
23587
+ }
23588
+ if (current === `
23589
+ ` || current === "\r") {
23590
+ output += current;
23591
+ }
23592
+ continue;
23593
+ }
23594
+ if (inString) {
23595
+ output += current;
23596
+ if (escaped) {
23597
+ escaped = false;
23598
+ continue;
23599
+ }
23600
+ if (current === "\\") {
23601
+ escaped = true;
23602
+ continue;
23603
+ }
23604
+ if (current === '"') {
23605
+ inString = false;
23606
+ }
23607
+ continue;
23608
+ }
23609
+ if (current === "/" && next === "/") {
23610
+ inLineComment = true;
23611
+ index += 1;
23612
+ continue;
23613
+ }
23614
+ if (current === "/" && next === "*") {
23615
+ inBlockComment = true;
23616
+ index += 1;
23617
+ continue;
23618
+ }
23619
+ if (current === '"') {
23620
+ inString = true;
23621
+ }
23622
+ output += current;
23219
23623
  }
23220
- };
23624
+ return output;
23625
+ }
23626
+ function removeTrailingJsonCommas(input) {
23627
+ let output = "";
23628
+ let inString = false;
23629
+ let escaped = false;
23630
+ for (let index = 0;index < input.length; index += 1) {
23631
+ const current = input[index] ?? "";
23632
+ if (inString) {
23633
+ output += current;
23634
+ if (escaped) {
23635
+ escaped = false;
23636
+ continue;
23637
+ }
23638
+ if (current === "\\") {
23639
+ escaped = true;
23640
+ continue;
23641
+ }
23642
+ if (current === '"') {
23643
+ inString = false;
23644
+ }
23645
+ continue;
23646
+ }
23647
+ if (current === '"') {
23648
+ inString = true;
23649
+ output += current;
23650
+ continue;
23651
+ }
23652
+ if (current === ",") {
23653
+ let lookahead = index + 1;
23654
+ while (lookahead < input.length && /\s/.test(input[lookahead] ?? "")) {
23655
+ lookahead += 1;
23656
+ }
23657
+ const nextToken = input[lookahead];
23658
+ if (nextToken === "}" || nextToken === "]") {
23659
+ continue;
23660
+ }
23661
+ }
23662
+ output += current;
23663
+ }
23664
+ return output;
23665
+ }
23666
+ function parseJsonc(input) {
23667
+ return JSON.parse(removeTrailingJsonCommas(stripJsonComments(input)));
23668
+ }
23669
+ function trimString(value) {
23670
+ return value?.trim() ?? "";
23671
+ }
23672
+ function mergePluginConfig(baseConfig, override) {
23673
+ const skillRecommend = {
23674
+ ...baseConfig.skillRecommend,
23675
+ ...override.skillRecommend
23676
+ };
23677
+ const notifications = {
23678
+ ...baseConfig.notifications,
23679
+ ...override.notifications
23680
+ };
23681
+ const tuiCommandMirror = {
23682
+ ...baseConfig.tuiCommandMirror,
23683
+ ...override.tuiCommandMirror
23684
+ };
23685
+ const overrideReservedSlashCommands = override.reservedSlashCommands?.map((command) => trimString(command)).filter(Boolean) ?? [];
23686
+ return {
23687
+ ...baseConfig,
23688
+ ...override,
23689
+ skillRecommend: {
23690
+ strategy: skillRecommend.strategy === "model" ? "model" : "heuristic",
23691
+ model: trimString(skillRecommend.model),
23692
+ systemPrompt: trimString(skillRecommend.systemPrompt) || DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
23693
+ },
23694
+ reservedSlashCommands: overrideReservedSlashCommands.length > 0 ? overrideReservedSlashCommands : baseConfig.reservedSlashCommands,
23695
+ notifications: {
23696
+ enabled: notifications.enabled === true,
23697
+ success: notifications.success !== false,
23698
+ errors: notifications.errors !== false
23699
+ },
23700
+ tuiCommandMirror: {
23701
+ enabled: tuiCommandMirror.enabled === true,
23702
+ directory: trimString(tuiCommandMirror.directory) || MANAGED_TUI_COMMANDS_DIRECTORY
23703
+ }
23704
+ };
23705
+ }
23706
+ function getManagedPluginConfigPaths(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
23707
+ return [
23708
+ join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME),
23709
+ join4(configDirectory, MANAGED_PLUGIN_JSON_FILENAME)
23710
+ ];
23711
+ }
23712
+ async function fileExists(filePath) {
23713
+ try {
23714
+ await access3(filePath);
23715
+ return true;
23716
+ } catch {
23717
+ return false;
23718
+ }
23719
+ }
23720
+ function renderManagedPluginConfigJsonc(config3 = createDefaultPluginConfig()) {
23721
+ const basePaths = config3.basePaths.map((basePath) => ` ${JSON.stringify(basePath)}`).join(`,
23722
+ `);
23723
+ return [
23724
+ "{",
23725
+ " // Enable verbose plugin logging output.",
23726
+ ` "debug": ${JSON.stringify(config3.debug)},`,
23727
+ "",
23728
+ " // Global skill roots. Project-local .opencode/.claude/.agents paths are appended automatically.",
23729
+ ' "basePaths": [',
23730
+ basePaths,
23731
+ " ],",
23732
+ "",
23733
+ " // Explicit slash entrypoint, for example: /skill git-release",
23734
+ ` "slashCommandName": ${JSON.stringify(config3.slashCommandName)},`,
23735
+ "",
23736
+ " // When true, /<skill-name> alias invocations can be intercepted after submit.",
23737
+ ` "enableSkillAliases": ${JSON.stringify(config3.enableSkillAliases)},`,
23738
+ "",
23739
+ " // Builtin or protected slash commands that should never be intercepted as /<skill-name> aliases.",
23740
+ ' "reservedSlashCommands": [',
23741
+ config3.reservedSlashCommands.map((command) => ` ${JSON.stringify(command)}`).join(`,
23742
+ `),
23743
+ " ],",
23744
+ "",
23745
+ " // Best-effort OS notifications triggered by this plugin.",
23746
+ ' "notifications": {',
23747
+ ` "enabled": ${JSON.stringify(config3.notifications.enabled)},`,
23748
+ ` "success": ${JSON.stringify(config3.notifications.success)},`,
23749
+ ` "errors": ${JSON.stringify(config3.notifications.errors)}`,
23750
+ " },",
23751
+ "",
23752
+ ' // skill_recommend strategy. Set strategy to "model" and fill model with provider/model to use an internal LLM call.',
23753
+ ' "skillRecommend": {',
23754
+ ` "strategy": ${JSON.stringify(config3.skillRecommend.strategy)},`,
23755
+ " // Model format: provider/model, for example openai/gpt-5.2",
23756
+ ` "model": ${JSON.stringify(config3.skillRecommend.model)},`,
23757
+ ' // The model must return strict JSON: {"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
23758
+ ` "systemPrompt": ${JSON.stringify(config3.skillRecommend.systemPrompt)}`,
23759
+ " },",
23760
+ "",
23761
+ " // TUI compatibility layer. Enabled by default so dynamic skills are mirrored as proxy command files and can appear in TUI slash autocomplete after restart.",
23762
+ ' "tuiCommandMirror": {',
23763
+ ` "enabled": ${JSON.stringify(config3.tuiCommandMirror.enabled)},`,
23764
+ ` "directory": ${JSON.stringify(config3.tuiCommandMirror.directory)}`,
23765
+ " }",
23766
+ "}",
23767
+ ""
23768
+ ].join(`
23769
+ `);
23770
+ }
23771
+ async function ensureManagedPluginConfigFile(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
23772
+ for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
23773
+ if (await fileExists(candidatePath)) {
23774
+ return candidatePath;
23775
+ }
23776
+ }
23777
+ await mkdir3(configDirectory, { recursive: true });
23778
+ const configPath = join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME);
23779
+ await writeFile3(configPath, renderManagedPluginConfigJsonc(), "utf8");
23780
+ return configPath;
23781
+ }
23782
+ async function loadManagedPluginConfig(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
23783
+ for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
23784
+ if (!await fileExists(candidatePath)) {
23785
+ continue;
23786
+ }
23787
+ const content = await readFile(candidatePath, "utf8");
23788
+ if (candidatePath.endsWith(".jsonc")) {
23789
+ return parseJsonc(content);
23790
+ }
23791
+ return JSON.parse(content);
23792
+ }
23793
+ return {};
23794
+ }
23221
23795
  async function getPluginConfig(ctx) {
23222
- const resolvedConfig = await loadConfig5(options2);
23796
+ const defaultConfig3 = createDefaultPluginConfig();
23797
+ await ensureManagedPluginConfigFile();
23798
+ const managedConfig = await loadManagedPluginConfig();
23799
+ const resolvedConfig = await loadConfig5(createConfigOptions(defaultConfig3));
23800
+ const mergedConfig = mergePluginConfig(mergePluginConfig(defaultConfig3, managedConfig), resolvedConfig);
23223
23801
  const configuredBasePaths = [
23224
- ...resolvedConfig.basePaths,
23802
+ ...mergedConfig.basePaths,
23225
23803
  ...getProjectSkillBasePaths(ctx.directory, ctx.worktree)
23226
23804
  ];
23227
- resolvedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
23228
- return resolvedConfig;
23805
+ mergedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
23806
+ mergedConfig.tuiCommandMirror = {
23807
+ enabled: mergedConfig.tuiCommandMirror.enabled,
23808
+ directory: resolveBasePath(mergedConfig.tuiCommandMirror.directory, ctx.directory)
23809
+ };
23810
+ return mergedConfig;
23229
23811
  }
23230
23812
 
23231
23813
  // src/commands/SlashCommand.ts
23232
23814
  var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
23233
23815
  var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
23234
- function normalizeSkillSelector3(selector) {
23816
+ function normalizeSkillSelector4(selector) {
23235
23817
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
23236
23818
  }
23819
+ function isReservedSlashCommand(invocationName, reservedSlashCommands) {
23820
+ const normalizedInvocationName = invocationName.trim().toLowerCase();
23821
+ return reservedSlashCommands.some((reservedCommand) => reservedCommand.trim().toLowerCase() === normalizedInvocationName);
23822
+ }
23237
23823
  function parseSlashCommand(text, slashCommandName) {
23238
23824
  const trimmedText = text.trim();
23239
23825
  if (!trimmedText.startsWith("/")) {
@@ -23268,21 +23854,21 @@ function findSkillBySelector(registry2, selector) {
23268
23854
  if (directMatch) {
23269
23855
  return directMatch;
23270
23856
  }
23271
- const normalizedSelector = normalizeSkillSelector3(selector);
23857
+ const normalizedSelector = normalizeSkillSelector4(selector);
23272
23858
  for (const skill of registry2.controller.skills) {
23273
- if (normalizeSkillSelector3(skill.toolName) === normalizedSelector) {
23859
+ if (normalizeSkillSelector4(skill.toolName) === normalizedSelector) {
23274
23860
  return skill;
23275
23861
  }
23276
- if (normalizeSkillSelector3(skill.name) === normalizedSelector) {
23862
+ if (normalizeSkillSelector4(skill.name) === normalizedSelector) {
23277
23863
  return skill;
23278
23864
  }
23279
23865
  }
23280
23866
  return null;
23281
23867
  }
23282
- function renderSlashSkillPrompt(args) {
23868
+ async function renderSlashSkillPrompt(args) {
23283
23869
  return [
23284
23870
  SLASH_COMMAND_SENTINEL,
23285
- formatLoadedSkill({
23871
+ await formatLoadedSkill({
23286
23872
  invocationName: args.invocationName,
23287
23873
  skill: args.skill,
23288
23874
  userMessage: args.userPrompt || "Apply this skill to the current request."
@@ -23328,37 +23914,40 @@ async function rewriteSlashCommandText(args) {
23328
23914
  if (isAliasInvocation && !args.enableSkillAliases) {
23329
23915
  return null;
23330
23916
  }
23917
+ if (isAliasInvocation && isReservedSlashCommand(parsedCommand.invocationName, args.reservedSlashCommands ?? [])) {
23918
+ return null;
23919
+ }
23331
23920
  await args.registry.controller.ready.whenReady();
23332
23921
  const skill = findSkillBySelector(args.registry, parsedCommand.skillSelector);
23333
23922
  if (!skill) {
23334
23923
  return null;
23335
23924
  }
23336
- return renderSlashSkillPrompt({
23925
+ return await renderSlashSkillPrompt({
23337
23926
  invocationName: parsedCommand.invocationName,
23338
23927
  skill,
23339
23928
  userPrompt: parsedCommand.userPrompt
23340
23929
  });
23341
23930
  }
23342
23931
 
23343
- // src/lib/renderers/JsonPromptRenderer.ts
23344
- var createJsonPromptRenderer = () => {
23345
- const renderer = {
23346
- format: "json",
23347
- render(args) {
23348
- if (args.type === "Skill") {
23349
- return JSON.stringify({
23350
- Skill: {
23351
- ...args.data,
23352
- linkedResources: extractSkillLinks(args.data.content),
23353
- skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path."
23354
- }
23355
- }, null, 2);
23356
- }
23357
- return JSON.stringify({ [args.type]: args.data }, null, 2);
23358
- }
23359
- };
23360
- return renderer;
23361
- };
23932
+ // src/commands/MirroredSkillCommand.ts
23933
+ async function rewriteMirroredSkillCommandParts(args) {
23934
+ if (!args.mirroredCommands.has(args.commandName)) {
23935
+ return false;
23936
+ }
23937
+ await args.registry.controller.ready.whenReady();
23938
+ const skill = findSkillBySelector(args.registry, args.commandName);
23939
+ if (!skill) {
23940
+ return false;
23941
+ }
23942
+ const preservedParts = args.output.parts.filter((part) => part.type !== "text");
23943
+ const text = await formatLoadedSkill({
23944
+ skill,
23945
+ invocationName: args.commandName,
23946
+ userMessage: args.commandArguments
23947
+ });
23948
+ args.output.parts = [{ type: "text", text }, ...preservedParts];
23949
+ return true;
23950
+ }
23362
23951
 
23363
23952
  // src/lib/xml.ts
23364
23953
  function escapeXml(str2) {
@@ -23413,6 +24002,7 @@ var createXmlPromptRenderer = () => {
23413
24002
  ...skill,
23414
24003
  linkedResources: extractSkillLinks(skill.content),
23415
24004
  skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path.",
24005
+ files: resourceMapToArray(skill.files),
23416
24006
  references: resourceMapToArray(skill.references),
23417
24007
  scripts: resourceMapToArray(skill.scripts),
23418
24008
  assets: resourceMapToArray(skill.assets)
@@ -23443,362 +24033,244 @@ var createXmlPromptRenderer = () => {
23443
24033
  return renderer;
23444
24034
  };
23445
24035
 
23446
- // node_modules/dedent/dist/dedent.mjs
23447
- function ownKeys(object2, enumerableOnly) {
23448
- var keys = Object.keys(object2);
23449
- if (Object.getOwnPropertySymbols) {
23450
- var symbols = Object.getOwnPropertySymbols(object2);
23451
- enumerableOnly && (symbols = symbols.filter(function(sym) {
23452
- return Object.getOwnPropertyDescriptor(object2, sym).enumerable;
23453
- })), keys.push.apply(keys, symbols);
23454
- }
23455
- return keys;
23456
- }
23457
- function _objectSpread(target) {
23458
- for (var i = 1;i < arguments.length; i++) {
23459
- var source = arguments[i] != null ? arguments[i] : {};
23460
- i % 2 ? ownKeys(Object(source), true).forEach(function(key) {
23461
- _defineProperty(target, key, source[key]);
23462
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function(key) {
23463
- Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
23464
- });
23465
- }
23466
- return target;
24036
+ // src/services/Notifier.ts
24037
+ import { execFile } from "child_process";
24038
+ import { promisify } from "util";
24039
+ var execFileAsync = promisify(execFile);
24040
+ var NOTIFICATION_TITLE = "OpenCode Dynamic Skills";
24041
+ function createDefaultRunner() {
24042
+ return async (command, args) => {
24043
+ await execFileAsync(command, args);
24044
+ };
23467
24045
  }
23468
- function _defineProperty(obj, key, value) {
23469
- key = _toPropertyKey(key);
23470
- if (key in obj) {
23471
- Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
23472
- } else {
23473
- obj[key] = value;
24046
+ function truncateNotificationText(value, maxLength = 180) {
24047
+ const normalized = value.replace(/\s+/g, " ").trim();
24048
+ if (normalized.length <= maxLength) {
24049
+ return normalized;
23474
24050
  }
23475
- return obj;
24051
+ return `${normalized.slice(0, maxLength - 1)}\u2026`;
23476
24052
  }
23477
- function _toPropertyKey(arg) {
23478
- var key = _toPrimitive(arg, "string");
23479
- return typeof key === "symbol" ? key : String(key);
24053
+ function escapeAppleScript(value) {
24054
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
23480
24055
  }
23481
- function _toPrimitive(input, hint) {
23482
- if (typeof input !== "object" || input === null)
23483
- return input;
23484
- var prim = input[Symbol.toPrimitive];
23485
- if (prim !== undefined) {
23486
- var res = prim.call(input, hint || "default");
23487
- if (typeof res !== "object")
23488
- return res;
23489
- throw new TypeError("@@toPrimitive must return a primitive value.");
23490
- }
23491
- return (hint === "string" ? String : Number)(input);
23492
- }
23493
- var dedent = createDedent({});
23494
- var dedent_default = dedent;
23495
- function createDedent(options3) {
23496
- dedent2.withOptions = (newOptions) => createDedent(_objectSpread(_objectSpread({}, options3), newOptions));
23497
- return dedent2;
23498
- function dedent2(strings, ...values) {
23499
- const raw = typeof strings === "string" ? [strings] : strings.raw;
23500
- const {
23501
- alignValues = false,
23502
- escapeSpecialCharacters = Array.isArray(strings),
23503
- trimWhitespace = true
23504
- } = options3;
23505
- let result = "";
23506
- for (let i = 0;i < raw.length; i++) {
23507
- let next = raw[i];
23508
- if (escapeSpecialCharacters) {
23509
- next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
23510
- }
23511
- result += next;
23512
- if (i < values.length) {
23513
- const value = alignValues ? alignValue(values[i], result) : values[i];
23514
- result += value;
23515
- }
23516
- }
23517
- const lines = result.split(`
23518
- `);
23519
- let mindent = null;
23520
- for (const l of lines) {
23521
- const m = l.match(/^(\s+)\S+/);
23522
- if (m) {
23523
- const indent = m[1].length;
23524
- if (!mindent) {
23525
- mindent = indent;
23526
- } else {
23527
- mindent = Math.min(mindent, indent);
23528
- }
23529
- }
23530
- }
23531
- if (mindent !== null) {
23532
- const m = mindent;
23533
- result = lines.map((l) => l[0] === " " || l[0] === "\t" ? l.slice(m) : l).join(`
23534
- `);
23535
- }
23536
- if (trimWhitespace) {
23537
- result = result.trim();
23538
- }
23539
- if (escapeSpecialCharacters) {
23540
- result = result.replace(/\\n/g, `
23541
- `).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\v/g, "\v").replace(/\\b/g, "\b").replace(/\\f/g, "\f").replace(/\\0/g, "\x00").replace(/\\x([\da-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\u\{([\da-fA-F]{1,6})\}/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/\\u([\da-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
23542
- }
23543
- if (typeof Bun !== "undefined") {
23544
- result = result.replace(/\\u(?:\{([\da-fA-F]{1,6})\}|([\da-fA-F]{4}))/g, (_, braced, unbraced) => {
23545
- var _ref;
23546
- const hex3 = (_ref = braced !== null && braced !== undefined ? braced : unbraced) !== null && _ref !== undefined ? _ref : "";
23547
- return String.fromCodePoint(parseInt(hex3, 16));
23548
- });
23549
- }
23550
- return result;
23551
- }
24056
+ function escapeXml2(value) {
24057
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
23552
24058
  }
23553
- function alignValue(value, precedingText) {
23554
- if (typeof value !== "string" || !value.includes(`
23555
- `)) {
23556
- return value;
23557
- }
23558
- const currentLine = precedingText.slice(precedingText.lastIndexOf(`
23559
- `) + 1);
23560
- const indentMatch = currentLine.match(/^(\s+)/);
23561
- if (indentMatch) {
23562
- const indent = indentMatch[1];
23563
- return value.replace(/\n/g, `
23564
- ${indent}`);
24059
+ function buildWindowsToastScript(title, message) {
24060
+ const xml = `<toast><visual><binding template='ToastGeneric'><text>${escapeXml2(title)}</text><text>${escapeXml2(message)}</text></binding></visual></toast>`;
24061
+ return [
24062
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
24063
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
24064
+ "$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
24065
+ `$xml.LoadXml(@'${xml}'@)`,
24066
+ "$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
24067
+ `$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${NOTIFICATION_TITLE}')`,
24068
+ "$notifier.Show($toast)"
24069
+ ].join("; ");
24070
+ }
24071
+ function getNotificationCommand(platform, title, message) {
24072
+ switch (platform) {
24073
+ case "darwin":
24074
+ return {
24075
+ command: "osascript",
24076
+ args: [
24077
+ "-e",
24078
+ `display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`
24079
+ ]
24080
+ };
24081
+ case "linux":
24082
+ return {
24083
+ command: "notify-send",
24084
+ args: [title, message]
24085
+ };
24086
+ case "win32":
24087
+ return {
24088
+ command: "powershell",
24089
+ args: ["-NoProfile", "-Command", buildWindowsToastScript(title, message)]
24090
+ };
24091
+ default:
24092
+ return null;
23565
24093
  }
23566
- return value;
23567
24094
  }
23568
-
23569
- // src/lib/renderers/MdPromptRenderer.ts
23570
- var createMdPromptRenderer = () => {
23571
- const renderObject = (obj, headingLevel, indentLevel = 0) => {
23572
- const entries = Object.entries(obj);
23573
- let output = "";
23574
- for (const [key, value] of entries) {
23575
- if (value === null || value === undefined) {
23576
- continue;
23577
- }
23578
- const heading = "#".repeat(headingLevel);
23579
- output += `${heading} ${key}`;
23580
- if (typeof value === "object" && !Array.isArray(value)) {
23581
- output += renderObject(value, Math.min(headingLevel + 1, 6), indentLevel);
23582
- } else if (Array.isArray(value)) {
23583
- output += renderArray(value, indentLevel);
23584
- } else {
23585
- const indent = " ".repeat(indentLevel);
23586
- const escapedValue = htmlEscape(String(value));
23587
- output += `${indent}- **${key}**: *${escapedValue}*`;
23588
- }
23589
- output += `
23590
- `;
24095
+ function createNotifier(args) {
24096
+ const runner = args.shell ?? createDefaultRunner();
24097
+ const platform = args.platform ?? process.platform;
24098
+ async function notify(title, message) {
24099
+ if (!args.config.enabled) {
24100
+ return;
23591
24101
  }
23592
- return output;
23593
- };
23594
- const renderArray = (arr, indentLevel) => {
23595
- const indent = " ".repeat(indentLevel);
23596
- let output = "";
23597
- for (const item of arr) {
23598
- if (item === null || item === undefined) {
23599
- continue;
23600
- }
23601
- if (typeof item === "object" && !Array.isArray(item)) {
23602
- const nestedObj = item;
23603
- for (const [key, value] of Object.entries(nestedObj)) {
23604
- if (value === null || value === undefined) {
23605
- continue;
23606
- }
23607
- if (typeof value === "object") {
23608
- if (Array.isArray(value)) {
23609
- output += `${indent}- **${key}**:
23610
- `;
23611
- output += renderArray(value, indentLevel + 1);
23612
- } else {
23613
- output += `${indent}- **${key}**
23614
- `;
23615
- output += renderObject(value, 4, indentLevel + 1);
23616
- }
23617
- } else {
23618
- const escapedValue = htmlEscape(String(value));
23619
- output += `${indent}- **${key}**: *${escapedValue}*
23620
- `;
23621
- }
23622
- }
23623
- } else if (Array.isArray(item)) {
23624
- output += renderArray(item, indentLevel + 1);
23625
- } else {
23626
- const escapedValue = htmlEscape(String(item));
23627
- output += `${indent}- *${escapedValue}*
23628
- `;
23629
- }
24102
+ const notificationCommand = getNotificationCommand(platform, truncateNotificationText(title, 80), truncateNotificationText(message));
24103
+ if (!notificationCommand) {
24104
+ args.logger.debug("Notifications are not supported on this platform.", platform);
24105
+ return;
23630
24106
  }
23631
- return output;
23632
- };
23633
- const htmlEscape = (value) => {
23634
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
23635
- };
23636
- const renderSkill = (skill) => {
23637
- const linkedResources = extractSkillLinks(skill.content);
23638
- return dedent_default`
23639
- # ${skill.name}
23640
-
23641
- > Skill root:
23642
- > ${skill.fullPath}
23643
-
23644
- Relative file references in this skill resolve from the skill root directory.
23645
- Always use skill_resource with the exact root-relative path for linked files.
23646
-
23647
- ${linkedResources.length > 0 ? dedent_default`
23648
- ## Linked files detected in skill content
23649
-
23650
- ${linkedResources.map((link) => `- [${link.label}](${link.originalPath}) \u2192 use skill_resource with ${link.resourcePath}`).join(`
23651
- `)}
23652
- ` : ""}
23653
-
23654
- ${skill.content}
23655
-
23656
- ## Metadata
23657
-
23658
- ${skill.metadata ? renderObject(skill.metadata, 3) : ""}
23659
-
23660
- ## References
23661
-
23662
- ${skill.references ? renderArray(resourceMapToArray(skill.references), 1) : ""}
23663
-
23664
- ## Scripts
23665
-
23666
- ${skill.scripts ? renderArray(resourceMapToArray(skill.scripts), 1) : ""}
23667
-
23668
- ## Assets
23669
-
23670
- ${skill.assets ? renderArray(resourceMapToArray(skill.assets), 1) : ""}
23671
- `;
23672
- };
23673
- const renderResource = (resource) => {
23674
- return renderObject(resource, 3);
23675
- };
23676
- const renderSearchResult = (result) => {
23677
- return renderObject(result, 3);
23678
- };
23679
- const renderer = {
23680
- format: "md",
23681
- render(args) {
23682
- if (args.type === "Skill") {
23683
- return renderSkill(args.data);
24107
+ try {
24108
+ await runner(notificationCommand.command, notificationCommand.args);
24109
+ } catch (error45) {
24110
+ args.logger.warn("Failed to send notification.", error45);
24111
+ }
24112
+ }
24113
+ return {
24114
+ async skillLoaded(skillNames) {
24115
+ if (!args.config.success || skillNames.length === 0) {
24116
+ return;
23684
24117
  }
23685
- if (args.type === "SkillResource") {
23686
- return renderResource(args.data);
24118
+ const count = skillNames.length;
24119
+ const message = count === 1 ? `Injected skill: ${skillNames[0]}` : `Injected ${count} skills: ${skillNames.join(", ")}`;
24120
+ await notify(NOTIFICATION_TITLE, message);
24121
+ },
24122
+ async resourceLoaded(skillName, relativePath) {
24123
+ if (!args.config.success) {
24124
+ return;
23687
24125
  }
23688
- if (args.type === "SkillSearchResults") {
23689
- return renderSearchResult(args.data);
24126
+ await notify(NOTIFICATION_TITLE, `Injected ${relativePath} from ${skillName}`);
24127
+ },
24128
+ async error(title, message) {
24129
+ if (!args.config.errors) {
24130
+ return;
23690
24131
  }
23691
- return renderObject({}, 3);
24132
+ await notify(title, message);
23692
24133
  }
23693
24134
  };
23694
- return renderer;
23695
- };
23696
-
23697
- // src/lib/createPromptRenderer.ts
23698
- function createPromptRenderer() {
23699
- const renderers = {
23700
- json: createJsonPromptRenderer(),
23701
- xml: createXmlPromptRenderer(),
23702
- md: createMdPromptRenderer()
23703
- };
23704
- const getFormatter = (format) => {
23705
- switch (format) {
23706
- case "json":
23707
- return renderers.json.render;
23708
- case "xml":
23709
- return renderers.xml.render;
23710
- case "md":
23711
- return renderers.md.render;
23712
- default:
23713
- throw new Error(`Unsupported format: ${format}`);
23714
- }
23715
- };
23716
- return {
23717
- getFormatter
23718
- };
23719
24135
  }
23720
24136
 
23721
- // src/lib/getModelFormat.ts
23722
- function getModelFormat(args) {
23723
- const { modelId, providerId, config: config3 } = args;
23724
- const modelRenderers = config3.modelRenderers ?? {};
23725
- if (providerId && modelId) {
23726
- const combinedKey = `${providerId}-${modelId}`;
23727
- if (combinedKey in modelRenderers) {
23728
- return modelRenderers[combinedKey];
23729
- }
23730
- }
23731
- if (modelId && modelId in modelRenderers) {
23732
- return modelRenderers[modelId];
23733
- }
23734
- return config3.promptRenderer;
24137
+ // src/services/TuiCommandMirror.ts
24138
+ import { mkdir as mkdir5, readdir as readdir3, readFile as readFile2, rm, writeFile as writeFile5 } from "fs/promises";
24139
+ import { join as join6 } from "path";
24140
+ var SHADOW_COMMAND_MARKER = "<!-- opencode-dynamic-skills:tui-command-mirror -->";
24141
+ function normalizeName(name) {
24142
+ return name.trim().toLowerCase();
23735
24143
  }
23736
-
23737
- // src/services/MessageModelIdAccountant.ts
23738
- function createMessageModelIdAccountant() {
23739
- const modelUsage = new Map;
23740
- const track = (info) => {
23741
- if (!modelUsage.has(info.sessionID)) {
23742
- modelUsage.set(info.sessionID, {});
23743
- }
23744
- const sessionMap = modelUsage.get(info.sessionID);
23745
- sessionMap[info.messageID] = {
23746
- modelID: info.modelID,
23747
- providerID: info.providerID
24144
+ function isManagedShadowCommand(content) {
24145
+ return content.includes(SHADOW_COMMAND_MARKER);
24146
+ }
24147
+ function renderShadowCommandFile(skill) {
24148
+ return [
24149
+ "---",
24150
+ `description: ${JSON.stringify(skill.description)}`,
24151
+ "---",
24152
+ "",
24153
+ SHADOW_COMMAND_MARKER,
24154
+ `Dynamic skill proxy for ${skill.name}.`,
24155
+ "This file is managed by opencode-dynamic-skills so OpenCode TUI can surface the skill as a slash command.",
24156
+ "The plugin replaces this template before execution.",
24157
+ "",
24158
+ "$ARGUMENTS",
24159
+ ""
24160
+ ].join(`
24161
+ `);
24162
+ }
24163
+ async function syncTuiCommandMirror(args) {
24164
+ if (!args.enabled) {
24165
+ return {
24166
+ mirrored: [],
24167
+ written: [],
24168
+ skipped: [],
24169
+ removed: []
23748
24170
  };
23749
- };
23750
- const untrackMessage = (args) => {
23751
- const sessionMap = modelUsage.get(args.sessionID);
23752
- if (sessionMap && sessionMap[args.messageID]) {
23753
- delete sessionMap[args.messageID];
23754
- if (Object.keys(sessionMap).length === 0) {
23755
- modelUsage.delete(args.sessionID);
24171
+ }
24172
+ await mkdir5(args.directory, { recursive: true });
24173
+ const reservedCommands = new Set(args.reservedSlashCommands.map(normalizeName));
24174
+ const mirrored = [];
24175
+ const written = [];
24176
+ const skipped = [];
24177
+ const removed = [];
24178
+ const desiredNames = new Set;
24179
+ for (const skill of args.skills) {
24180
+ const normalizedSkillName = normalizeName(skill.name);
24181
+ if (reservedCommands.has(normalizedSkillName)) {
24182
+ skipped.push(skill.name);
24183
+ continue;
24184
+ }
24185
+ desiredNames.add(normalizedSkillName);
24186
+ const commandPath = join6(args.directory, `${skill.name}.md`);
24187
+ const renderedCommand = renderShadowCommandFile(skill);
24188
+ try {
24189
+ const existingContent = await readFile2(commandPath, "utf8");
24190
+ if (!isManagedShadowCommand(existingContent)) {
24191
+ skipped.push(skill.name);
24192
+ continue;
24193
+ }
24194
+ if (existingContent === renderedCommand) {
24195
+ mirrored.push(skill.name);
24196
+ continue;
23756
24197
  }
24198
+ } catch {}
24199
+ await writeFile5(commandPath, renderedCommand, "utf8");
24200
+ mirrored.push(skill.name);
24201
+ written.push(skill.name);
24202
+ }
24203
+ for (const entry of await readdir3(args.directory, { withFileTypes: true })) {
24204
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
24205
+ continue;
23757
24206
  }
23758
- };
23759
- const untrackSession = (sessionID) => {
23760
- modelUsage.delete(sessionID);
23761
- };
23762
- const getModelInfo = (args) => {
23763
- const sessionMap = modelUsage.get(args.sessionID);
23764
- return sessionMap ? sessionMap[args.messageID] : undefined;
23765
- };
23766
- const reset3 = () => {
23767
- modelUsage.clear();
23768
- };
24207
+ const commandPath = join6(args.directory, entry.name);
24208
+ const content = await readFile2(commandPath, "utf8").catch(() => "");
24209
+ if (!isManagedShadowCommand(content)) {
24210
+ continue;
24211
+ }
24212
+ const name = entry.name.replace(/\.md$/i, "");
24213
+ if (desiredNames.has(normalizeName(name))) {
24214
+ continue;
24215
+ }
24216
+ await rm(commandPath, { force: true });
24217
+ removed.push(name);
24218
+ }
24219
+ args.logger.debug("[OpenCodeDynamicSkills] TUI command mirror synced", {
24220
+ directory: args.directory,
24221
+ mirrored,
24222
+ written,
24223
+ removed,
24224
+ skipped
24225
+ });
23769
24226
  return {
23770
- reset: reset3,
23771
- track,
23772
- untrackMessage,
23773
- untrackSession,
23774
- getModelInfo
24227
+ mirrored,
24228
+ written,
24229
+ skipped,
24230
+ removed
23775
24231
  };
23776
24232
  }
23777
24233
 
23778
24234
  // src/index.ts
23779
24235
  var SkillsPlugin = async (ctx) => {
23780
24236
  const config3 = await getPluginConfig(ctx);
23781
- const api2 = await createApi(config3);
24237
+ const api2 = await createApi(config3, ctx.client);
23782
24238
  const sendPrompt = createInstructionInjector(ctx);
23783
- const promptRenderer = createPromptRenderer();
23784
- const modelIdAccountant = createMessageModelIdAccountant();
23785
- api2.registry.initialise();
23786
- return {
23787
- "chat.message": async (input, output) => {
23788
- if (input.messageID && input.model?.providerID && input.model?.modelID) {
23789
- modelIdAccountant.track({
23790
- messageID: input.messageID,
23791
- providerID: input.model.providerID,
23792
- modelID: input.model.modelID,
23793
- sessionID: input.sessionID
24239
+ const renderXmlPrompt = createXmlPromptRenderer().render;
24240
+ const notifier = createNotifier({
24241
+ config: config3.notifications,
24242
+ logger: api2.logger
24243
+ });
24244
+ const mirroredSkillCommands = new Set;
24245
+ const registryInitialisation = api2.registry.initialise();
24246
+ if (config3.tuiCommandMirror.enabled) {
24247
+ try {
24248
+ await registryInitialisation;
24249
+ const mirrorResult = await syncTuiCommandMirror({
24250
+ skills: api2.registry.controller.skills,
24251
+ directory: config3.tuiCommandMirror.directory,
24252
+ enabled: config3.tuiCommandMirror.enabled,
24253
+ logger: api2.logger,
24254
+ reservedSlashCommands: config3.reservedSlashCommands
24255
+ });
24256
+ for (const commandName of mirrorResult.mirrored) {
24257
+ mirroredSkillCommands.add(commandName);
24258
+ }
24259
+ if (mirrorResult.written.length > 0 || mirrorResult.removed.length > 0) {
24260
+ api2.logger.warn("[OpenCodeDynamicSkills] TUI command mirror updated. Restart OpenCode to refresh slash autocomplete.", {
24261
+ directory: config3.tuiCommandMirror.directory,
24262
+ mirrored: mirrorResult.mirrored,
24263
+ written: mirrorResult.written,
24264
+ removed: mirrorResult.removed,
24265
+ skipped: mirrorResult.skipped
23794
24266
  });
23795
24267
  }
23796
- const format = getModelFormat({
23797
- modelId: input.model?.modelID,
23798
- providerId: input.model?.providerID,
23799
- config: config3
23800
- });
23801
- promptRenderer.getFormatter(format);
24268
+ } catch (error45) {
24269
+ api2.logger.warn("[OpenCodeDynamicSkills] Failed to sync TUI command mirror. Dynamic skills will still work after submit, but TUI slash autocomplete may miss them.", error45 instanceof Error ? error45.message : String(error45));
24270
+ }
24271
+ }
24272
+ return {
24273
+ "chat.message": async (_input, output) => {
23802
24274
  for (const part of output.parts) {
23803
24275
  if (part.type !== "text") {
23804
24276
  continue;
@@ -23812,22 +24284,40 @@ var SkillsPlugin = async (ctx) => {
23812
24284
  text: part.text,
23813
24285
  registry: api2.registry,
23814
24286
  slashCommandName: config3.slashCommandName,
23815
- enableSkillAliases: config3.enableSkillAliases
24287
+ enableSkillAliases: config3.enableSkillAliases,
24288
+ reservedSlashCommands: config3.reservedSlashCommands
23816
24289
  });
23817
24290
  if (!rewrittenText) {
23818
24291
  continue;
23819
24292
  }
23820
24293
  part.text = rewrittenText;
24294
+ const parsedSkillName = part.text.match(/\*\*Skill identifier\*\*: ([^\n]+)/)?.[1];
24295
+ if (parsedSkillName) {
24296
+ await notifier.skillLoaded([parsedSkillName]);
24297
+ }
23821
24298
  break;
23822
24299
  }
23823
24300
  },
24301
+ "command.execute.before": async (input, output) => {
24302
+ const rewritten = await rewriteMirroredSkillCommandParts({
24303
+ commandName: input.command,
24304
+ commandArguments: input.arguments,
24305
+ mirroredCommands: mirroredSkillCommands,
24306
+ registry: api2.registry,
24307
+ output
24308
+ });
24309
+ if (!rewritten) {
24310
+ return;
24311
+ }
24312
+ const skill = api2.registry.controller.skills.find((entry) => entry.name === input.command);
24313
+ if (skill) {
24314
+ await notifier.skillLoaded([skill.toolName]);
24315
+ }
24316
+ },
23824
24317
  async event(args) {
23825
24318
  switch (args.event.type) {
23826
- case "message.removed":
23827
- modelIdAccountant.untrackMessage(args.event.properties);
23828
- break;
23829
- case "session.deleted":
23830
- modelIdAccountant.untrackSession(args.event.properties.info.id);
24319
+ case "session.error":
24320
+ await notifier.error("OpenCode session error", "A session error occurred in the current session.");
23831
24321
  break;
23832
24322
  }
23833
24323
  },
@@ -23839,29 +24329,23 @@ var SkillsPlugin = async (ctx) => {
23839
24329
  skill_names: tool.schema.array(tool.schema.string()).describe("An array of skill names to load.")
23840
24330
  },
23841
24331
  execute: async (args, toolCtx) => {
23842
- const messageID = toolCtx.messageID;
23843
- const sessionID = toolCtx.sessionID;
23844
- const modelInfo = modelIdAccountant.getModelInfo({
23845
- messageID,
23846
- sessionID
23847
- });
23848
- const format = getModelFormat({
23849
- modelId: modelInfo?.modelID,
23850
- providerId: modelInfo?.providerID,
23851
- config: config3
23852
- });
23853
- const renderer = promptRenderer.getFormatter(format);
23854
- const results = await api2.loadSkill(args.skill_names);
23855
- for await (const skill of results.loaded) {
23856
- await sendPrompt(renderer({ data: skill, type: "Skill" }), {
23857
- sessionId: toolCtx.sessionID,
23858
- agent: toolCtx.agent
24332
+ try {
24333
+ const results = await api2.loadSkill(args.skill_names);
24334
+ for await (const skill of results.loaded) {
24335
+ await sendPrompt(await formatLoadedSkill({ skill }), {
24336
+ sessionId: toolCtx.sessionID,
24337
+ agent: toolCtx.agent
24338
+ });
24339
+ }
24340
+ await notifier.skillLoaded(results.loaded.map((skill) => skill.toolName));
24341
+ return JSON.stringify({
24342
+ loaded: results.loaded.map((skill) => skill.toolName),
24343
+ not_found: results.notFound
23859
24344
  });
24345
+ } catch (error45) {
24346
+ await notifier.error("skill_use failed", error45 instanceof Error ? error45.message : String(error45));
24347
+ throw error45;
23860
24348
  }
23861
- return JSON.stringify({
23862
- loaded: results.loaded.map((skill) => skill.toolName),
23863
- not_found: results.notFound
23864
- });
23865
24349
  }
23866
24350
  }),
23867
24351
  skill_find: tool({
@@ -23869,21 +24353,9 @@ var SkillsPlugin = async (ctx) => {
23869
24353
  args: {
23870
24354
  query: tool.schema.union([tool.schema.string(), tool.schema.array(tool.schema.string())]).describe("The search query string or array of strings.")
23871
24355
  },
23872
- execute: async (args, toolCtx) => {
23873
- const messageID = toolCtx.messageID;
23874
- const sessionID = toolCtx.sessionID;
23875
- const modelInfo = modelIdAccountant.getModelInfo({
23876
- messageID,
23877
- sessionID
23878
- });
23879
- const format = getModelFormat({
23880
- config: config3,
23881
- modelId: modelInfo?.modelID,
23882
- providerId: modelInfo?.providerID
23883
- });
23884
- const renderer = promptRenderer.getFormatter(format);
24356
+ execute: async (args) => {
23885
24357
  const results = await api2.findSkills(args);
23886
- const output = renderer({
24358
+ const output = renderXmlPrompt({
23887
24359
  data: results,
23888
24360
  type: "SkillSearchResults"
23889
24361
  });
@@ -23896,21 +24368,9 @@ var SkillsPlugin = async (ctx) => {
23896
24368
  task: tool.schema.string().describe("The task or request to recommend skills for."),
23897
24369
  limit: tool.schema.number().int().min(1).max(10).optional().describe("Maximum number of recommendations to return.")
23898
24370
  },
23899
- execute: async (args, toolCtx) => {
23900
- const messageID = toolCtx.messageID;
23901
- const sessionID = toolCtx.sessionID;
23902
- const modelInfo = modelIdAccountant.getModelInfo({
23903
- messageID,
23904
- sessionID
23905
- });
23906
- const format = getModelFormat({
23907
- config: config3,
23908
- modelId: modelInfo?.modelID,
23909
- providerId: modelInfo?.providerID
23910
- });
23911
- const renderer = promptRenderer.getFormatter(format);
24371
+ execute: async (args) => {
23912
24372
  const results = await api2.recommendSkills(args);
23913
- return renderer({
24373
+ return renderXmlPrompt({
23914
24374
  data: results,
23915
24375
  type: "SkillSearchResults"
23916
24376
  });
@@ -23923,31 +24383,25 @@ var SkillsPlugin = async (ctx) => {
23923
24383
  relative_path: tool.schema.string().describe("The relative path to the resource file within the skill directory.")
23924
24384
  },
23925
24385
  execute: async (args, toolCtx) => {
23926
- const messageID = toolCtx.messageID;
23927
- const sessionID = toolCtx.sessionID;
23928
- const modelInfo = modelIdAccountant.getModelInfo({
23929
- messageID,
23930
- sessionID
23931
- });
23932
- const format = getModelFormat({
23933
- config: config3,
23934
- modelId: modelInfo?.modelID,
23935
- providerId: modelInfo?.providerID
23936
- });
23937
- const renderer = promptRenderer.getFormatter(format);
23938
- const result = await api2.readResource(args);
23939
- if (!result.injection) {
23940
- throw new Error("Failed to read resource");
24386
+ try {
24387
+ const result = await api2.readResource(args);
24388
+ if (!result.injection) {
24389
+ throw new Error("Failed to read resource");
24390
+ }
24391
+ await sendPrompt(renderXmlPrompt({ data: result.injection, type: "SkillResource" }), {
24392
+ sessionId: toolCtx.sessionID,
24393
+ agent: toolCtx.agent
24394
+ });
24395
+ await notifier.resourceLoaded(args.skill_name, result.injection.resource_path);
24396
+ return JSON.stringify({
24397
+ result: "Resource injected successfully",
24398
+ resource_path: result.injection.resource_path,
24399
+ resource_mimetype: result.injection.resource_mimetype
24400
+ });
24401
+ } catch (error45) {
24402
+ await notifier.error("skill_resource failed", error45 instanceof Error ? error45.message : String(error45));
24403
+ throw error45;
23941
24404
  }
23942
- await sendPrompt(renderer({ data: result.injection, type: "SkillResource" }), {
23943
- sessionId: toolCtx.sessionID,
23944
- agent: toolCtx.agent
23945
- });
23946
- return JSON.stringify({
23947
- result: "Resource injected successfully",
23948
- resource_path: result.injection.resource_path,
23949
- resource_mimetype: result.injection.resource_mimetype
23950
- });
23951
24405
  }
23952
24406
  })
23953
24407
  }