opencode-dynamic-skills 1.0.1 → 1.1.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/README.md CHANGED
@@ -21,9 +21,14 @@ This project is a reboot of the archived `opencode-skillful` codebase.
21
21
  This project is derived from and adapted from:
22
22
 
23
23
  - https://github.com/zenobi-us/opencode-skillful
24
+ - https://github.com/code-yeongyu/oh-my-openagent
25
+ - Anthropic Agent Skills specification and skills ecosystem: https://github.com/anthropics/skills
24
26
 
25
27
  The current codebase restructures and extends that work for a fresh restart.
26
28
 
29
+ This project also references the slash-command and skill-loading ideas explored by oh-my-openagent.
30
+ Thanks to the maintainers and contributors of these projects for the public implementation ideas and prior art.
31
+
27
32
  ## Goals
28
33
 
29
34
  - Keep skills **lazy-loaded**, instead of injecting all skills into every session
@@ -37,11 +42,16 @@ The current codebase restructures and extends that work for a fresh restart.
37
42
 
38
43
  The plugin provides:
39
44
 
45
+ - `skill`
40
46
  - `skill_find`
41
47
  - `skill_recommend`
42
48
  - `skill_use`
43
49
  - `skill_resource`
44
50
 
51
+ The default design keeps the initial tool context small.
52
+ It does **not** inject the full skill catalog or every skill description up front.
53
+ Discovery is deferred to runtime through `skill_find`, `skill_recommend`, direct slash interception, or explicit `skill(name="...")` calls.
54
+
45
55
  ### Slash skill command
46
56
 
47
57
  Default form:
@@ -68,7 +78,8 @@ Optional aliases are also supported:
68
78
  /git-release draft a release checklist
69
79
  ```
70
80
 
71
- Aliases are disabled by default to avoid collisions with native or official OpenCode commands.
81
+ Aliases are enabled by default in the pure oh-my-openagent-style flow, so `/<skill-name>` can be intercepted after submit.
82
+ This does not mean OpenCode will show dynamic picker suggestions for those aliases.
72
83
 
73
84
  ### Skill-root-relative resources
74
85
 
@@ -112,7 +123,7 @@ Example:
112
123
  "gpt-4": "json"
113
124
  },
114
125
  "slashCommandName": "skill",
115
- "enableSkillAliases": false
126
+ "enableSkillAliases": true
116
127
  }
117
128
  ```
118
129
 
package/README.zh-CN.md CHANGED
@@ -21,9 +21,14 @@
21
21
  本项目来自并改造于:
22
22
 
23
23
  - https://github.com/zenobi-us/opencode-skillful
24
+ - https://github.com/code-yeongyu/oh-my-openagent
25
+ - Anthropic Agent Skills 规范与 skills 生态:https://github.com/anthropics/skills
24
26
 
25
27
  当前版本在此基础上做了重新组织和重启开发。
26
28
 
29
+ 其中 slash 风格 skill 调用与 skill 装载思路,明确参考了 oh-my-openagent 的公开实现。
30
+ 感谢这些项目的维护者与贡献者。
31
+
27
32
  ## 目标
28
33
 
29
34
  - 保持 **按需加载**,不把所有 skill 预注入到每个会话
@@ -37,11 +42,16 @@
37
42
 
38
43
  插件提供:
39
44
 
45
+ - `skill`
40
46
  - `skill_find`
41
47
  - `skill_recommend`
42
48
  - `skill_use`
43
49
  - `skill_resource`
44
50
 
51
+ 默认设计会尽量保持初始工具上下文很小。
52
+ 它**不会**在一开始把全部 skill 目录和每个 skill 的 description 都注入进去。
53
+ 真正的发现与装载会延后到运行时,通过 `skill_find`、`skill_recommend`、直接 slash 拦截或显式 `skill(name="...")` 调用完成。
54
+
45
55
  ### Slash 技能命令
46
56
 
47
57
  默认形式:
@@ -68,7 +78,8 @@
68
78
  /git-release 帮我起草 release checklist
69
79
  ```
70
80
 
71
- alias 默认关闭,避免与 OpenCode 内置命令或官方 commands 冲突。
81
+ 在纯 oh-my-openagent 风格下,alias 默认开启,因此提交 `/<skill-name>` 后插件会尝试接管。
82
+ 但这不代表 OpenCode 会在输入阶段为这些 alias 提供动态候选列表。
72
83
 
73
84
  ### Skill 根目录相对资源
74
85
 
@@ -112,7 +123,7 @@ templates/pr.md
112
123
  "gpt-4": "json"
113
124
  },
114
125
  "slashCommandName": "skill",
115
- "enableSkillAliases": false
126
+ "enableSkillAliases": true
116
127
  }
117
128
  ```
118
129
 
package/dist/api.d.ts CHANGED
@@ -34,63 +34,22 @@
34
34
  * // Note: registry is created but NOT yet initialized
35
35
  * // Must be done by caller: await registry.initialise()
36
36
  */
37
+ import { createLogger } from './services/logger';
38
+ import { createSkillRegistry } from './services/SkillRegistry';
39
+ import { createSkillFinder } from './tools/SkillFinder';
40
+ import { createSkillRecommender } from './tools/SkillRecommender';
41
+ import { createSkillResourceReader } from './tools/SkillResourceReader';
42
+ import { createSkillTool } from './tools/Skill';
43
+ import { createSkillLoader } from './tools/SkillUser';
37
44
  import type { PluginConfig } from './types';
