opencode-dynamic-skills 1.0.2 → 1.2.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,7 +18059,136 @@ function createSkillResourceReader(provider) {
17899
18059
  };
17900
18060
  }
17901
18061
 
18062
+ // src/lib/SkillLinks.ts
18063
+ import path2 from "path";
18064
+ var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
18065
+ function normalizeLinkedSkillPath(target) {
18066
+ const trimmedTarget = target.trim();
18067
+ if (trimmedTarget.length === 0 || trimmedTarget.startsWith("#") || trimmedTarget.startsWith("/") || SCHEME_PATTERN.test(trimmedTarget)) {
18068
+ return null;
18069
+ }
18070
+ const [pathWithoutFragment] = trimmedTarget.split("#", 1);
18071
+ const [pathWithoutQuery] = pathWithoutFragment.split("?", 1);
18072
+ const normalizedPath = path2.posix.normalize(pathWithoutQuery.replace(/\\/g, "/")).replace(/^\.\//, "");
18073
+ if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
18074
+ return null;
18075
+ }
18076
+ return normalizedPath;
18077
+ }
18078
+ function extractSkillLinks(content) {
18079
+ const links = new Map;
18080
+ for (const match of content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
18081
+ const label = match[1]?.trim();
18082
+ const originalPath = match[2]?.trim();
18083
+ if (!label || !originalPath) {
18084
+ continue;
18085
+ }
18086
+ const resourcePath = normalizeLinkedSkillPath(originalPath);
18087
+ if (!resourcePath) {
18088
+ continue;
18089
+ }
18090
+ links.set(`${label}:${resourcePath}`, {
18091
+ label,
18092
+ originalPath,
18093
+ resourcePath
18094
+ });
18095
+ }
18096
+ for (const match of content.matchAll(/(^|[^a-zA-Z0-9])@([a-zA-Z0-9_.\-/]+(?:\/[a-zA-Z0-9_.\-/]+)*)/g)) {
18097
+ const originalPath = match[2]?.trim();
18098
+ if (!originalPath) {
18099
+ continue;
18100
+ }
18101
+ const resourcePath = normalizeLinkedSkillPath(originalPath);
18102
+ if (!resourcePath) {
18103
+ continue;
18104
+ }
18105
+ links.set(`@:${resourcePath}`, {
18106
+ label: `@${resourcePath}`,
18107
+ originalPath: `@${originalPath}`,
18108
+ resourcePath
18109
+ });
18110
+ }
18111
+ return Array.from(links.values());
18112
+ }
18113
+
18114
+ // src/lib/formatLoadedSkill.ts
18115
+ function formatLoadedSkill(args) {
18116
+ const linkedResources = extractSkillLinks(args.skill.content);
18117
+ const output = [
18118
+ `## Skill: ${args.skill.name}`,
18119
+ "",
18120
+ `**Skill identifier**: ${args.skill.toolName}`,
18121
+ `**Base directory**: ${args.skill.fullPath}`
18122
+ ];
18123
+ if (args.invocationName) {
18124
+ output.push(`**Invocation**: /${args.invocationName}`);
18125
+ }
18126
+ if (args.userMessage?.trim()) {
18127
+ output.push(`**User request**: ${args.userMessage.trim()}`);
18128
+ }
18129
+ output.push("", "Relative file references in this skill resolve from the skill root directory.");
18130
+ output.push("Use skill_resource with the exact root-relative path when you need a linked file.");
18131
+ if (args.skill.files.size > 0) {
18132
+ output.push("", "### Available files", "");
18133
+ for (const relativePath of Array.from(args.skill.files.keys()).sort()) {
18134
+ output.push(`- ${relativePath}`);
18135
+ }
18136
+ }
18137
+ if (linkedResources.length > 0) {
18138
+ output.push("", "### Linked files", "");
18139
+ for (const link of linkedResources) {
18140
+ output.push(`- ${link.originalPath} -> ${link.resourcePath}`);
18141
+ }
18142
+ }
18143
+ output.push("", args.skill.content);
18144
+ return output.join(`
18145
+ `);
18146
+ }
18147
+
18148
+ // src/tools/Skill.ts
18149
+ function normalizeSkillSelector2(selector) {
18150
+ return selector.trim().toLowerCase().replace(/[/-]/g, "_");
18151
+ }
18152
+ function findSkill(registry2, selector) {
18153
+ const directMatch = registry2.controller.get(selector);
18154
+ if (directMatch) {
18155
+ return directMatch;
18156
+ }
18157
+ const normalizedSelector = normalizeSkillSelector2(selector);
18158
+ for (const skill of registry2.controller.skills) {
18159
+ if (normalizeSkillSelector2(skill.name) === normalizedSelector || normalizeSkillSelector2(skill.toolName) === normalizedSelector) {
18160
+ return skill;
18161
+ }
18162
+ }
18163
+ return null;
18164
+ }
18165
+ function createSkillTool(registry2) {
18166
+ return tool({
18167
+ description: "Load a dynamic skill by exact name or slash alias without preloading the entire skill catalog. Use skill_find or skill_recommend first if you do not know the skill name.",
18168
+ args: {
18169
+ name: tool.schema.string().describe("The skill name. Use without the leading slash, for example bmad-help."),
18170
+ user_message: tool.schema.string().optional().describe("Optional user request or arguments to apply with the skill.")
18171
+ },
18172
+ async execute(args) {
18173
+ await registry2.controller.ready.whenReady();
18174
+ const skill = findSkill(registry2, args.name.replace(/^\//, ""));
18175
+ if (!skill) {
18176
+ const available = registry2.controller.skills.map((entry) => entry.name).join(", ");
18177
+ throw new Error(`Skill "${args.name}" not found. Available: ${available || "none"}`);
18178
+ }
18179
+ return formatLoadedSkill({
18180
+ skill,
18181
+ invocationName: args.name.replace(/^\//, ""),
18182
+ userMessage: args.user_message
18183
+ });
18184
+ }
18185
+ });
18186
+ }
18187
+
17902
18188
  // src/tools/SkillUser.ts
18189
+ function normalizeSkillSelector3(selector) {
18190
+ return selector.trim().toLowerCase().replace(/[/-]/g, "_");
18191
+ }
17903
18192
  function createSkillLoader(provider) {
17904
18193
  const registry2 = provider.controller;
17905
18194
  async function loadSkills(skillNames) {
@@ -17907,7 +18196,8 @@ function createSkillLoader(provider) {
17907
18196
  const loaded = [];
17908
18197
  const notFound = [];
17909
18198
  for (const name of skillNames) {
17910
- const skill = registry2.get(name);
18199
+ const normalizedName = normalizeSkillSelector3(name);
18200
+ const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector3(candidate.name) === normalizedName || normalizeSkillSelector3(candidate.toolName) === normalizedName);
17911
18201
  if (skill) {
17912
18202
  loaded.push(skill);
17913
18203
  } else {
@@ -17923,7 +18213,7 @@ function createSkillLoader(provider) {
17923
18213
  }
17924
18214
 
17925
18215
  // src/api.ts
17926
- var createApi = async (config2) => {
18216
+ var createApi = async (config2, client) => {
17927
18217
  const logger = createLogger(config2);
17928
18218
  const registry2 = await createSkillRegistry(config2, logger);
17929
18219
  return {
@@ -17931,14 +18221,18 @@ var createApi = async (config2) => {
17931
18221
  logger,
17932
18222
  config: config2,
17933
18223
  findSkills: createSkillFinder(registry2),
17934
- recommendSkills: createSkillRecommender(registry2),
18224
+ recommendSkills: createSkillRecommender(registry2, {
18225
+ client,
18226
+ config: config2.skillRecommend
18227
+ }),
17935
18228
  readResource: createSkillResourceReader(registry2),
17936
- loadSkill: createSkillLoader(registry2)
18229
+ loadSkill: createSkillLoader(registry2),
18230
+ skillTool: createSkillTool(registry2)
17937
18231
  };
17938
18232
  };
17939
18233
 
17940
18234
  // node_modules/bunfig/dist/index.js
17941
- import { existsSync as existsSync2, statSync } from "fs";
18235
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
17942
18236
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
17943
18237
  import { homedir as homedir2 } from "os";
17944
18238
  import { dirname as dirname3, resolve as resolve7 } from "path";
@@ -18060,7 +18354,7 @@ class ConfigCache {
18060
18354
  if (!existsSync2(configPath)) {
18061
18355
  return true;
18062
18356
  }
18063
- const stats = statSync(configPath);
18357
+ const stats = statSync2(configPath);
18064
18358
  return stats.mtime > cachedTimestamp;
18065
18359
  } catch {
18066
18360
  return true;
@@ -18079,7 +18373,7 @@ class ConfigCache {
18079
18373
  }
18080
18374
  setWithFileCheck(configName, value, configPath, customTtl) {
18081
18375
  try {
18082
- const stats = existsSync2(configPath) ? statSync(configPath) : null;
18376
+ const stats = existsSync2(configPath) ? statSync2(configPath) : null;
18083
18377
  const fileTimestamp = stats ? stats.mtime : new Date;
18084
18378
  this.set(configName, { value, fileTimestamp }, configPath, customTtl);
18085
18379
  } catch {
@@ -18399,10 +18693,10 @@ async function loadConfig({
18399
18693
  var defaultConfigDir = resolve(process3.cwd(), "config");
18400
18694
  var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
18401
18695
  function getProjectRoot(filePath, options2 = {}) {
18402
- let path2 = process2.cwd();
18403
- while (path2.includes("storage"))
18404
- path2 = resolve2(path2, "..");
18405
- const finalPath = resolve2(path2, filePath || "");
18696
+ let path3 = process2.cwd();
18697
+ while (path3.includes("storage"))
18698
+ path3 = resolve2(path3, "..");
18699
+ const finalPath = resolve2(path3, filePath || "");
18406
18700
  if (options2?.relative)
18407
18701
  return relative3(process2.cwd(), finalPath);
18408
18702
  return finalPath;
@@ -19849,10 +20143,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
19849
20143
  return config3;
19850
20144
  const envPrefix = name.toUpperCase().replace(/-/g, "_");
19851
20145
  const result = { ...config3 };
19852
- function processObject(obj, path2 = []) {
20146
+ function processObject(obj, path3 = []) {
19853
20147
  const result2 = { ...obj };
19854
20148
  for (const [key, value] of Object.entries(obj)) {
19855
- const envPath = [...path2, key];
20149
+ const envPath = [...path3, key];
19856
20150
  const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
19857
20151
  const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
19858
20152
  const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
@@ -20034,10 +20328,10 @@ async function loadConfig3({
20034
20328
  var defaultConfigDir2 = resolve3(process6.cwd(), "config");
20035
20329
  var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
20036
20330
  function getProjectRoot2(filePath, options2 = {}) {
20037
- let path2 = process7.cwd();
20038
- while (path2.includes("storage"))
20039
- path2 = resolve4(path2, "..");
20040
- const finalPath = resolve4(path2, filePath || "");
20331
+ let path3 = process7.cwd();
20332
+ while (path3.includes("storage"))
20333
+ path3 = resolve4(path3, "..");
20334
+ const finalPath = resolve4(path3, filePath || "");
20041
20335
  if (options2?.relative)
20042
20336
  return relative2(process7.cwd(), finalPath);
20043
20337
  return finalPath;
@@ -21474,10 +21768,10 @@ class EnvVarError extends BunfigError {
21474
21768
 
21475
21769
  class FileSystemError extends BunfigError {
21476
21770
  code = "FILE_SYSTEM_ERROR";
21477
- constructor(operation, path2, cause) {
21478
- super(`File system ${operation} failed for "${path2}": ${cause.message}`, {
21771
+ constructor(operation, path3, cause) {
21772
+ super(`File system ${operation} failed for "${path3}": ${cause.message}`, {
21479
21773
  operation,
21480
- path: path2,
21774
+ path: path3,
21481
21775
  originalError: cause.name,
21482
21776
  originalMessage: cause.message
21483
21777
  });
@@ -21550,8 +21844,8 @@ var ErrorFactory = {
21550
21844
  envVar(envKey, envValue, expectedType, configName) {
21551
21845
  return new EnvVarError(envKey, envValue, expectedType, configName);
21552
21846
  },
21553
- fileSystem(operation, path2, cause) {
21554
- return new FileSystemError(operation, path2, cause);
21847
+ fileSystem(operation, path3, cause) {
21848
+ return new FileSystemError(operation, path3, cause);
21555
21849
  },
21556
21850
  typeGeneration(configDir, outputPath, cause) {
21557
21851
  return new TypeGenerationError(configDir, outputPath, cause);
@@ -21687,9 +21981,9 @@ class EnvProcessor {
21687
21981
  }
21688
21982
  return key.replace(/([A-Z])/g, "_$1").toUpperCase();
21689
21983
  }
21690
- processObject(obj, path2, envPrefix, options2) {
21984
+ processObject(obj, path3, envPrefix, options2) {
21691
21985
  for (const [key, value] of Object.entries(obj)) {
21692
- const envPath = [...path2, key];
21986
+ const envPath = [...path3, key];
21693
21987
  const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
21694
21988
  const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
21695
21989
  const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
@@ -21774,9 +22068,9 @@ class EnvProcessor {
21774
22068
  return this.formatAsText(envVars, configName);
21775
22069
  }
21776
22070
  }
21777
- extractEnvVarInfo(obj, path2, prefix, envVars) {
22071
+ extractEnvVarInfo(obj, path3, prefix, envVars) {
21778
22072
  for (const [key, value] of Object.entries(obj)) {
21779
- const envPath = [...path2, key];
22073
+ const envPath = [...path3, key];
21780
22074
  const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
21781
22075
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
21782
22076
  this.extractEnvVarInfo(value, envPath, prefix, envVars);
@@ -22182,8 +22476,8 @@ class ConfigFileLoader {
22182
22476
  }
22183
22477
  async getFileModificationTime(filePath) {
22184
22478
  try {
22185
- const { statSync: statSync2 } = await import("fs");
22186
- const stats = statSync2(filePath);
22479
+ const { statSync: statSync22 } = await import("fs");
22480
+ const stats = statSync22(filePath);
22187
22481
  return stats.mtime;
22188
22482
  } catch {
22189
22483
  return null;
@@ -22191,15 +22485,15 @@ class ConfigFileLoader {
22191
22485
  }
22192
22486
  async preloadConfigurations(configPaths, options2 = {}) {
22193
22487
  const preloaded = new Map;
22194
- await Promise.allSettled(configPaths.map(async (path2) => {
22488
+ await Promise.allSettled(configPaths.map(async (path3) => {
22195
22489
  try {
22196
- const result = await this.loadFromPath(path2, {}, options2);
22490
+ const result = await this.loadFromPath(path3, {}, options2);
22197
22491
  if (result) {
22198
- preloaded.set(path2, result.config);
22492
+ preloaded.set(path3, result.config);
22199
22493
  }
22200
22494
  } catch (error45) {
22201
22495
  if (options2.verbose) {
22202
- console.warn(`Failed to preload ${path2}:`, error45);
22496
+ console.warn(`Failed to preload ${path3}:`, error45);
22203
22497
  }
22204
22498
  }
22205
22499
  }));
@@ -22278,13 +22572,13 @@ class ConfigValidator {
22278
22572
  warnings
22279
22573
  };
22280
22574
  }
22281
- validateObjectAgainstSchema(value, schema, path2, errors3, warnings, options2) {
22575
+ validateObjectAgainstSchema(value, schema, path3, errors3, warnings, options2) {
22282
22576
  if (options2.validateTypes && schema.type) {
22283
22577
  const actualType = Array.isArray(value) ? "array" : typeof value;
22284
22578
  const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
22285
22579
  if (!expectedTypes.includes(actualType)) {
22286
22580
  errors3.push({
22287
- path: path2,
22581
+ path: path3,
22288
22582
  message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
22289
22583
  expected: expectedTypes.join(" or "),
22290
22584
  actual: actualType,
@@ -22296,7 +22590,7 @@ class ConfigValidator {
22296
22590
  }
22297
22591
  if (schema.enum && !schema.enum.includes(value)) {
22298
22592
  errors3.push({
22299
- path: path2,
22593
+ path: path3,
22300
22594
  message: `Value must be one of: ${schema.enum.join(", ")}`,
22301
22595
  expected: schema.enum.join(", "),
22302
22596
  actual: value,
@@ -22308,7 +22602,7 @@ class ConfigValidator {
22308
22602
  if (typeof value === "string") {
22309
22603
  if (schema.minLength !== undefined && value.length < schema.minLength) {
22310
22604
  errors3.push({
22311
- path: path2,
22605
+ path: path3,
22312
22606
  message: `String length must be at least ${schema.minLength}`,
22313
22607
  expected: `>= ${schema.minLength}`,
22314
22608
  actual: value.length,
@@ -22317,7 +22611,7 @@ class ConfigValidator {
22317
22611
  }
22318
22612
  if (schema.maxLength !== undefined && value.length > schema.maxLength) {
22319
22613
  errors3.push({
22320
- path: path2,
22614
+ path: path3,
22321
22615
  message: `String length must not exceed ${schema.maxLength}`,
22322
22616
  expected: `<= ${schema.maxLength}`,
22323
22617
  actual: value.length,
@@ -22328,7 +22622,7 @@ class ConfigValidator {
22328
22622
  const regex = new RegExp(schema.pattern);
22329
22623
  if (!regex.test(value)) {
22330
22624
  errors3.push({
22331
- path: path2,
22625
+ path: path3,
22332
22626
  message: `String does not match pattern ${schema.pattern}`,
22333
22627
  expected: schema.pattern,
22334
22628
  actual: value,
@@ -22340,7 +22634,7 @@ class ConfigValidator {
22340
22634
  if (typeof value === "number") {
22341
22635
  if (schema.minimum !== undefined && value < schema.minimum) {
22342
22636
  errors3.push({
22343
- path: path2,
22637
+ path: path3,
22344
22638
  message: `Value must be at least ${schema.minimum}`,
22345
22639
  expected: `>= ${schema.minimum}`,
22346
22640
  actual: value,
@@ -22349,7 +22643,7 @@ class ConfigValidator {
22349
22643
  }
22350
22644
  if (schema.maximum !== undefined && value > schema.maximum) {
22351
22645
  errors3.push({
22352
- path: path2,
22646
+ path: path3,
22353
22647
  message: `Value must not exceed ${schema.maximum}`,
22354
22648
  expected: `<= ${schema.maximum}`,
22355
22649
  actual: value,
@@ -22359,7 +22653,7 @@ class ConfigValidator {
22359
22653
  }
22360
22654
  if (Array.isArray(value) && schema.items) {
22361
22655
  for (let i = 0;i < value.length; i++) {
22362
- const itemPath = path2 ? `${path2}[${i}]` : `[${i}]`;
22656
+ const itemPath = path3 ? `${path3}[${i}]` : `[${i}]`;
22363
22657
  this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
22364
22658
  if (options2.stopOnFirstError && errors3.length > 0)
22365
22659
  return;
@@ -22371,7 +22665,7 @@ class ConfigValidator {
22371
22665
  for (const requiredProp of schema.required) {
22372
22666
  if (!(requiredProp in obj)) {
22373
22667
  errors3.push({
22374
- path: path2 ? `${path2}.${requiredProp}` : requiredProp,
22668
+ path: path3 ? `${path3}.${requiredProp}` : requiredProp,
22375
22669
  message: `Missing required property '${requiredProp}'`,
22376
22670
  expected: "required",
22377
22671
  rule: "required"
@@ -22384,7 +22678,7 @@ class ConfigValidator {
22384
22678
  if (schema.properties) {
22385
22679
  for (const [propName, propSchema] of Object.entries(schema.properties)) {
22386
22680
  if (propName in obj) {
22387
- const propPath = path2 ? `${path2}.${propName}` : propName;
22681
+ const propPath = path3 ? `${path3}.${propName}` : propName;
22388
22682
  this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
22389
22683
  if (options2.stopOnFirstError && errors3.length > 0)
22390
22684
  return;
@@ -22396,7 +22690,7 @@ class ConfigValidator {
22396
22690
  for (const propName of Object.keys(obj)) {
22397
22691
  if (!allowedProps.has(propName)) {
22398
22692
  warnings.push({
22399
- path: path2 ? `${path2}.${propName}` : propName,
22693
+ path: path3 ? `${path3}.${propName}` : propName,
22400
22694
  message: `Additional property '${propName}' is not allowed`,
22401
22695
  rule: "additionalProperties"
22402
22696
  });
@@ -22430,12 +22724,12 @@ class ConfigValidator {
22430
22724
  warnings
22431
22725
  };
22432
22726
  }
22433
- validateWithRule(value, rule, path2) {
22727
+ validateWithRule(value, rule, path3) {
22434
22728
  const errors3 = [];
22435
22729
  if (rule.required && (value === undefined || value === null)) {
22436
22730
  errors3.push({
22437
- path: path2,
22438
- message: rule.message || `Property '${path2}' is required`,
22731
+ path: path3,
22732
+ message: rule.message || `Property '${path3}' is required`,
22439
22733
  expected: "required",
22440
22734
  rule: "required"
22441
22735
  });
@@ -22448,7 +22742,7 @@ class ConfigValidator {
22448
22742
  const actualType = Array.isArray(value) ? "array" : typeof value;
22449
22743
  if (actualType !== rule.type) {
22450
22744
  errors3.push({
22451
- path: path2,
22745
+ path: path3,
22452
22746
  message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
22453
22747
  expected: rule.type,
22454
22748
  actual: actualType,
@@ -22460,7 +22754,7 @@ class ConfigValidator {
22460
22754
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22461
22755
  if (length < rule.min) {
22462
22756
  errors3.push({
22463
- path: path2,
22757
+ path: path3,
22464
22758
  message: rule.message || `Value must be at least ${rule.min}`,
22465
22759
  expected: `>= ${rule.min}`,
22466
22760
  actual: length,
@@ -22472,7 +22766,7 @@ class ConfigValidator {
22472
22766
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22473
22767
  if (length > rule.max) {
22474
22768
  errors3.push({
22475
- path: path2,
22769
+ path: path3,
22476
22770
  message: rule.message || `Value must not exceed ${rule.max}`,
22477
22771
  expected: `<= ${rule.max}`,
22478
22772
  actual: length,
@@ -22483,7 +22777,7 @@ class ConfigValidator {
22483
22777
  if (rule.pattern && typeof value === "string") {
22484
22778
  if (!rule.pattern.test(value)) {
22485
22779
  errors3.push({
22486
- path: path2,
22780
+ path: path3,
22487
22781
  message: rule.message || `Value does not match pattern ${rule.pattern}`,
22488
22782
  expected: rule.pattern.toString(),
22489
22783
  actual: value,
@@ -22493,7 +22787,7 @@ class ConfigValidator {
22493
22787
  }
22494
22788
  if (rule.enum && !rule.enum.includes(value)) {
22495
22789
  errors3.push({
22496
- path: path2,
22790
+ path: path3,
22497
22791
  message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
22498
22792
  expected: rule.enum.join(", "),
22499
22793
  actual: value,
@@ -22504,7 +22798,7 @@ class ConfigValidator {
22504
22798
  const customError = rule.validator(value);
22505
22799
  if (customError) {
22506
22800
  errors3.push({
22507
- path: path2,
22801
+ path: path3,
22508
22802
  message: rule.message || customError,
22509
22803
  rule: "custom"
22510
22804
  });
@@ -22512,10 +22806,10 @@ class ConfigValidator {
22512
22806
  }
22513
22807
  return errors3;
22514
22808
  }
22515
- getValueByPath(obj, path2) {
22516
- if (!path2)
22809
+ getValueByPath(obj, path3) {
22810
+ if (!path3)
22517
22811
  return obj;
22518
- const keys = path2.split(".");
22812
+ const keys = path3.split(".");
22519
22813
  let current = obj;
22520
22814
  for (const key of keys) {
22521
22815
  if (current && typeof current === "object" && key in current) {
@@ -22941,10 +23235,10 @@ async function loadConfig5(options2) {
22941
23235
  function applyEnvVarsToConfig2(name, config4, verbose = false) {
22942
23236
  const _envProcessor = new EnvProcessor;
22943
23237
  const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
22944
- function processConfigLevel(obj, path2 = []) {
23238
+ function processConfigLevel(obj, path3 = []) {
22945
23239
  const result = { ...obj };
22946
23240
  for (const [key, value] of Object.entries(obj)) {
22947
- const currentPath = [...path2, key];
23241
+ const currentPath = [...path3, key];
22948
23242
  const envKeys = [
22949
23243
  `${envPrefix}_${currentPath.join("_").toUpperCase()}`,
22950
23244
  `${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
@@ -22989,8 +23283,46 @@ var defaultConfigDir3 = resolve7(process12.cwd(), "config");
22989
23283
  var defaultGeneratedDir3 = resolve7(process12.cwd(), "src/generated");
22990
23284
 
22991
23285
  // src/config.ts
23286
+ import { access as access3, mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
22992
23287
  import { homedir as homedir3 } from "os";
22993
23288
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join4, normalize, resolve as resolve8 } from "path";
23289
+ var DEFAULT_RESERVED_SLASH_COMMANDS = [
23290
+ "agent",
23291
+ "agents",
23292
+ "compact",
23293
+ "connect",
23294
+ "details",
23295
+ "editor",
23296
+ "exit",
23297
+ "export",
23298
+ "fork",
23299
+ "help",
23300
+ "init",
23301
+ "mcp",
23302
+ "model",
23303
+ "models",
23304
+ "new",
23305
+ "open",
23306
+ "redo",
23307
+ "sessions",
23308
+ "share",
23309
+ "skills",
23310
+ "terminal",
23311
+ "themes",
23312
+ "thinking",
23313
+ "undo",
23314
+ "unshare"
23315
+ ];
23316
+ var DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT = [
23317
+ "You are selecting the most relevant dynamic skills for a user request.",
23318
+ "Return strict JSON only with this shape:",
23319
+ '{"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
23320
+ "Only recommend skills from the provided catalog.",
23321
+ "Prefer the smallest set of high-confidence matches."
23322
+ ].join(" ");
23323
+ var MANAGED_PLUGIN_CONFIG_DIRECTORY = join4(homedir3(), ".config", "opencode");
23324
+ var MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
23325
+ var MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
22994
23326
  function getOpenCodeConfigPaths() {
22995
23327
  const home = homedir3();
22996
23328
  const paths = [];
@@ -23010,14 +23342,14 @@ function getOpenCodeConfigPaths() {
23010
23342
  paths.push(join4(home, ".opencode"));
23011
23343
  return paths;
23012
23344
  }
23013
- function expandTildePath(path2) {
23014
- if (path2 === "~") {
23345
+ function expandTildePath(path3) {
23346
+ if (path3 === "~") {
23015
23347
  return homedir3();
23016
23348
  }
23017
- if (path2.startsWith("~/")) {
23018
- return join4(homedir3(), path2.slice(2));
23349
+ if (path3.startsWith("~/")) {
23350
+ return join4(homedir3(), path3.slice(2));
23019
23351
  }
23020
- return path2;
23352
+ return path3;
23021
23353
  }
23022
23354
  var createPathKey = (absolutePath) => {
23023
23355
  const normalizedPath = normalize(absolutePath);
@@ -23081,34 +23413,278 @@ var defaultSkillBasePaths = [
23081
23413
  join4(homedir3(), ".claude", "skills"),
23082
23414
  join4(homedir3(), ".agents", "skills")
23083
23415
  ];
23084
- var options2 = {
23085
- name: "opencode-dynamic-skills",
23086
- cwd: "./",
23087
- defaultConfig: {
23416
+ function createDefaultSkillRecommendConfig() {
23417
+ return {
23418
+ strategy: "heuristic",
23419
+ model: "",
23420
+ systemPrompt: DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
23421
+ };
23422
+ }
23423
+ function createDefaultNotificationConfig() {
23424
+ return {
23425
+ enabled: false,
23426
+ success: true,
23427
+ errors: true
23428
+ };
23429
+ }
23430
+ function createDefaultPluginConfig() {
23431
+ return {
23088
23432
  debug: false,
23089
23433
  basePaths: defaultSkillBasePaths,
23090
- promptRenderer: "xml",
23091
- modelRenderers: {},
23092
23434
  slashCommandName: "skill",
23093
- enableSkillAliases: false
23435
+ enableSkillAliases: true,
23436
+ reservedSlashCommands: [...DEFAULT_RESERVED_SLASH_COMMANDS],
23437
+ notifications: createDefaultNotificationConfig(),
23438
+ skillRecommend: createDefaultSkillRecommendConfig()
23439
+ };
23440
+ }
23441
+ function createConfigOptions(defaultConfig3) {
23442
+ return {
23443
+ name: "opencode-dynamic-skills",
23444
+ cwd: "./",
23445
+ defaultConfig: defaultConfig3
23446
+ };
23447
+ }
23448
+ function stripJsonComments(input) {
23449
+ let output = "";
23450
+ let inString = false;
23451
+ let escaped = false;
23452
+ let inLineComment = false;
23453
+ let inBlockComment = false;
23454
+ for (let index = 0;index < input.length; index += 1) {
23455
+ const current = input[index] ?? "";
23456
+ const next = input[index + 1] ?? "";
23457
+ if (inLineComment) {
23458
+ if (current === `
23459
+ ` || current === "\r") {
23460
+ inLineComment = false;
23461
+ output += current;
23462
+ }
23463
+ continue;
23464
+ }
23465
+ if (inBlockComment) {
23466
+ if (current === "*" && next === "/") {
23467
+ inBlockComment = false;
23468
+ index += 1;
23469
+ continue;
23470
+ }
23471
+ if (current === `
23472
+ ` || current === "\r") {
23473
+ output += current;
23474
+ }
23475
+ continue;
23476
+ }
23477
+ if (inString) {
23478
+ output += current;
23479
+ if (escaped) {
23480
+ escaped = false;
23481
+ continue;
23482
+ }
23483
+ if (current === "\\") {
23484
+ escaped = true;
23485
+ continue;
23486
+ }
23487
+ if (current === '"') {
23488
+ inString = false;
23489
+ }
23490
+ continue;
23491
+ }
23492
+ if (current === "/" && next === "/") {
23493
+ inLineComment = true;
23494
+ index += 1;
23495
+ continue;
23496
+ }
23497
+ if (current === "/" && next === "*") {
23498
+ inBlockComment = true;
23499
+ index += 1;
23500
+ continue;
23501
+ }
23502
+ if (current === '"') {
23503
+ inString = true;
23504
+ }
23505
+ output += current;
23094
23506
  }
23095
- };
23507
+ return output;
23508
+ }
23509
+ function removeTrailingJsonCommas(input) {
23510
+ let output = "";
23511
+ let inString = false;
23512
+ let escaped = false;
23513
+ for (let index = 0;index < input.length; index += 1) {
23514
+ const current = input[index] ?? "";
23515
+ if (inString) {
23516
+ output += current;
23517
+ if (escaped) {
23518
+ escaped = false;
23519
+ continue;
23520
+ }
23521
+ if (current === "\\") {
23522
+ escaped = true;
23523
+ continue;
23524
+ }
23525
+ if (current === '"') {
23526
+ inString = false;
23527
+ }
23528
+ continue;
23529
+ }
23530
+ if (current === '"') {
23531
+ inString = true;
23532
+ output += current;
23533
+ continue;
23534
+ }
23535
+ if (current === ",") {
23536
+ let lookahead = index + 1;
23537
+ while (lookahead < input.length && /\s/.test(input[lookahead] ?? "")) {
23538
+ lookahead += 1;
23539
+ }
23540
+ const nextToken = input[lookahead];
23541
+ if (nextToken === "}" || nextToken === "]") {
23542
+ continue;
23543
+ }
23544
+ }
23545
+ output += current;
23546
+ }
23547
+ return output;
23548
+ }
23549
+ function parseJsonc(input) {
23550
+ return JSON.parse(removeTrailingJsonCommas(stripJsonComments(input)));
23551
+ }
23552
+ function trimString(value) {
23553
+ return value?.trim() ?? "";
23554
+ }
23555
+ function mergePluginConfig(baseConfig, override) {
23556
+ const skillRecommend = {
23557
+ ...baseConfig.skillRecommend,
23558
+ ...override.skillRecommend
23559
+ };
23560
+ const notifications = {
23561
+ ...baseConfig.notifications,
23562
+ ...override.notifications
23563
+ };
23564
+ const overrideReservedSlashCommands = override.reservedSlashCommands?.map((command) => trimString(command)).filter(Boolean) ?? [];
23565
+ return {
23566
+ ...baseConfig,
23567
+ ...override,
23568
+ skillRecommend: {
23569
+ strategy: skillRecommend.strategy === "model" ? "model" : "heuristic",
23570
+ model: trimString(skillRecommend.model),
23571
+ systemPrompt: trimString(skillRecommend.systemPrompt) || DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
23572
+ },
23573
+ reservedSlashCommands: overrideReservedSlashCommands.length > 0 ? overrideReservedSlashCommands : baseConfig.reservedSlashCommands,
23574
+ notifications: {
23575
+ enabled: notifications.enabled === true,
23576
+ success: notifications.success !== false,
23577
+ errors: notifications.errors !== false
23578
+ }
23579
+ };
23580
+ }
23581
+ function getManagedPluginConfigPaths(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
23582
+ return [
23583
+ join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME),
23584
+ join4(configDirectory, MANAGED_PLUGIN_JSON_FILENAME)
23585
+ ];
23586
+ }
23587
+ async function fileExists(filePath) {
23588
+ try {
23589
+ await access3(filePath);
23590
+ return true;
23591
+ } catch {
23592
+ return false;
23593
+ }
23594
+ }
23595
+ function renderManagedPluginConfigJsonc(config3 = createDefaultPluginConfig()) {
23596
+ const basePaths = config3.basePaths.map((basePath) => ` ${JSON.stringify(basePath)}`).join(`,
23597
+ `);
23598
+ return [
23599
+ "{",
23600
+ " // Enable verbose plugin logging output.",
23601
+ ` "debug": ${JSON.stringify(config3.debug)},`,
23602
+ "",
23603
+ " // Global skill roots. Project-local .opencode/.claude/.agents paths are appended automatically.",
23604
+ ' "basePaths": [',
23605
+ basePaths,
23606
+ " ],",
23607
+ "",
23608
+ " // Explicit slash entrypoint, for example: /skill git-release",
23609
+ ` "slashCommandName": ${JSON.stringify(config3.slashCommandName)},`,
23610
+ "",
23611
+ " // When true, /<skill-name> alias invocations can be intercepted after submit.",
23612
+ ` "enableSkillAliases": ${JSON.stringify(config3.enableSkillAliases)},`,
23613
+ "",
23614
+ " // Builtin or protected slash commands that should never be intercepted as /<skill-name> aliases.",
23615
+ ' "reservedSlashCommands": [',
23616
+ config3.reservedSlashCommands.map((command) => ` ${JSON.stringify(command)}`).join(`,
23617
+ `),
23618
+ " ],",
23619
+ "",
23620
+ " // Best-effort OS notifications triggered by this plugin.",
23621
+ ' "notifications": {',
23622
+ ` "enabled": ${JSON.stringify(config3.notifications.enabled)},`,
23623
+ ` "success": ${JSON.stringify(config3.notifications.success)},`,
23624
+ ` "errors": ${JSON.stringify(config3.notifications.errors)}`,
23625
+ " },",
23626
+ "",
23627
+ ' // skill_recommend strategy. Set strategy to "model" and fill model with provider/model to use an internal LLM call.',
23628
+ ' "skillRecommend": {',
23629
+ ` "strategy": ${JSON.stringify(config3.skillRecommend.strategy)},`,
23630
+ " // Model format: provider/model, for example openai/gpt-5.2",
23631
+ ` "model": ${JSON.stringify(config3.skillRecommend.model)},`,
23632
+ ' // The model must return strict JSON: {"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
23633
+ ` "systemPrompt": ${JSON.stringify(config3.skillRecommend.systemPrompt)}`,
23634
+ " }",
23635
+ "}",
23636
+ ""
23637
+ ].join(`
23638
+ `);
23639
+ }
23640
+ async function ensureManagedPluginConfigFile(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
23641
+ for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
23642
+ if (await fileExists(candidatePath)) {
23643
+ return candidatePath;
23644
+ }
23645
+ }
23646
+ await mkdir3(configDirectory, { recursive: true });
23647
+ const configPath = join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME);
23648
+ await writeFile3(configPath, renderManagedPluginConfigJsonc(), "utf8");
23649
+ return configPath;
23650
+ }
23651
+ async function loadManagedPluginConfig(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
23652
+ for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
23653
+ if (!await fileExists(candidatePath)) {
23654
+ continue;
23655
+ }
23656
+ const content = await readFile(candidatePath, "utf8");
23657
+ if (candidatePath.endsWith(".jsonc")) {
23658
+ return parseJsonc(content);
23659
+ }
23660
+ return JSON.parse(content);
23661
+ }
23662
+ return {};
23663
+ }
23096
23664
  async function getPluginConfig(ctx) {
23097
- const resolvedConfig = await loadConfig5(options2);
23665
+ const defaultConfig3 = createDefaultPluginConfig();
23666
+ await ensureManagedPluginConfigFile();
23667
+ const managedConfig = await loadManagedPluginConfig();
23668
+ const resolvedConfig = await loadConfig5(createConfigOptions(defaultConfig3));
23669
+ const mergedConfig = mergePluginConfig(mergePluginConfig(defaultConfig3, managedConfig), resolvedConfig);
23098
23670
  const configuredBasePaths = [
23099
- ...resolvedConfig.basePaths,
23671
+ ...mergedConfig.basePaths,
23100
23672
  ...getProjectSkillBasePaths(ctx.directory, ctx.worktree)
23101
23673
  ];
23102
- resolvedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
23103
- return resolvedConfig;
23674
+ mergedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
23675
+ return mergedConfig;
23104
23676
  }
23105
23677
 
23106
23678
  // src/commands/SlashCommand.ts
23107
23679
  var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
23108
23680
  var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
23109
- function normalizeSkillSelector(selector) {
23681
+ function normalizeSkillSelector4(selector) {
23110
23682
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
23111
23683
  }
23684
+ function isReservedSlashCommand(invocationName, reservedSlashCommands) {
23685
+ const normalizedInvocationName = invocationName.trim().toLowerCase();
23686
+ return reservedSlashCommands.some((reservedCommand) => reservedCommand.trim().toLowerCase() === normalizedInvocationName);
23687
+ }
23112
23688
  function parseSlashCommand(text, slashCommandName) {
23113
23689
  const trimmedText = text.trim();
23114
23690
  if (!trimmedText.startsWith("/")) {
@@ -23143,30 +23719,26 @@ function findSkillBySelector(registry2, selector) {
23143
23719
  if (directMatch) {
23144
23720
  return directMatch;
23145
23721
  }
23146
- const normalizedSelector = normalizeSkillSelector(selector);
23722
+ const normalizedSelector = normalizeSkillSelector4(selector);
23147
23723
  for (const skill of registry2.controller.skills) {
23148
- if (normalizeSkillSelector(skill.toolName) === normalizedSelector) {
23724
+ if (normalizeSkillSelector4(skill.toolName) === normalizedSelector) {
23149
23725
  return skill;
23150
23726
  }
23151
- if (normalizeSkillSelector(skill.name) === normalizedSelector) {
23727
+ if (normalizeSkillSelector4(skill.name) === normalizedSelector) {
23152
23728
  return skill;
23153
23729
  }
23154
23730
  }
23155
23731
  return null;
23156
23732
  }
23157
23733
  function renderSlashSkillPrompt(args) {
23158
- const userPrompt = args.userPrompt || "Apply this skill to the current request.";
23159
23734
  return [
23160
23735
  SLASH_COMMAND_SENTINEL,
23161
- `Slash command: /${args.invocationName}`,
23162
- `Load the following dynamic skill and use it for this request.`,
23163
- `Skill identifier: ${args.skill.toolName}`,
23164
- `Skill root directory: ${args.skill.fullPath}`,
23165
- `Resolve every file reference inside the skill relative to the skill root directory.`,
23166
- `If you need a supporting file, use skill_resource with the exact root-relative path.`,
23167
- `User request: ${userPrompt}`,
23168
- "",
23169
- args.renderedSkill
23736
+ formatLoadedSkill({
23737
+ invocationName: args.invocationName,
23738
+ skill: args.skill,
23739
+ userMessage: args.userPrompt || "Apply this skill to the current request."
23740
+ }),
23741
+ ""
23170
23742
  ].join(`
23171
23743
  `);
23172
23744
  }
@@ -23207,6 +23779,9 @@ async function rewriteSlashCommandText(args) {
23207
23779
  if (isAliasInvocation && !args.enableSkillAliases) {
23208
23780
  return null;
23209
23781
  }
23782
+ if (isAliasInvocation && isReservedSlashCommand(parsedCommand.invocationName, args.reservedSlashCommands ?? [])) {
23783
+ return null;
23784
+ }
23210
23785
  await args.registry.controller.ready.whenReady();
23211
23786
  const skill = findSkillBySelector(args.registry, parsedCommand.skillSelector);
23212
23787
  if (!skill) {
@@ -23214,69 +23789,11 @@ async function rewriteSlashCommandText(args) {
23214
23789
  }
23215
23790
  return renderSlashSkillPrompt({
23216
23791
  invocationName: parsedCommand.invocationName,
23217
- renderedSkill: args.renderSkill(skill),
23218
23792
  skill,
23219
23793
  userPrompt: parsedCommand.userPrompt
23220
23794
  });
23221
23795
  }
23222
23796
 
23223
- // src/lib/SkillLinks.ts
23224
- import path2 from "path";
23225
- var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
23226
- function normalizeLinkedSkillPath(target) {
23227
- const trimmedTarget = target.trim();
23228
- if (trimmedTarget.length === 0 || trimmedTarget.startsWith("#") || trimmedTarget.startsWith("/") || SCHEME_PATTERN.test(trimmedTarget)) {
23229
- return null;
23230
- }
23231
- const [pathWithoutFragment] = trimmedTarget.split("#", 1);
23232
- const [pathWithoutQuery] = pathWithoutFragment.split("?", 1);
23233
- const normalizedPath = path2.posix.normalize(pathWithoutQuery.replace(/\\/g, "/")).replace(/^\.\//, "");
23234
- if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
23235
- return null;
23236
- }
23237
- return normalizedPath;
23238
- }
23239
- function extractSkillLinks(content) {
23240
- const links = new Map;
23241
- for (const match of content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
23242
- const label = match[1]?.trim();
23243
- const originalPath = match[2]?.trim();
23244
- if (!label || !originalPath) {
23245
- continue;
23246
- }
23247
- const resourcePath = normalizeLinkedSkillPath(originalPath);
23248
- if (!resourcePath) {
23249
- continue;
23250
- }
23251
- links.set(`${label}:${resourcePath}`, {
23252
- label,
23253
- originalPath,
23254
- resourcePath
23255
- });
23256
- }
23257
- return Array.from(links.values());
23258
- }
23259
-
23260
- // src/lib/renderers/JsonPromptRenderer.ts
23261
- var createJsonPromptRenderer = () => {
23262
- const renderer = {
23263
- format: "json",
23264
- render(args) {
23265
- if (args.type === "Skill") {
23266
- return JSON.stringify({
23267
- Skill: {
23268
- ...args.data,
23269
- linkedResources: extractSkillLinks(args.data.content),
23270
- skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path."
23271
- }
23272
- }, null, 2);
23273
- }
23274
- return JSON.stringify({ [args.type]: args.data }, null, 2);
23275
- }
23276
- };
23277
- return renderer;
23278
- };
23279
-
23280
23797
  // src/lib/xml.ts
23281
23798
  function escapeXml(str2) {
23282
23799
  return String(str2).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -23330,6 +23847,7 @@ var createXmlPromptRenderer = () => {
23330
23847
  ...skill,
23331
23848
  linkedResources: extractSkillLinks(skill.content),
23332
23849
  skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path.",
23850
+ files: resourceMapToArray(skill.files),
23333
23851
  references: resourceMapToArray(skill.references),
23334
23852
  scripts: resourceMapToArray(skill.scripts),
23335
23853
  assets: resourceMapToArray(skill.assets)
@@ -23360,362 +23878,120 @@ var createXmlPromptRenderer = () => {
23360
23878
  return renderer;
23361
23879
  };
23362
23880
 
23363
- // node_modules/dedent/dist/dedent.mjs
23364
- function ownKeys(object2, enumerableOnly) {
23365
- var keys = Object.keys(object2);
23366
- if (Object.getOwnPropertySymbols) {
23367
- var symbols = Object.getOwnPropertySymbols(object2);
23368
- enumerableOnly && (symbols = symbols.filter(function(sym) {
23369
- return Object.getOwnPropertyDescriptor(object2, sym).enumerable;
23370
- })), keys.push.apply(keys, symbols);
23371
- }
23372
- return keys;
23373
- }
23374
- function _objectSpread(target) {
23375
- for (var i = 1;i < arguments.length; i++) {
23376
- var source = arguments[i] != null ? arguments[i] : {};
23377
- i % 2 ? ownKeys(Object(source), true).forEach(function(key) {
23378
- _defineProperty(target, key, source[key]);
23379
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function(key) {
23380
- Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
23381
- });
23382
- }
23383
- return target;
23881
+ // src/services/Notifier.ts
23882
+ import { execFile } from "child_process";
23883
+ import { promisify } from "util";
23884
+ var execFileAsync = promisify(execFile);
23885
+ var NOTIFICATION_TITLE = "OpenCode Dynamic Skills";
23886
+ function createDefaultRunner() {
23887
+ return async (command, args) => {
23888
+ await execFileAsync(command, args);
23889
+ };
23384
23890
  }
23385
- function _defineProperty(obj, key, value) {
23386
- key = _toPropertyKey(key);
23387
- if (key in obj) {
23388
- Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
23389
- } else {
23390
- obj[key] = value;
23891
+ function truncateNotificationText(value, maxLength = 180) {
23892
+ const normalized = value.replace(/\s+/g, " ").trim();
23893
+ if (normalized.length <= maxLength) {
23894
+ return normalized;
23391
23895
  }
23392
- return obj;
23896
+ return `${normalized.slice(0, maxLength - 1)}\u2026`;
23393
23897
  }
23394
- function _toPropertyKey(arg) {
23395
- var key = _toPrimitive(arg, "string");
23396
- return typeof key === "symbol" ? key : String(key);
23898
+ function escapeAppleScript(value) {
23899
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
23397
23900
  }
23398
- function _toPrimitive(input, hint) {
23399
- if (typeof input !== "object" || input === null)
23400
- return input;
23401
- var prim = input[Symbol.toPrimitive];
23402
- if (prim !== undefined) {
23403
- var res = prim.call(input, hint || "default");
23404
- if (typeof res !== "object")
23405
- return res;
23406
- throw new TypeError("@@toPrimitive must return a primitive value.");
23407
- }
23408
- return (hint === "string" ? String : Number)(input);
23409
- }
23410
- var dedent = createDedent({});
23411
- var dedent_default = dedent;
23412
- function createDedent(options3) {
23413
- dedent2.withOptions = (newOptions) => createDedent(_objectSpread(_objectSpread({}, options3), newOptions));
23414
- return dedent2;
23415
- function dedent2(strings, ...values) {
23416
- const raw = typeof strings === "string" ? [strings] : strings.raw;
23417
- const {
23418
- alignValues = false,
23419
- escapeSpecialCharacters = Array.isArray(strings),
23420
- trimWhitespace = true
23421
- } = options3;
23422
- let result = "";
23423
- for (let i = 0;i < raw.length; i++) {
23424
- let next = raw[i];
23425
- if (escapeSpecialCharacters) {
23426
- next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
23427
- }
23428
- result += next;
23429
- if (i < values.length) {
23430
- const value = alignValues ? alignValue(values[i], result) : values[i];
23431
- result += value;
23432
- }
23433
- }
23434
- const lines = result.split(`
23435
- `);
23436
- let mindent = null;
23437
- for (const l of lines) {
23438
- const m = l.match(/^(\s+)\S+/);
23439
- if (m) {
23440
- const indent = m[1].length;
23441
- if (!mindent) {
23442
- mindent = indent;
23443
- } else {
23444
- mindent = Math.min(mindent, indent);
23445
- }
23446
- }
23447
- }
23448
- if (mindent !== null) {
23449
- const m = mindent;
23450
- result = lines.map((l) => l[0] === " " || l[0] === "\t" ? l.slice(m) : l).join(`
23451
- `);
23452
- }
23453
- if (trimWhitespace) {
23454
- result = result.trim();
23455
- }
23456
- if (escapeSpecialCharacters) {
23457
- result = result.replace(/\\n/g, `
23458
- `).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)));
23459
- }
23460
- if (typeof Bun !== "undefined") {
23461
- result = result.replace(/\\u(?:\{([\da-fA-F]{1,6})\}|([\da-fA-F]{4}))/g, (_, braced, unbraced) => {
23462
- var _ref;
23463
- const hex3 = (_ref = braced !== null && braced !== undefined ? braced : unbraced) !== null && _ref !== undefined ? _ref : "";
23464
- return String.fromCodePoint(parseInt(hex3, 16));
23465
- });
23466
- }
23467
- return result;
23468
- }
23901
+ function escapeXml2(value) {
23902
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
23469
23903
  }
23470
- function alignValue(value, precedingText) {
23471
- if (typeof value !== "string" || !value.includes(`
23472
- `)) {
23473
- return value;
23474
- }
23475
- const currentLine = precedingText.slice(precedingText.lastIndexOf(`
23476
- `) + 1);
23477
- const indentMatch = currentLine.match(/^(\s+)/);
23478
- if (indentMatch) {
23479
- const indent = indentMatch[1];
23480
- return value.replace(/\n/g, `
23481
- ${indent}`);
23904
+ function buildWindowsToastScript(title, message) {
23905
+ const xml = `<toast><visual><binding template='ToastGeneric'><text>${escapeXml2(title)}</text><text>${escapeXml2(message)}</text></binding></visual></toast>`;
23906
+ return [
23907
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
23908
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
23909
+ "$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
23910
+ `$xml.LoadXml(@'${xml}'@)`,
23911
+ "$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
23912
+ `$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${NOTIFICATION_TITLE}')`,
23913
+ "$notifier.Show($toast)"
23914
+ ].join("; ");
23915
+ }
23916
+ function getNotificationCommand(platform, title, message) {
23917
+ switch (platform) {
23918
+ case "darwin":
23919
+ return {
23920
+ command: "osascript",
23921
+ args: [
23922
+ "-e",
23923
+ `display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`
23924
+ ]
23925
+ };
23926
+ case "linux":
23927
+ return {
23928
+ command: "notify-send",
23929
+ args: [title, message]
23930
+ };
23931
+ case "win32":
23932
+ return {
23933
+ command: "powershell",
23934
+ args: ["-NoProfile", "-Command", buildWindowsToastScript(title, message)]
23935
+ };
23936
+ default:
23937
+ return null;
23482
23938
  }
23483
- return value;
23484
23939
  }
23485
-
23486
- // src/lib/renderers/MdPromptRenderer.ts
23487
- var createMdPromptRenderer = () => {
23488
- const renderObject = (obj, headingLevel, indentLevel = 0) => {
23489
- const entries = Object.entries(obj);
23490
- let output = "";
23491
- for (const [key, value] of entries) {
23492
- if (value === null || value === undefined) {
23493
- continue;
23494
- }
23495
- const heading = "#".repeat(headingLevel);
23496
- output += `${heading} ${key}`;
23497
- if (typeof value === "object" && !Array.isArray(value)) {
23498
- output += renderObject(value, Math.min(headingLevel + 1, 6), indentLevel);
23499
- } else if (Array.isArray(value)) {
23500
- output += renderArray(value, indentLevel);
23501
- } else {
23502
- const indent = " ".repeat(indentLevel);
23503
- const escapedValue = htmlEscape(String(value));
23504
- output += `${indent}- **${key}**: *${escapedValue}*`;
23505
- }
23506
- output += `
23507
- `;
23508
- }
23509
- return output;
23510
- };
23511
- const renderArray = (arr, indentLevel) => {
23512
- const indent = " ".repeat(indentLevel);
23513
- let output = "";
23514
- for (const item of arr) {
23515
- if (item === null || item === undefined) {
23516
- continue;
23517
- }
23518
- if (typeof item === "object" && !Array.isArray(item)) {
23519
- const nestedObj = item;
23520
- for (const [key, value] of Object.entries(nestedObj)) {
23521
- if (value === null || value === undefined) {
23522
- continue;
23523
- }
23524
- if (typeof value === "object") {
23525
- if (Array.isArray(value)) {
23526
- output += `${indent}- **${key}**:
23527
- `;
23528
- output += renderArray(value, indentLevel + 1);
23529
- } else {
23530
- output += `${indent}- **${key}**
23531
- `;
23532
- output += renderObject(value, 4, indentLevel + 1);
23533
- }
23534
- } else {
23535
- const escapedValue = htmlEscape(String(value));
23536
- output += `${indent}- **${key}**: *${escapedValue}*
23537
- `;
23538
- }
23539
- }
23540
- } else if (Array.isArray(item)) {
23541
- output += renderArray(item, indentLevel + 1);
23542
- } else {
23543
- const escapedValue = htmlEscape(String(item));
23544
- output += `${indent}- *${escapedValue}*
23545
- `;
23546
- }
23547
- }
23548
- return output;
23549
- };
23550
- const htmlEscape = (value) => {
23551
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
23552
- };
23553
- const renderSkill = (skill) => {
23554
- const linkedResources = extractSkillLinks(skill.content);
23555
- return dedent_default`
23556
- # ${skill.name}
23557
-
23558
- > Skill root:
23559
- > ${skill.fullPath}
23560
-
23561
- Relative file references in this skill resolve from the skill root directory.
23562
- Always use skill_resource with the exact root-relative path for linked files.
23563
-
23564
- ${linkedResources.length > 0 ? dedent_default`
23565
- ## Linked files detected in skill content
23566
-
23567
- ${linkedResources.map((link) => `- [${link.label}](${link.originalPath}) \u2192 use skill_resource with ${link.resourcePath}`).join(`
23568
- `)}
23569
- ` : ""}
23570
-
23571
- ${skill.content}
23572
-
23573
- ## Metadata
23574
-
23575
- ${skill.metadata ? renderObject(skill.metadata, 3) : ""}
23576
-
23577
- ## References
23578
-
23579
- ${skill.references ? renderArray(resourceMapToArray(skill.references), 1) : ""}
23580
-
23581
- ## Scripts
23582
-
23583
- ${skill.scripts ? renderArray(resourceMapToArray(skill.scripts), 1) : ""}
23584
-
23585
- ## Assets
23586
-
23587
- ${skill.assets ? renderArray(resourceMapToArray(skill.assets), 1) : ""}
23588
- `;
23589
- };
23590
- const renderResource = (resource) => {
23591
- return renderObject(resource, 3);
23592
- };
23593
- const renderSearchResult = (result) => {
23594
- return renderObject(result, 3);
23595
- };
23596
- const renderer = {
23597
- format: "md",
23598
- render(args) {
23599
- if (args.type === "Skill") {
23600
- return renderSkill(args.data);
23601
- }
23602
- if (args.type === "SkillResource") {
23603
- return renderResource(args.data);
23604
- }
23605
- if (args.type === "SkillSearchResults") {
23606
- return renderSearchResult(args.data);
23607
- }
23608
- return renderObject({}, 3);
23940
+ function createNotifier(args) {
23941
+ const runner = args.shell ?? createDefaultRunner();
23942
+ const platform = args.platform ?? process.platform;
23943
+ async function notify(title, message) {
23944
+ if (!args.config.enabled) {
23945
+ return;
23609
23946
  }
23610
- };
23611
- return renderer;
23612
- };
23613
-
23614
- // src/lib/createPromptRenderer.ts
23615
- function createPromptRenderer() {
23616
- const renderers = {
23617
- json: createJsonPromptRenderer(),
23618
- xml: createXmlPromptRenderer(),
23619
- md: createMdPromptRenderer()
23620
- };
23621
- const getFormatter = (format) => {
23622
- switch (format) {
23623
- case "json":
23624
- return renderers.json.render;
23625
- case "xml":
23626
- return renderers.xml.render;
23627
- case "md":
23628
- return renderers.md.render;
23629
- default:
23630
- throw new Error(`Unsupported format: ${format}`);
23947
+ const notificationCommand = getNotificationCommand(platform, truncateNotificationText(title, 80), truncateNotificationText(message));
23948
+ if (!notificationCommand) {
23949
+ args.logger.debug("Notifications are not supported on this platform.", platform);
23950
+ return;
23631
23951
  }
23632
- };
23633
- return {
23634
- getFormatter
23635
- };
23636
- }
23637
-
23638
- // src/lib/getModelFormat.ts
23639
- function getModelFormat(args) {
23640
- const { modelId, providerId, config: config3 } = args;
23641
- const modelRenderers = config3.modelRenderers ?? {};
23642
- if (providerId && modelId) {
23643
- const combinedKey = `${providerId}-${modelId}`;
23644
- if (combinedKey in modelRenderers) {
23645
- return modelRenderers[combinedKey];
23952
+ try {
23953
+ await runner(notificationCommand.command, notificationCommand.args);
23954
+ } catch (error45) {
23955
+ args.logger.warn("Failed to send notification.", error45);
23646
23956
  }
23647
23957
  }
23648
- if (modelId && modelId in modelRenderers) {
23649
- return modelRenderers[modelId];
23650
- }
23651
- return config3.promptRenderer;
23652
- }
23653
-
23654
- // src/services/MessageModelIdAccountant.ts
23655
- function createMessageModelIdAccountant() {
23656
- const modelUsage = new Map;
23657
- const track = (info) => {
23658
- if (!modelUsage.has(info.sessionID)) {
23659
- modelUsage.set(info.sessionID, {});
23660
- }
23661
- const sessionMap = modelUsage.get(info.sessionID);
23662
- sessionMap[info.messageID] = {
23663
- modelID: info.modelID,
23664
- providerID: info.providerID
23665
- };
23666
- };
23667
- const untrackMessage = (args) => {
23668
- const sessionMap = modelUsage.get(args.sessionID);
23669
- if (sessionMap && sessionMap[args.messageID]) {
23670
- delete sessionMap[args.messageID];
23671
- if (Object.keys(sessionMap).length === 0) {
23672
- modelUsage.delete(args.sessionID);
23958
+ return {
23959
+ async skillLoaded(skillNames) {
23960
+ if (!args.config.success || skillNames.length === 0) {
23961
+ return;
23962
+ }
23963
+ const count = skillNames.length;
23964
+ const message = count === 1 ? `Injected skill: ${skillNames[0]}` : `Injected ${count} skills: ${skillNames.join(", ")}`;
23965
+ await notify(NOTIFICATION_TITLE, message);
23966
+ },
23967
+ async resourceLoaded(skillName, relativePath) {
23968
+ if (!args.config.success) {
23969
+ return;
23970
+ }
23971
+ await notify(NOTIFICATION_TITLE, `Injected ${relativePath} from ${skillName}`);
23972
+ },
23973
+ async error(title, message) {
23974
+ if (!args.config.errors) {
23975
+ return;
23673
23976
  }
23977
+ await notify(title, message);
23674
23978
  }
23675
23979
  };
23676
- const untrackSession = (sessionID) => {
23677
- modelUsage.delete(sessionID);
23678
- };
23679
- const getModelInfo = (args) => {
23680
- const sessionMap = modelUsage.get(args.sessionID);
23681
- return sessionMap ? sessionMap[args.messageID] : undefined;
23682
- };
23683
- const reset3 = () => {
23684
- modelUsage.clear();
23685
- };
23686
- return {
23687
- reset: reset3,
23688
- track,
23689
- untrackMessage,
23690
- untrackSession,
23691
- getModelInfo
23692
- };
23693
23980
  }
23694
23981
 
23695
23982
  // src/index.ts
23696
23983
  var SkillsPlugin = async (ctx) => {
23697
23984
  const config3 = await getPluginConfig(ctx);
23698
- const api2 = await createApi(config3);
23985
+ const api2 = await createApi(config3, ctx.client);
23699
23986
  const sendPrompt = createInstructionInjector(ctx);
23700
- const promptRenderer = createPromptRenderer();
23701
- const modelIdAccountant = createMessageModelIdAccountant();
23987
+ const renderer = createXmlPromptRenderer().render;
23988
+ const notifier = createNotifier({
23989
+ config: config3.notifications,
23990
+ logger: api2.logger
23991
+ });
23702
23992
  api2.registry.initialise();
23703
23993
  return {
23704
- "chat.message": async (input, output) => {
23705
- if (input.messageID && input.model?.providerID && input.model?.modelID) {
23706
- modelIdAccountant.track({
23707
- messageID: input.messageID,
23708
- providerID: input.model.providerID,
23709
- modelID: input.model.modelID,
23710
- sessionID: input.sessionID
23711
- });
23712
- }
23713
- const format = getModelFormat({
23714
- modelId: input.model?.modelID,
23715
- providerId: input.model?.providerID,
23716
- config: config3
23717
- });
23718
- const renderSkill = promptRenderer.getFormatter(format);
23994
+ "chat.message": async (_input, output) => {
23719
23995
  for (const part of output.parts) {
23720
23996
  if (part.type !== "text") {
23721
23997
  continue;
@@ -23728,57 +24004,53 @@ var SkillsPlugin = async (ctx) => {
23728
24004
  const rewrittenText = await rewriteSlashCommandText({
23729
24005
  text: part.text,
23730
24006
  registry: api2.registry,
23731
- renderSkill: (skill) => renderSkill({ data: skill, type: "Skill" }),
23732
24007
  slashCommandName: config3.slashCommandName,
23733
- enableSkillAliases: config3.enableSkillAliases
24008
+ enableSkillAliases: config3.enableSkillAliases,
24009
+ reservedSlashCommands: config3.reservedSlashCommands
23734
24010
  });
23735
24011
  if (!rewrittenText) {
23736
24012
  continue;
23737
24013
  }
23738
24014
  part.text = rewrittenText;
24015
+ const parsedSkillName = part.text.match(/\*\*Skill identifier\*\*: ([^\n]+)/)?.[1];
24016
+ if (parsedSkillName) {
24017
+ await notifier.skillLoaded([parsedSkillName]);
24018
+ }
23739
24019
  break;
23740
24020
  }
23741
24021
  },
23742
24022
  async event(args) {
23743
24023
  switch (args.event.type) {
23744
- case "message.removed":
23745
- modelIdAccountant.untrackMessage(args.event.properties);
23746
- break;
23747
- case "session.deleted":
23748
- modelIdAccountant.untrackSession(args.event.properties.info.id);
24024
+ case "session.error":
24025
+ await notifier.error("OpenCode session error", "A session error occurred in the current session.");
23749
24026
  break;
23750
24027
  }
23751
24028
  },
23752
24029
  tool: {
24030
+ skill: api2.skillTool,
23753
24031
  skill_use: tool({
23754
24032
  description: "Load one or more skills into the chat. Provide an array of skill names to load them as user messages.",
23755
24033
  args: {
23756
24034
  skill_names: tool.schema.array(tool.schema.string()).describe("An array of skill names to load.")
23757
24035
  },
23758
24036
  execute: async (args, toolCtx) => {
23759
- const messageID = toolCtx.messageID;
23760
- const sessionID = toolCtx.sessionID;
23761
- const modelInfo = modelIdAccountant.getModelInfo({
23762
- messageID,
23763
- sessionID
23764
- });
23765
- const format = getModelFormat({
23766
- modelId: modelInfo?.modelID,
23767
- providerId: modelInfo?.providerID,
23768
- config: config3
23769
- });
23770
- const renderer = promptRenderer.getFormatter(format);
23771
- const results = await api2.loadSkill(args.skill_names);
23772
- for await (const skill of results.loaded) {
23773
- await sendPrompt(renderer({ data: skill, type: "Skill" }), {
23774
- sessionId: toolCtx.sessionID,
23775
- agent: toolCtx.agent
24037
+ try {
24038
+ const results = await api2.loadSkill(args.skill_names);
24039
+ for await (const skill of results.loaded) {
24040
+ await sendPrompt(renderer({ data: skill, type: "Skill" }), {
24041
+ sessionId: toolCtx.sessionID,
24042
+ agent: toolCtx.agent
24043
+ });
24044
+ }
24045
+ await notifier.skillLoaded(results.loaded.map((skill) => skill.toolName));
24046
+ return JSON.stringify({
24047
+ loaded: results.loaded.map((skill) => skill.toolName),
24048
+ not_found: results.notFound
23776
24049
  });
24050
+ } catch (error45) {
24051
+ await notifier.error("skill_use failed", error45 instanceof Error ? error45.message : String(error45));
24052
+ throw error45;
23777
24053
  }
23778
- return JSON.stringify({
23779
- loaded: results.loaded.map((skill) => skill.toolName),
23780
- not_found: results.notFound
23781
- });
23782
24054
  }
23783
24055
  }),
23784
24056
  skill_find: tool({
@@ -23786,19 +24058,7 @@ var SkillsPlugin = async (ctx) => {
23786
24058
  args: {
23787
24059
  query: tool.schema.union([tool.schema.string(), tool.schema.array(tool.schema.string())]).describe("The search query string or array of strings.")
23788
24060
  },
23789
- execute: async (args, toolCtx) => {
23790
- const messageID = toolCtx.messageID;
23791
- const sessionID = toolCtx.sessionID;
23792
- const modelInfo = modelIdAccountant.getModelInfo({
23793
- messageID,
23794
- sessionID
23795
- });
23796
- const format = getModelFormat({
23797
- config: config3,
23798
- modelId: modelInfo?.modelID,
23799
- providerId: modelInfo?.providerID
23800
- });
23801
- const renderer = promptRenderer.getFormatter(format);
24061
+ execute: async (args) => {
23802
24062
  const results = await api2.findSkills(args);
23803
24063
  const output = renderer({
23804
24064
  data: results,
@@ -23813,19 +24073,7 @@ var SkillsPlugin = async (ctx) => {
23813
24073
  task: tool.schema.string().describe("The task or request to recommend skills for."),
23814
24074
  limit: tool.schema.number().int().min(1).max(10).optional().describe("Maximum number of recommendations to return.")
23815
24075
  },
23816
- execute: async (args, toolCtx) => {
23817
- const messageID = toolCtx.messageID;
23818
- const sessionID = toolCtx.sessionID;
23819
- const modelInfo = modelIdAccountant.getModelInfo({
23820
- messageID,
23821
- sessionID
23822
- });
23823
- const format = getModelFormat({
23824
- config: config3,
23825
- modelId: modelInfo?.modelID,
23826
- providerId: modelInfo?.providerID
23827
- });
23828
- const renderer = promptRenderer.getFormatter(format);
24076
+ execute: async (args) => {
23829
24077
  const results = await api2.recommendSkills(args);
23830
24078
  return renderer({
23831
24079
  data: results,
@@ -23840,31 +24088,25 @@ var SkillsPlugin = async (ctx) => {
23840
24088
  relative_path: tool.schema.string().describe("The relative path to the resource file within the skill directory.")
23841
24089
  },
23842
24090
  execute: async (args, toolCtx) => {
23843
- const messageID = toolCtx.messageID;
23844
- const sessionID = toolCtx.sessionID;
23845
- const modelInfo = modelIdAccountant.getModelInfo({
23846
- messageID,
23847
- sessionID
23848
- });
23849
- const format = getModelFormat({
23850
- config: config3,
23851
- modelId: modelInfo?.modelID,
23852
- providerId: modelInfo?.providerID
23853
- });
23854
- const renderer = promptRenderer.getFormatter(format);
23855
- const result = await api2.readResource(args);
23856
- if (!result.injection) {
23857
- throw new Error("Failed to read resource");
24091
+ try {
24092
+ const result = await api2.readResource(args);
24093
+ if (!result.injection) {
24094
+ throw new Error("Failed to read resource");
24095
+ }
24096
+ await sendPrompt(renderer({ data: result.injection, type: "SkillResource" }), {
24097
+ sessionId: toolCtx.sessionID,
24098
+ agent: toolCtx.agent
24099
+ });
24100
+ await notifier.resourceLoaded(args.skill_name, result.injection.resource_path);
24101
+ return JSON.stringify({
24102
+ result: "Resource injected successfully",
24103
+ resource_path: result.injection.resource_path,
24104
+ resource_mimetype: result.injection.resource_mimetype
24105
+ });
24106
+ } catch (error45) {
24107
+ await notifier.error("skill_resource failed", error45 instanceof Error ? error45.message : String(error45));
24108
+ throw error45;
23858
24109
  }
23859
- await sendPrompt(renderer({ data: result.injection, type: "SkillResource" }), {
23860
- sessionId: toolCtx.sessionID,
23861
- agent: toolCtx.agent
23862
- });
23863
- return JSON.stringify({
23864
- result: "Resource injected successfully",
23865
- resource_path: result.injection.resource_path,
23866
- resource_mimetype: result.injection.resource_mimetype
23867
- });
23868
24110
  }
23869
24111
  })
23870
24112
  }