opencode-dynamic-skills 1.1.0 → 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
  }
@@ -17968,6 +18128,12 @@ function formatLoadedSkill(args) {
17968
18128
  }
17969
18129
  output.push("", "Relative file references in this skill resolve from the skill root directory.");
17970
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
+ }
17971
18137
  if (linkedResources.length > 0) {
17972
18138
  output.push("", "### Linked files", "");
17973
18139
  for (const link of linkedResources) {
@@ -17980,7 +18146,7 @@ function formatLoadedSkill(args) {
17980
18146
  }
17981
18147
 
17982
18148
  // src/tools/Skill.ts
17983
- function normalizeSkillSelector(selector) {
18149
+ function normalizeSkillSelector2(selector) {
17984
18150
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
17985
18151
  }
17986
18152
  function findSkill(registry2, selector) {
@@ -17988,9 +18154,9 @@ function findSkill(registry2, selector) {
17988
18154
  if (directMatch) {
17989
18155
  return directMatch;
17990
18156
  }
17991
- const normalizedSelector = normalizeSkillSelector(selector);
18157
+ const normalizedSelector = normalizeSkillSelector2(selector);
17992
18158
  for (const skill of registry2.controller.skills) {
17993
- if (normalizeSkillSelector(skill.name) === normalizedSelector || normalizeSkillSelector(skill.toolName) === normalizedSelector) {
18159
+ if (normalizeSkillSelector2(skill.name) === normalizedSelector || normalizeSkillSelector2(skill.toolName) === normalizedSelector) {
17994
18160
  return skill;
17995
18161
  }
17996
18162
  }
@@ -18020,7 +18186,7 @@ function createSkillTool(registry2) {
18020
18186
  }
18021
18187
 
18022
18188
  // src/tools/SkillUser.ts
18023
- function normalizeSkillSelector2(selector) {
18189
+ function normalizeSkillSelector3(selector) {
18024
18190
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
18025
18191
  }
18026
18192
  function createSkillLoader(provider) {
@@ -18030,8 +18196,8 @@ function createSkillLoader(provider) {
18030
18196
  const loaded = [];
18031
18197
  const notFound = [];
18032
18198
  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);
18199
+ const normalizedName = normalizeSkillSelector3(name);
18200
+ const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector3(candidate.name) === normalizedName || normalizeSkillSelector3(candidate.toolName) === normalizedName);
18035
18201
  if (skill) {
18036
18202
  loaded.push(skill);
18037
18203
  } else {
@@ -18047,7 +18213,7 @@ function createSkillLoader(provider) {
18047
18213
  }
18048
18214
 
18049
18215
  // src/api.ts
18050
- var createApi = async (config2) => {
18216
+ var createApi = async (config2, client) => {
18051
18217
  const logger = createLogger(config2);
18052
18218
  const registry2 = await createSkillRegistry(config2, logger);
18053
18219
  return {
@@ -18055,7 +18221,10 @@ var createApi = async (config2) => {
18055
18221
  logger,
18056
18222
  config: config2,
18057
18223
  findSkills: createSkillFinder(registry2),
18058
- recommendSkills: createSkillRecommender(registry2),
18224
+ recommendSkills: createSkillRecommender(registry2, {
18225
+ client,
18226
+ config: config2.skillRecommend
18227
+ }),
18059
18228
  readResource: createSkillResourceReader(registry2),
18060
18229
  loadSkill: createSkillLoader(registry2),
18061
18230
  skillTool: createSkillTool(registry2)
@@ -18063,7 +18232,7 @@ var createApi = async (config2) => {
18063
18232
  };
18064
18233
 
18065
18234
  // node_modules/bunfig/dist/index.js
18066
- import { existsSync as existsSync2, statSync } from "fs";
18235
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
18067
18236
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
18068
18237
  import { homedir as homedir2 } from "os";
18069
18238
  import { dirname as dirname3, resolve as resolve7 } from "path";
@@ -18185,7 +18354,7 @@ class ConfigCache {
18185
18354
  if (!existsSync2(configPath)) {
18186
18355
  return true;
18187
18356
  }
18188
- const stats = statSync(configPath);
18357
+ const stats = statSync2(configPath);
18189
18358
  return stats.mtime > cachedTimestamp;
18190
18359
  } catch {
18191
18360
  return true;
@@ -18204,7 +18373,7 @@ class ConfigCache {
18204
18373
  }
18205
18374
  setWithFileCheck(configName, value, configPath, customTtl) {
18206
18375
  try {
18207
- const stats = existsSync2(configPath) ? statSync(configPath) : null;
18376
+ const stats = existsSync2(configPath) ? statSync2(configPath) : null;
18208
18377
  const fileTimestamp = stats ? stats.mtime : new Date;
18209
18378
  this.set(configName, { value, fileTimestamp }, configPath, customTtl);
18210
18379
  } catch {
@@ -22307,8 +22476,8 @@ class ConfigFileLoader {
22307
22476
  }
22308
22477
  async getFileModificationTime(filePath) {
22309
22478
  try {
22310
- const { statSync: statSync2 } = await import("fs");
22311
- const stats = statSync2(filePath);
22479
+ const { statSync: statSync22 } = await import("fs");
22480
+ const stats = statSync22(filePath);
22312
22481
  return stats.mtime;
22313
22482
  } catch {
22314
22483
  return null;
@@ -23114,8 +23283,46 @@ var defaultConfigDir3 = resolve7(process12.cwd(), "config");
23114
23283
  var defaultGeneratedDir3 = resolve7(process12.cwd(), "src/generated");
23115
23284
 
23116
23285
  // src/config.ts
23286
+ import { access as access3, mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
23117
23287
  import { homedir as homedir3 } from "os";
23118
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";
23119
23326
  function getOpenCodeConfigPaths() {
23120
23327
  const home = homedir3();
23121
23328
  const paths = [];
@@ -23206,34 +23413,278 @@ var defaultSkillBasePaths = [
23206
23413
  join4(homedir3(), ".claude", "skills"),
23207
23414
  join4(homedir3(), ".agents", "skills")
23208
23415
  ];
23209
- var options2 = {
23210
- name: "opencode-dynamic-skills",
23211
- cwd: "./",
23212
- 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 {
23213
23432
  debug: false,
23214
23433
  basePaths: defaultSkillBasePaths,
23215
- promptRenderer: "xml",
23216
- modelRenderers: {},
23217
23434
  slashCommandName: "skill",
23218
- enableSkillAliases: true
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;
23219
23506
  }
23220
- };
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
+ }
23221
23664
  async function getPluginConfig(ctx) {
23222
- 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);
23223
23670
  const configuredBasePaths = [
23224
- ...resolvedConfig.basePaths,
23671
+ ...mergedConfig.basePaths,
23225
23672
  ...getProjectSkillBasePaths(ctx.directory, ctx.worktree)
23226
23673
  ];
23227
- resolvedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
23228
- return resolvedConfig;
23674
+ mergedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
23675
+ return mergedConfig;
23229
23676
  }
23230
23677
 
23231
23678
  // src/commands/SlashCommand.ts
23232
23679
  var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
23233
23680
  var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
23234
- function normalizeSkillSelector3(selector) {
23681
+ function normalizeSkillSelector4(selector) {
23235
23682
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
23236
23683
  }
23684
+ function isReservedSlashCommand(invocationName, reservedSlashCommands) {
23685
+ const normalizedInvocationName = invocationName.trim().toLowerCase();
23686
+ return reservedSlashCommands.some((reservedCommand) => reservedCommand.trim().toLowerCase() === normalizedInvocationName);
23687
+ }
23237
23688
  function parseSlashCommand(text, slashCommandName) {
23238
23689
  const trimmedText = text.trim();
23239
23690
  if (!trimmedText.startsWith("/")) {
@@ -23268,12 +23719,12 @@ function findSkillBySelector(registry2, selector) {
23268
23719
  if (directMatch) {
23269
23720
  return directMatch;
23270
23721
  }
23271
- const normalizedSelector = normalizeSkillSelector3(selector);
23722
+ const normalizedSelector = normalizeSkillSelector4(selector);
23272
23723
  for (const skill of registry2.controller.skills) {
23273
- if (normalizeSkillSelector3(skill.toolName) === normalizedSelector) {
23724
+ if (normalizeSkillSelector4(skill.toolName) === normalizedSelector) {
23274
23725
  return skill;
23275
23726
  }
23276
- if (normalizeSkillSelector3(skill.name) === normalizedSelector) {
23727
+ if (normalizeSkillSelector4(skill.name) === normalizedSelector) {
23277
23728
  return skill;
23278
23729
  }
23279
23730
  }
@@ -23328,6 +23779,9 @@ async function rewriteSlashCommandText(args) {
23328
23779
  if (isAliasInvocation && !args.enableSkillAliases) {
23329
23780
  return null;
23330
23781
  }
23782
+ if (isAliasInvocation && isReservedSlashCommand(parsedCommand.invocationName, args.reservedSlashCommands ?? [])) {
23783
+ return null;
23784
+ }
23331
23785
  await args.registry.controller.ready.whenReady();
23332
23786
  const skill = findSkillBySelector(args.registry, parsedCommand.skillSelector);
23333
23787
  if (!skill) {
@@ -23340,26 +23794,6 @@ async function rewriteSlashCommandText(args) {
23340
23794
  });
23341
23795
  }
23342
23796
 
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
- };
23362
-
23363
23797
  // src/lib/xml.ts
23364
23798
  function escapeXml(str2) {
23365
23799
  return String(str2).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -23413,6 +23847,7 @@ var createXmlPromptRenderer = () => {
23413
23847
  ...skill,
23414
23848
  linkedResources: extractSkillLinks(skill.content),
23415
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),
23416
23851
  references: resourceMapToArray(skill.references),
23417
23852
  scripts: resourceMapToArray(skill.scripts),
23418
23853
  assets: resourceMapToArray(skill.assets)
@@ -23443,362 +23878,120 @@ var createXmlPromptRenderer = () => {
23443
23878
  return renderer;
23444
23879
  };
23445
23880
 
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;
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
+ };
23467
23890
  }
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;
23891
+ function truncateNotificationText(value, maxLength = 180) {
23892
+ const normalized = value.replace(/\s+/g, " ").trim();
23893
+ if (normalized.length <= maxLength) {
23894
+ return normalized;
23474
23895
  }
23475
- return obj;
23896
+ return `${normalized.slice(0, maxLength - 1)}\u2026`;
23476
23897
  }
23477
- function _toPropertyKey(arg) {
23478
- var key = _toPrimitive(arg, "string");
23479
- return typeof key === "symbol" ? key : String(key);
23898
+ function escapeAppleScript(value) {
23899
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
23480
23900
  }
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
- }
23901
+ function escapeXml2(value) {
23902
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
23552
23903
  }
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}`);
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;
23565
23938
  }
23566
- return value;
23567
23939
  }
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
- `;
23591
- }
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
- }
23630
- }
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);
23684
- }
23685
- if (args.type === "SkillResource") {
23686
- return renderResource(args.data);
23687
- }
23688
- if (args.type === "SkillSearchResults") {
23689
- return renderSearchResult(args.data);
23690
- }
23691
- 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;
23692
23946
  }
23693
- };
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}`);
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;
23714
23951
  }
23715
- };
23716
- return {
23717
- getFormatter
23718
- };
23719
- }
23720
-
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];
23952
+ try {
23953
+ await runner(notificationCommand.command, notificationCommand.args);
23954
+ } catch (error45) {
23955
+ args.logger.warn("Failed to send notification.", error45);
23729
23956
  }
23730
23957
  }
23731
- if (modelId && modelId in modelRenderers) {
23732
- return modelRenderers[modelId];
23733
- }
23734
- return config3.promptRenderer;
23735
- }
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
23748
- };
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);
23958
+ return {
23959
+ async skillLoaded(skillNames) {
23960
+ if (!args.config.success || skillNames.length === 0) {
23961
+ return;
23756
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;
23976
+ }
23977
+ await notify(title, message);
23757
23978
  }
23758
23979
  };
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
- };
23769
- return {
23770
- reset: reset3,
23771
- track,
23772
- untrackMessage,
23773
- untrackSession,
23774
- getModelInfo
23775
- };
23776
23980
  }
23777
23981
 
23778
23982
  // src/index.ts
23779
23983
  var SkillsPlugin = async (ctx) => {
23780
23984
  const config3 = await getPluginConfig(ctx);
23781
- const api2 = await createApi(config3);
23985
+ const api2 = await createApi(config3, ctx.client);
23782
23986
  const sendPrompt = createInstructionInjector(ctx);
23783
- const promptRenderer = createPromptRenderer();
23784
- const modelIdAccountant = createMessageModelIdAccountant();
23987
+ const renderer = createXmlPromptRenderer().render;
23988
+ const notifier = createNotifier({
23989
+ config: config3.notifications,
23990
+ logger: api2.logger
23991
+ });
23785
23992
  api2.registry.initialise();
23786
23993
  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
23794
- });
23795
- }
23796
- const format = getModelFormat({
23797
- modelId: input.model?.modelID,
23798
- providerId: input.model?.providerID,
23799
- config: config3
23800
- });
23801
- promptRenderer.getFormatter(format);
23994
+ "chat.message": async (_input, output) => {
23802
23995
  for (const part of output.parts) {
23803
23996
  if (part.type !== "text") {
23804
23997
  continue;
@@ -23812,22 +24005,24 @@ var SkillsPlugin = async (ctx) => {
23812
24005
  text: part.text,
23813
24006
  registry: api2.registry,
23814
24007
  slashCommandName: config3.slashCommandName,
23815
- enableSkillAliases: config3.enableSkillAliases
24008
+ enableSkillAliases: config3.enableSkillAliases,
24009
+ reservedSlashCommands: config3.reservedSlashCommands
23816
24010
  });
23817
24011
  if (!rewrittenText) {
23818
24012
  continue;
23819
24013
  }
23820
24014
  part.text = rewrittenText;
24015
+ const parsedSkillName = part.text.match(/\*\*Skill identifier\*\*: ([^\n]+)/)?.[1];
24016
+ if (parsedSkillName) {
24017
+ await notifier.skillLoaded([parsedSkillName]);
24018
+ }
23821
24019
  break;
23822
24020
  }
23823
24021
  },
23824
24022
  async event(args) {
23825
24023
  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);
24024
+ case "session.error":
24025
+ await notifier.error("OpenCode session error", "A session error occurred in the current session.");
23831
24026
  break;
23832
24027
  }
23833
24028
  },
@@ -23839,29 +24034,23 @@ var SkillsPlugin = async (ctx) => {
23839
24034
  skill_names: tool.schema.array(tool.schema.string()).describe("An array of skill names to load.")
23840
24035
  },
23841
24036
  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
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
23859
24049
  });
24050
+ } catch (error45) {
24051
+ await notifier.error("skill_use failed", error45 instanceof Error ? error45.message : String(error45));
24052
+ throw error45;
23860
24053
  }
23861
- return JSON.stringify({
23862
- loaded: results.loaded.map((skill) => skill.toolName),
23863
- not_found: results.notFound
23864
- });
23865
24054
  }
23866
24055
  }),
23867
24056
  skill_find: tool({
@@ -23869,19 +24058,7 @@ var SkillsPlugin = async (ctx) => {
23869
24058
  args: {
23870
24059
  query: tool.schema.union([tool.schema.string(), tool.schema.array(tool.schema.string())]).describe("The search query string or array of strings.")
23871
24060
  },
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);
24061
+ execute: async (args) => {
23885
24062
  const results = await api2.findSkills(args);
23886
24063
  const output = renderer({
23887
24064
  data: results,
@@ -23896,19 +24073,7 @@ var SkillsPlugin = async (ctx) => {
23896
24073
  task: tool.schema.string().describe("The task or request to recommend skills for."),
23897
24074
  limit: tool.schema.number().int().min(1).max(10).optional().describe("Maximum number of recommendations to return.")
23898
24075
  },
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);
24076
+ execute: async (args) => {
23912
24077
  const results = await api2.recommendSkills(args);
23913
24078
  return renderer({
23914
24079
  data: results,
@@ -23923,31 +24088,25 @@ var SkillsPlugin = async (ctx) => {
23923
24088
  relative_path: tool.schema.string().describe("The relative path to the resource file within the skill directory.")
23924
24089
  },
23925
24090
  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");
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;
23941
24109
  }
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
24110
  }
23952
24111
  })
23953
24112
  }