38
- export declare const createApi: (config: PluginConfig) => Promise<{
39
- registry: import("./types").SkillRegistry;
40
- logger: import("./types").PluginLogger;
45
+ export type SkillsApi = {
46
+ registry: Awaited<ReturnType<typeof createSkillRegistry>>;
47
+ logger: ReturnType<typeof createLogger>;
41
48
  config: PluginConfig;
42
- findSkills: (args: {
43
- query: string | string[];
44
- }) => Promise<{
45
- query: string | string[];
46
- skills: {
47
- name: string;
48
- description: string;
49
- }[];
50
- summary: {
51
- total: number;
52
- matches: number;
53
- feedback: string;
54
- };
55
- debug: import("./types").SkillRegistryDebugInfo | undefined;
56
- }>;
57
- recommendSkills: (args: {
58
- task: string;
59
- limit?: number;
60
- }) => Promise<{
61
- mode: "recommend";
62
- query: string;
63
- skills: {
64
- name: string;
65
- description: string;
66
- }[];
67
- recommendations: {
68
- name: string;
69
- description: string;
70
- score: number;
71
- reason: string;
72
- }[];
73
- guidance: string;
74
- summary: {
75
- total: number;
76
- matches: number;
77
- feedback: string;
78
- };
79
- debug: import("./types").SkillRegistryDebugInfo | undefined;
80
- }>;
81
- readResource: (args: {
82
- skill_name: string;
83
- relative_path: string;
84
- }) => Promise<{
85
- injection: {
86
- skill_name: string;
87
- resource_path: string;
88
- resource_mimetype: string;
89
- content: string;
90
- };
91
- }>;
92
- loadSkill: (skillNames: string[]) => Promise<{
93
- loaded: import("./types").Skill[];
94
- notFound: string[];
95
- }>;
96
- }>;
49
+ findSkills: ReturnType<typeof createSkillFinder>;
50
+ recommendSkills: ReturnType<typeof createSkillRecommender>;
51
+ readResource: ReturnType<typeof createSkillResourceReader>;
52
+ loadSkill: ReturnType<typeof createSkillLoader>;
53
+ skillTool: ReturnType<typeof createSkillTool>;
54
+ };
55
+ export declare const createApi: (config: PluginConfig) => Promise<SkillsApi>;
@@ -10,7 +10,6 @@ export declare function parseSlashCommand(text: string, slashCommandName: string
10
10
  export declare function findSkillBySelector(registry: SkillRegistry, selector: string): Skill | null;
11
11
  export declare function renderSlashSkillPrompt(args: {
12
12
  invocationName: string;
13
- renderedSkill: string;
14
13
  skill: Skill;
15
14
  userPrompt: string;
16
15
  }): string;
@@ -19,7 +18,6 @@ export declare function rewriteRecommendSlashCommandText(text: string): Promise<
19
18
  export declare function rewriteSlashCommandText(args: {
20
19
  text: string;
21
20
  registry: SkillRegistry;
22
- renderSkill: (skill: Skill) => string;
23
21
  slashCommandName: string;
24
22
  enableSkillAliases: boolean;
25
23
  }): Promise<string | null>;
package/dist/index.js CHANGED
@@ -17839,6 +17839,18 @@ function createResourceCandidates(type, relativePath) {
17839
17839
  function normalizeSkillIdentifier(identifier) {
17840
17840
  return identifier.trim().toLowerCase().replace(/[/-]/g, "_");
17841
17841
  }
17842
+ function resolveFileWithinSkillRoot(skill, relativePath) {
17843
+ const normalizedPath = normalizeSkillResourcePath(relativePath);
17844
+ const absolutePath = path.resolve(skill.fullPath, normalizedPath);
17845
+ const relativeFromSkillRoot = path.relative(skill.fullPath, absolutePath);
17846
+ if (relativeFromSkillRoot.startsWith("..") || path.isAbsolute(relativeFromSkillRoot) || !doesPathExist(absolutePath)) {
17847
+ return null;
17848
+ }
17849
+ return {
17850
+ absolutePath,
17851
+ mimeType: detectMimeType(absolutePath)
17852
+ };
17853
+ }
17842
17854
  function createSkillResourceResolver(provider) {
17843
17855
  return async (args) => {
17844
17856
  const skillIdentifier = normalizeSkillIdentifier(args.skill_name);
@@ -17849,18 +17861,19 @@ function createSkillResourceResolver(provider) {
17849
17861
  const resourceMap = getSkillResources(skill);
17850
17862
  const candidatePaths = createResourceCandidates(args.type, args.relative_path);
17851
17863
  const resourceEntry = candidatePaths.map((candidatePath) => resourceMap.get(candidatePath)).find((candidate) => candidate !== undefined);
17852
- if (!resourceEntry) {
17864
+ const resolvedResource = resourceEntry ?? candidatePaths.map((candidatePath) => resolveFileWithinSkillRoot(skill, candidatePath)).find((candidate) => candidate !== null);
17865
+ if (!resolvedResource) {
17853
17866
  throw new Error(`Resource not found: Skill "${args.skill_name}" does not have a resource at path "${args.relative_path}"`);
17854
17867
  }
17855
17868
  try {
17856
- const content = await readSkillFile(resourceEntry.absolutePath);
17869
+ const content = await readSkillFile(resolvedResource.absolutePath);
17857
17870
  return {
17858
- absolute_path: resourceEntry.absolutePath,
17871
+ absolute_path: resolvedResource.absolutePath,
17859
17872
  content,
17860
- mimeType: resourceEntry.mimeType
17873
+ mimeType: resolvedResource.mimeType
17861
17874
  };
17862
17875
  } catch (error45) {
17863
- throw new Error(`Failed to read resource at ${resourceEntry.absolutePath}: ${error45 instanceof Error ? error45.message : String(error45)}`);
17876
+ throw new Error(`Failed to read resource at ${resolvedResource.absolutePath}: ${error45 instanceof Error ? error45.message : String(error45)}`);
17864
17877
  }
17865
17878
  };
17866
17879
  }
@@ -17886,7 +17899,130 @@ function createSkillResourceReader(provider) {
17886
17899
  };
17887
17900
  }
17888
17901
 
17902
+ // src/lib/SkillLinks.ts
17903
+ import path2 from "path";
17904
+ var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
17905
+ function normalizeLinkedSkillPath(target) {
17906
+ const trimmedTarget = target.trim();
17907
+ if (trimmedTarget.length === 0 || trimmedTarget.startsWith("#") || trimmedTarget.startsWith("/") || SCHEME_PATTERN.test(trimmedTarget)) {
17908
+ return null;
17909
+ }
17910
+ const [pathWithoutFragment] = trimmedTarget.split("#", 1);
17911
+ const [pathWithoutQuery] = pathWithoutFragment.split("?", 1);
17912
+ const normalizedPath = path2.posix.normalize(pathWithoutQuery.replace(/\\/g, "/")).replace(/^\.\//, "");
17913
+ if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
17914
+ return null;
17915
+ }
17916
+ return normalizedPath;
17917
+ }
17918
+ function extractSkillLinks(content) {
17919
+ const links = new Map;
17920
+ for (const match of content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
17921
+ const label = match[1]?.trim();
17922
+ const originalPath = match[2]?.trim();
17923
+ if (!label || !originalPath) {
17924
+ continue;
17925
+ }
17926
+ const resourcePath = normalizeLinkedSkillPath(originalPath);
17927
+ if (!resourcePath) {
17928
+ continue;
17929
+ }
17930
+ links.set(`${label}:${resourcePath}`, {
17931
+ label,
17932
+ originalPath,
17933
+ resourcePath
17934
+ });
17935
+ }
17936
+ for (const match of content.matchAll(/(^|[^a-zA-Z0-9])@([a-zA-Z0-9_.\-/]+(?:\/[a-zA-Z0-9_.\-/]+)*)/g)) {
17937
+ const originalPath = match[2]?.trim();
17938
+ if (!originalPath) {
17939
+ continue;
17940
+ }
17941
+ const resourcePath = normalizeLinkedSkillPath(originalPath);
17942
+ if (!resourcePath) {
17943
+ continue;
17944
+ }
17945
+ links.set(`@:${resourcePath}`, {
17946
+ label: `@${resourcePath}`,
17947
+ originalPath: `@${originalPath}`,
17948
+ resourcePath
17949
+ });
17950
+ }
17951
+ return Array.from(links.values());
17952
+ }
17953
+
17954
+ // src/lib/formatLoadedSkill.ts
17955
+ function formatLoadedSkill(args) {
17956
+ const linkedResources = extractSkillLinks(args.skill.content);
17957
+ const output = [
17958
+ `## Skill: ${args.skill.name}`,
17959
+ "",
17960
+ `**Skill identifier**: ${args.skill.toolName}`,
17961
+ `**Base directory**: ${args.skill.fullPath}`
17962
+ ];
17963
+ if (args.invocationName) {
17964
+ output.push(`**Invocation**: /${args.invocationName}`);
17965
+ }
17966
+ if (args.userMessage?.trim()) {
17967
+ output.push(`**User request**: ${args.userMessage.trim()}`);
17968
+ }
17969
+ output.push("", "Relative file references in this skill resolve from the skill root directory.");
17970
+ output.push("Use skill_resource with the exact root-relative path when you need a linked file.");
17971
+ if (linkedResources.length > 0) {
17972
+ output.push("", "### Linked files", "");
17973
+ for (const link of linkedResources) {
17974
+ output.push(`- ${link.originalPath} -> ${link.resourcePath}`);
17975
+ }
17976
+ }
17977
+ output.push("", args.skill.content);
17978
+ return output.join(`
17979
+ `);
17980
+ }
17981
+
17982
+ // src/tools/Skill.ts
17983
+ function normalizeSkillSelector(selector) {
17984
+ return selector.trim().toLowerCase().replace(/[/-]/g, "_");
17985
+ }
17986
+ function findSkill(registry2, selector) {
17987
+ const directMatch = registry2.controller.get(selector);
17988
+ if (directMatch) {
17989
+ return directMatch;
17990
+ }
17991
+ const normalizedSelector = normalizeSkillSelector(selector);
17992
+ for (const skill of registry2.controller.skills) {
17993
+ if (normalizeSkillSelector(skill.name) === normalizedSelector || normalizeSkillSelector(skill.toolName) === normalizedSelector) {
17994
+ return skill;
17995
+ }
17996
+ }
17997
+ return null;
17998
+ }
17999
+ function createSkillTool(registry2) {
18000
+ return tool({
18001
+ 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.",
18002
+ args: {
18003
+ name: tool.schema.string().describe("The skill name. Use without the leading slash, for example bmad-help."),
18004
+ user_message: tool.schema.string().optional().describe("Optional user request or arguments to apply with the skill.")
18005
+ },
18006
+ async execute(args) {
18007
+ await registry2.controller.ready.whenReady();
18008
+ const skill = findSkill(registry2, args.name.replace(/^\//, ""));
18009
+ if (!skill) {
18010
+ const available = registry2.controller.skills.map((entry) => entry.name).join(", ");
18011
+ throw new Error(`Skill "${args.name}" not found. Available: ${available || "none"}`);
18012
+ }
18013
+ return formatLoadedSkill({
18014
+ skill,
18015
+ invocationName: args.name.replace(/^\//, ""),
18016
+ userMessage: args.user_message
18017
+ });
18018
+ }
18019
+ });
18020
+ }
18021
+
17889
18022
  // src/tools/SkillUser.ts
18023
+ function normalizeSkillSelector2(selector) {
18024
+ return selector.trim().toLowerCase().replace(/[/-]/g, "_");
18025
+ }
17890
18026
  function createSkillLoader(provider) {
17891
18027
  const registry2 = provider.controller;
17892
18028
  async function loadSkills(skillNames) {
@@ -17894,7 +18030,8 @@ function createSkillLoader(provider) {
17894
18030
  const loaded = [];
17895
18031
  const notFound = [];
17896
18032
  for (const name of skillNames) {
17897
- const skill = registry2.get(name);
18033
+ const normalizedName = normalizeSkillSelector2(name);
18034
+ const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector2(candidate.name) === normalizedName || normalizeSkillSelector2(candidate.toolName) === normalizedName);
17898
18035
  if (skill) {
17899
18036
  loaded.push(skill);
17900
18037
  } else {
@@ -17920,7 +18057,8 @@ var createApi = async (config2) => {
17920
18057
  findSkills: createSkillFinder(registry2),
17921
18058
  recommendSkills: createSkillRecommender(registry2),
17922
18059
  readResource: createSkillResourceReader(registry2),
17923
- loadSkill: createSkillLoader(registry2)
18060
+ loadSkill: createSkillLoader(registry2),
18061
+ skillTool: createSkillTool(registry2)
17924
18062
  };
17925
18063
  };
17926
18064
 
@@ -18386,10 +18524,10 @@ async function loadConfig({
18386
18524
  var defaultConfigDir = resolve(process3.cwd(), "config");
18387
18525
  var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
18388
18526
  function getProjectRoot(filePath, options2 = {}) {
18389
- let path2 = process2.cwd();
18390
- while (path2.includes("storage"))
18391
- path2 = resolve2(path2, "..");
18392
- const finalPath = resolve2(path2, filePath || "");
18527
+ let path3 = process2.cwd();
18528
+ while (path3.includes("storage"))
18529
+ path3 = resolve2(path3, "..");
18530
+ const finalPath = resolve2(path3, filePath || "");
18393
18531
  if (options2?.relative)
18394
18532
  return relative3(process2.cwd(), finalPath);
18395
18533
  return finalPath;
@@ -19836,10 +19974,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
19836
19974
  return config3;
19837
19975
  const envPrefix = name.toUpperCase().replace(/-/g, "_");
19838
19976
  const result = { ...config3 };
19839
- function processObject(obj, path2 = []) {
19977
+ function processObject(obj, path3 = []) {
19840
19978
  const result2 = { ...obj };
19841
19979
  for (const [key, value] of Object.entries(obj)) {
19842
- const envPath = [...path2, key];
19980
+ const envPath = [...path3, key];
19843
19981
  const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
19844
19982
  const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
19845
19983
  const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
@@ -20021,10 +20159,10 @@ async function loadConfig3({
20021
20159
  var defaultConfigDir2 = resolve3(process6.cwd(), "config");
20022
20160
  var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
20023
20161
  function getProjectRoot2(filePath, options2 = {}) {
20024
- let path2 = process7.cwd();
20025
- while (path2.includes("storage"))
20026
- path2 = resolve4(path2, "..");
20027
- const finalPath = resolve4(path2, filePath || "");
20162
+ let path3 = process7.cwd();
20163
+ while (path3.includes("storage"))
20164
+ path3 = resolve4(path3, "..");
20165
+ const finalPath = resolve4(path3, filePath || "");
20028
20166
  if (options2?.relative)
20029
20167
  return relative2(process7.cwd(), finalPath);
20030
20168
  return finalPath;
@@ -21461,10 +21599,10 @@ class EnvVarError extends BunfigError {
21461
21599
 
21462
21600
  class FileSystemError extends BunfigError {
21463
21601
  code = "FILE_SYSTEM_ERROR";
21464
- constructor(operation, path2, cause) {
21465
- super(`File system ${operation} failed for "${path2}": ${cause.message}`, {
21602
+ constructor(operation, path3, cause) {
21603
+ super(`File system ${operation} failed for "${path3}": ${cause.message}`, {
21466
21604
  operation,
21467
- path: path2,
21605
+ path: path3,
21468
21606
  originalError: cause.name,
21469
21607
  originalMessage: cause.message
21470
21608
  });
@@ -21537,8 +21675,8 @@ var ErrorFactory = {
21537
21675
  envVar(envKey, envValue, expectedType, configName) {
21538
21676
  return new EnvVarError(envKey, envValue, expectedType, configName);
21539
21677
  },
21540
- fileSystem(operation, path2, cause) {
21541
- return new FileSystemError(operation, path2, cause);
21678
+ fileSystem(operation, path3, cause) {
21679
+ return new FileSystemError(operation, path3, cause);
21542
21680
  },
21543
21681
  typeGeneration(configDir, outputPath, cause) {
21544
21682
  return new TypeGenerationError(configDir, outputPath, cause);
@@ -21674,9 +21812,9 @@ class EnvProcessor {
21674
21812
  }
21675
21813
  return key.replace(/([A-Z])/g, "_$1").toUpperCase();
21676
21814
  }
21677
- processObject(obj, path2, envPrefix, options2) {
21815
+ processObject(obj, path3, envPrefix, options2) {
21678
21816
  for (const [key, value] of Object.entries(obj)) {
21679
- const envPath = [...path2, key];
21817
+ const envPath = [...path3, key];
21680
21818
  const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
21681
21819
  const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
21682
21820
  const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
@@ -21761,9 +21899,9 @@ class EnvProcessor {
21761
21899
  return this.formatAsText(envVars, configName);
21762
21900
  }
21763
21901
  }
21764
- extractEnvVarInfo(obj, path2, prefix, envVars) {
21902
+ extractEnvVarInfo(obj, path3, prefix, envVars) {
21765
21903
  for (const [key, value] of Object.entries(obj)) {
21766
- const envPath = [...path2, key];
21904
+ const envPath = [...path3, key];
21767
21905
  const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
21768
21906
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
21769
21907
  this.extractEnvVarInfo(value, envPath, prefix, envVars);
@@ -22178,15 +22316,15 @@ class ConfigFileLoader {
22178
22316
  }
22179
22317
  async preloadConfigurations(configPaths, options2 = {}) {
22180
22318
  const preloaded = new Map;
22181
- await Promise.allSettled(configPaths.map(async (path2) => {
22319
+ await Promise.allSettled(configPaths.map(async (path3) => {
22182
22320
  try {
22183
- const result = await this.loadFromPath(path2, {}, options2);
22321
+ const result = await this.loadFromPath(path3, {}, options2);
22184
22322
  if (result) {
22185
- preloaded.set(path2, result.config);
22323
+ preloaded.set(path3, result.config);
22186
22324
  }
22187
22325
  } catch (error45) {
22188
22326
  if (options2.verbose) {
22189
- console.warn(`Failed to preload ${path2}:`, error45);
22327
+ console.warn(`Failed to preload ${path3}:`, error45);
22190
22328
  }
22191
22329
  }
22192
22330
  }));
@@ -22265,13 +22403,13 @@ class ConfigValidator {
22265
22403
  warnings
22266
22404
  };
22267
22405
  }
22268
- validateObjectAgainstSchema(value, schema, path2, errors3, warnings, options2) {
22406
+ validateObjectAgainstSchema(value, schema, path3, errors3, warnings, options2) {
22269
22407
  if (options2.validateTypes && schema.type) {
22270
22408
  const actualType = Array.isArray(value) ? "array" : typeof value;
22271
22409
  const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
22272
22410
  if (!expectedTypes.includes(actualType)) {
22273
22411
  errors3.push({
22274
- path: path2,
22412
+ path: path3,
22275
22413
  message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
22276
22414
  expected: expectedTypes.join(" or "),
22277
22415
  actual: actualType,
@@ -22283,7 +22421,7 @@ class ConfigValidator {
22283
22421
  }
22284
22422
  if (schema.enum && !schema.enum.includes(value)) {
22285
22423
  errors3.push({
22286
- path: path2,
22424
+ path: path3,
22287
22425
  message: `Value must be one of: ${schema.enum.join(", ")}`,
22288
22426
  expected: schema.enum.join(", "),
22289
22427
  actual: value,
@@ -22295,7 +22433,7 @@ class ConfigValidator {
22295
22433
  if (typeof value === "string") {
22296
22434
  if (schema.minLength !== undefined && value.length < schema.minLength) {
22297
22435
  errors3.push({
22298
- path: path2,
22436
+ path: path3,
22299
22437
  message: `String length must be at least ${schema.minLength}`,
22300
22438
  expected: `>= ${schema.minLength}`,
22301
22439
  actual: value.length,
@@ -22304,7 +22442,7 @@ class ConfigValidator {
22304
22442
  }
22305
22443
  if (schema.maxLength !== undefined && value.length > schema.maxLength) {
22306
22444
  errors3.push({
22307
- path: path2,
22445
+ path: path3,
22308
22446
  message: `String length must not exceed ${schema.maxLength}`,
22309
22447
  expected: `<= ${schema.maxLength}`,
22310
22448
  actual: value.length,
@@ -22315,7 +22453,7 @@ class ConfigValidator {
22315
22453
  const regex = new RegExp(schema.pattern);
22316
22454
  if (!regex.test(value)) {
22317
22455
  errors3.push({
22318
- path: path2,
22456
+ path: path3,
22319
22457
  message: `String does not match pattern ${schema.pattern}`,
22320
22458
  expected: schema.pattern,
22321
22459
  actual: value,
@@ -22327,7 +22465,7 @@ class ConfigValidator {
22327
22465
  if (typeof value === "number") {
22328
22466
  if (schema.minimum !== undefined && value < schema.minimum) {
22329
22467
  errors3.push({
22330
- path: path2,
22468
+ path: path3,
22331
22469
  message: `Value must be at least ${schema.minimum}`,
22332
22470
  expected: `>= ${schema.minimum}`,
22333
22471
  actual: value,
@@ -22336,7 +22474,7 @@ class ConfigValidator {
22336
22474
  }
22337
22475
  if (schema.maximum !== undefined && value > schema.maximum) {
22338
22476
  errors3.push({
22339
- path: path2,
22477
+ path: path3,
22340
22478
  message: `Value must not exceed ${schema.maximum}`,
22341
22479
  expected: `<= ${schema.maximum}`,
22342
22480
  actual: value,
@@ -22346,7 +22484,7 @@ class ConfigValidator {
22346
22484
  }
22347
22485
  if (Array.isArray(value) && schema.items) {
22348
22486
  for (let i = 0;i < value.length; i++) {
22349
- const itemPath = path2 ? `${path2}[${i}]` : `[${i}]`;
22487
+ const itemPath = path3 ? `${path3}[${i}]` : `[${i}]`;
22350
22488
  this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
22351
22489
  if (options2.stopOnFirstError && errors3.length > 0)
22352
22490
  return;
@@ -22358,7 +22496,7 @@ class ConfigValidator {
22358
22496
  for (const requiredProp of schema.required) {
22359
22497
  if (!(requiredProp in obj)) {
22360
22498
  errors3.push({
22361
- path: path2 ? `${path2}.${requiredProp}` : requiredProp,
22499
+ path: path3 ? `${path3}.${requiredProp}` : requiredProp,
22362
22500
  message: `Missing required property '${requiredProp}'`,
22363
22501
  expected: "required",
22364
22502
  rule: "required"
@@ -22371,7 +22509,7 @@ class ConfigValidator {
22371
22509
  if (schema.properties) {
22372
22510
  for (const [propName, propSchema] of Object.entries(schema.properties)) {
22373
22511
  if (propName in obj) {
22374
- const propPath = path2 ? `${path2}.${propName}` : propName;
22512
+ const propPath = path3 ? `${path3}.${propName}` : propName;
22375
22513
  this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
22376
22514
  if (options2.stopOnFirstError && errors3.length > 0)
22377
22515
  return;
@@ -22383,7 +22521,7 @@ class ConfigValidator {
22383
22521
  for (const propName of Object.keys(obj)) {
22384
22522
  if (!allowedProps.has(propName)) {
22385
22523
  warnings.push({
22386
- path: path2 ? `${path2}.${propName}` : propName,
22524
+ path: path3 ? `${path3}.${propName}` : propName,
22387
22525
  message: `Additional property '${propName}' is not allowed`,
22388
22526
  rule: "additionalProperties"
22389
22527
  });
@@ -22417,12 +22555,12 @@ class ConfigValidator {
22417
22555
  warnings
22418
22556
  };
22419
22557
  }
22420
- validateWithRule(value, rule, path2) {
22558
+ validateWithRule(value, rule, path3) {
22421
22559
  const errors3 = [];
22422
22560
  if (rule.required && (value === undefined || value === null)) {
22423
22561
  errors3.push({
22424
- path: path2,
22425
- message: rule.message || `Property '${path2}' is required`,
22562
+ path: path3,
22563
+ message: rule.message || `Property '${path3}' is required`,
22426
22564
  expected: "required",
22427
22565
  rule: "required"
22428
22566
  });
@@ -22435,7 +22573,7 @@ class ConfigValidator {
22435
22573
  const actualType = Array.isArray(value) ? "array" : typeof value;
22436
22574
  if (actualType !== rule.type) {
22437
22575
  errors3.push({
22438
- path: path2,
22576
+ path: path3,
22439
22577
  message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
22440
22578
  expected: rule.type,
22441
22579
  actual: actualType,
@@ -22447,7 +22585,7 @@ class ConfigValidator {
22447
22585
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22448
22586
  if (length < rule.min) {
22449
22587
  errors3.push({
22450
- path: path2,
22588
+ path: path3,
22451
22589
  message: rule.message || `Value must be at least ${rule.min}`,
22452
22590
  expected: `>= ${rule.min}`,
22453
22591
  actual: length,
@@ -22459,7 +22597,7 @@ class ConfigValidator {
22459
22597
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22460
22598
  if (length > rule.max) {
22461
22599
  errors3.push({
22462
- path: path2,
22600
+ path: path3,
22463
22601
  message: rule.message || `Value must not exceed ${rule.max}`,
22464
22602
  expected: `<= ${rule.max}`,
22465
22603
  actual: length,
@@ -22470,7 +22608,7 @@ class ConfigValidator {
22470
22608
  if (rule.pattern && typeof value === "string") {
22471
22609
  if (!rule.pattern.test(value)) {
22472
22610
  errors3.push({
22473
- path: path2,
22611
+ path: path3,
22474
22612
  message: rule.message || `Value does not match pattern ${rule.pattern}`,
22475
22613
  expected: rule.pattern.toString(),
22476
22614
  actual: value,
@@ -22480,7 +22618,7 @@ class ConfigValidator {
22480
22618
  }
22481
22619
  if (rule.enum && !rule.enum.includes(value)) {
22482
22620
  errors3.push({
22483
- path: path2,
22621
+ path: path3,
22484
22622
  message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
22485
22623
  expected: rule.enum.join(", "),
22486
22624
  actual: value,
@@ -22491,7 +22629,7 @@ class ConfigValidator {
22491
22629
  const customError = rule.validator(value);
22492
22630
  if (customError) {
22493
22631
  errors3.push({
22494
- path: path2,
22632
+ path: path3,
22495
22633
  message: rule.message || customError,
22496
22634
  rule: "custom"
22497
22635
  });
@@ -22499,10 +22637,10 @@ class ConfigValidator {
22499
22637
  }
22500
22638
  return errors3;
22501
22639
  }
22502
- getValueByPath(obj, path2) {
22503
- if (!path2)
22640
+ getValueByPath(obj, path3) {
22641
+ if (!path3)
22504
22642
  return obj;
22505
- const keys = path2.split(".");
22643
+ const keys = path3.split(".");
22506
22644
  let current = obj;
22507
22645
  for (const key of keys) {
22508
22646
  if (current && typeof current === "object" && key in current) {
@@ -22928,10 +23066,10 @@ async function loadConfig5(options2) {
22928
23066
  function applyEnvVarsToConfig2(name, config4, verbose = false) {
22929
23067
  const _envProcessor = new EnvProcessor;
22930
23068
  const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
22931
- function processConfigLevel(obj, path2 = []) {
23069
+ function processConfigLevel(obj, path3 = []) {
22932
23070
  const result = { ...obj };
22933
23071
  for (const [key, value] of Object.entries(obj)) {
22934
- const currentPath = [...path2, key];
23072
+ const currentPath = [...path3, key];
22935
23073
  const envKeys = [
22936
23074
  `${envPrefix}_${currentPath.join("_").toUpperCase()}`,
22937
23075
  `${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
@@ -22997,14 +23135,14 @@ function getOpenCodeConfigPaths() {
22997
23135
  paths.push(join4(home, ".opencode"));
22998
23136
  return paths;
22999
23137
  }
23000
- function expandTildePath(path2) {
23001
- if (path2 === "~") {
23138
+ function expandTildePath(path3) {
23139
+ if (path3 === "~") {
23002
23140
  return homedir3();
23003
23141
  }
23004
- if (path2.startsWith("~/")) {
23005
- return join4(homedir3(), path2.slice(2));
23142
+ if (path3.startsWith("~/")) {
23143
+ return join4(homedir3(), path3.slice(2));
23006
23144
  }
23007
- return path2;
23145
+ return path3;
23008
23146
  }
23009
23147
  var createPathKey = (absolutePath) => {
23010
23148
  const normalizedPath = normalize(absolutePath);
@@ -23077,7 +23215,7 @@ var options2 = {
23077
23215
  promptRenderer: "xml",
23078
23216
  modelRenderers: {},
23079
23217
  slashCommandName: "skill",
23080
- enableSkillAliases: false
23218
+ enableSkillAliases: true
23081
23219
  }
23082
23220
  };
23083
23221
  async function getPluginConfig(ctx) {
@@ -23093,7 +23231,7 @@ async function getPluginConfig(ctx) {
23093
23231
  // src/commands/SlashCommand.ts
23094
23232
  var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
23095
23233
  var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
23096
- function normalizeSkillSelector(selector) {
23234
+ function normalizeSkillSelector3(selector) {
23097
23235
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
23098
23236
  }
23099
23237
  function parseSlashCommand(text, slashCommandName) {
@@ -23130,30 +23268,26 @@ function findSkillBySelector(registry2, selector) {
23130
23268
  if (directMatch) {
23131
23269
  return directMatch;
23132
23270
  }
23133
- const normalizedSelector = normalizeSkillSelector(selector);
23271
+ const normalizedSelector = normalizeSkillSelector3(selector);
23134
23272
  for (const skill of registry2.controller.skills) {
23135
- if (normalizeSkillSelector(skill.toolName) === normalizedSelector) {
23273
+ if (normalizeSkillSelector3(skill.toolName) === normalizedSelector) {
23136
23274
  return skill;
23137
23275
  }
23138
- if (normalizeSkillSelector(skill.name) === normalizedSelector) {
23276
+ if (normalizeSkillSelector3(skill.name) === normalizedSelector) {
23139
23277
  return skill;
23140
23278
  }
23141
23279
  }
23142
23280
  return null;
23143
23281
  }
23144
23282
  function renderSlashSkillPrompt(args) {
23145
- const userPrompt = args.userPrompt || "Apply this skill to the current request.";
23146
23283
  return [
23147
23284
  SLASH_COMMAND_SENTINEL,
23148
- `Slash command: /${args.invocationName}`,
23149
- `Load the following dynamic skill and use it for this request.`,
23150
- `Skill identifier: ${args.skill.toolName}`,
23151
- `Skill root directory: ${args.skill.fullPath}`,
23152
- `Resolve every file reference inside the skill relative to the skill root directory.`,
23153
- `If you need a supporting file, use skill_resource with the exact root-relative path.`,
23154
- `User request: ${userPrompt}`,
23155
- "",
23156
- args.renderedSkill
23285
+ formatLoadedSkill({
23286
+ invocationName: args.invocationName,
23287
+ skill: args.skill,
23288
+ userMessage: args.userPrompt || "Apply this skill to the current request."
23289
+ }),
23290
+ ""
23157
23291
  ].join(`
23158
23292
  `);
23159
23293
  }
@@ -23201,7 +23335,6 @@ async function rewriteSlashCommandText(args) {
23201
23335
  }
23202
23336
  return renderSlashSkillPrompt({
23203
23337
  invocationName: parsedCommand.invocationName,
23204
- renderedSkill: args.renderSkill(skill),
23205
23338
  skill,
23206
23339
  userPrompt: parsedCommand.userPrompt
23207
23340
  });
@@ -23212,6 +23345,15 @@ var createJsonPromptRenderer = () => {
23212
23345
  const renderer = {
23213
23346
  format: "json",
23214
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
+ }
23215
23357
  return JSON.stringify({ [args.type]: args.data }, null, 2);
23216
23358
  }
23217
23359
  };
@@ -23269,6 +23411,8 @@ var createXmlPromptRenderer = () => {
23269
23411
  const prepareSkill = (skill) => {
23270
23412
  return {
23271
23413
  ...skill,
23414
+ linkedResources: extractSkillLinks(skill.content),
23415
+ skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path.",
23272
23416
  references: resourceMapToArray(skill.references),
23273
23417
  scripts: resourceMapToArray(skill.scripts),
23274
23418
  assets: resourceMapToArray(skill.assets)
@@ -23490,6 +23634,7 @@ var createMdPromptRenderer = () => {
23490
23634
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
23491
23635
  };
23492
23636
  const renderSkill = (skill) => {
23637
+ const linkedResources = extractSkillLinks(skill.content);
23493
23638
  return dedent_default`
23494
23639
  # ${skill.name}
23495
23640
 
@@ -23497,6 +23642,14 @@ var createMdPromptRenderer = () => {
23497
23642
  > ${skill.fullPath}
23498
23643
 
23499
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
+ ` : ""}
23500
23653
 
23501
23654
  ${skill.content}
23502
23655
 
@@ -23645,7 +23798,7 @@ var SkillsPlugin = async (ctx) => {
23645
23798
  providerId: input.model?.providerID,
23646
23799
  config: config3
23647
23800
  });
23648
- const renderSkill = promptRenderer.getFormatter(format);
23801
+ promptRenderer.getFormatter(format);
23649
23802
  for (const part of output.parts) {
23650
23803
  if (part.type !== "text") {
23651
23804
  continue;
@@ -23658,7 +23811,6 @@ var SkillsPlugin = async (ctx) => {
23658
23811
  const rewrittenText = await rewriteSlashCommandText({
23659
23812
  text: part.text,
23660
23813
  registry: api2.registry,
23661
- renderSkill: (skill) => renderSkill({ data: skill, type: "Skill" }),
23662
23814
  slashCommandName: config3.slashCommandName,
23663
23815
  enableSkillAliases: config3.enableSkillAliases
23664
23816
  });
@@ -23680,6 +23832,7 @@ var SkillsPlugin = async (ctx) => {
23680
23832
  }
23681
23833
  },
23682
23834
  tool: {
23835
+ skill: api2.skillTool,
23683
23836
  skill_use: tool({
23684
23837
  description: "Load one or more skills into the chat. Provide an array of skill names to load them as user messages.",
23685
23838
  args: {
@@ -0,0 +1,7 @@
1
+ export type SkillLink = {
2
+ label: string;
3
+ originalPath: string;
4
+ resourcePath: string;
5
+ };
6
+ export declare function normalizeLinkedSkillPath(target: string): string | null;
7
+ export declare function extractSkillLinks(content: string): SkillLink[];
@@ -0,0 +1,6 @@
1
+ import type { Skill } from '../types';
2
+ export declare function formatLoadedSkill(args: {
3
+ skill: Skill;
4
+ invocationName?: string;
5
+ userMessage?: string;
6
+ }): string;
@@ -0,0 +1,3 @@
1
+ import { type ToolDefinition } from '@opencode-ai/plugin';
2
+ import type { SkillRegistry } from '../types';
3
+ export declare function createSkillTool(registry: SkillRegistry): ToolDefinition;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-dynamic-skills",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "OpenCode plugin for dynamic skill loading, slash skill commands, and skill-root-relative resources",
5
5
  "homepage": "https://github.com/Wu-H-Y/opencode-dynamic-skills#readme",
6
6
  "bugs": {