opencode-dynamic-skills 1.0.2 → 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
@@ -17899,7 +17899,130 @@ function createSkillResourceReader(provider) {
17899
17899
  };
17900
17900
  }
17901
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
+
17902
18022
  // src/tools/SkillUser.ts
18023
+ function normalizeSkillSelector2(selector) {
18024
+ return selector.trim().toLowerCase().replace(/[/-]/g, "_");
18025
+ }
17903
18026
  function createSkillLoader(provider) {
17904
18027
  const registry2 = provider.controller;
17905
18028
  async function loadSkills(skillNames) {
@@ -17907,7 +18030,8 @@ function createSkillLoader(provider) {
17907
18030
  const loaded = [];
17908
18031
  const notFound = [];
17909
18032
  for (const name of skillNames) {
17910
- 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);
17911
18035
  if (skill) {
17912
18036
  loaded.push(skill);
17913
18037
  } else {
@@ -17933,7 +18057,8 @@ var createApi = async (config2) => {
17933
18057
  findSkills: createSkillFinder(registry2),
17934
18058
  recommendSkills: createSkillRecommender(registry2),
17935
18059
  readResource: createSkillResourceReader(registry2),
17936
- loadSkill: createSkillLoader(registry2)
18060
+ loadSkill: createSkillLoader(registry2),
18061
+ skillTool: createSkillTool(registry2)
17937
18062
  };
17938
18063
  };
17939
18064
 
@@ -18399,10 +18524,10 @@ async function loadConfig({
18399
18524
  var defaultConfigDir = resolve(process3.cwd(), "config");
18400
18525
  var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
18401
18526
  function getProjectRoot(filePath, options2 = {}) {
18402
- let path2 = process2.cwd();
18403
- while (path2.includes("storage"))
18404
- path2 = resolve2(path2, "..");
18405
- 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 || "");
18406
18531
  if (options2?.relative)
18407
18532
  return relative3(process2.cwd(), finalPath);
18408
18533
  return finalPath;
@@ -19849,10 +19974,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
19849
19974
  return config3;
19850
19975
  const envPrefix = name.toUpperCase().replace(/-/g, "_");
19851
19976
  const result = { ...config3 };
19852
- function processObject(obj, path2 = []) {
19977
+ function processObject(obj, path3 = []) {
19853
19978
  const result2 = { ...obj };
19854
19979
  for (const [key, value] of Object.entries(obj)) {
19855
- const envPath = [...path2, key];
19980
+ const envPath = [...path3, key];
19856
19981
  const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
19857
19982
  const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
19858
19983
  const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
@@ -20034,10 +20159,10 @@ async function loadConfig3({
20034
20159
  var defaultConfigDir2 = resolve3(process6.cwd(), "config");
20035
20160
  var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
20036
20161
  function getProjectRoot2(filePath, options2 = {}) {
20037
- let path2 = process7.cwd();
20038
- while (path2.includes("storage"))
20039
- path2 = resolve4(path2, "..");
20040
- 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 || "");
20041
20166
  if (options2?.relative)
20042
20167
  return relative2(process7.cwd(), finalPath);
20043
20168
  return finalPath;
@@ -21474,10 +21599,10 @@ class EnvVarError extends BunfigError {
21474
21599
 
21475
21600
  class FileSystemError extends BunfigError {
21476
21601
  code = "FILE_SYSTEM_ERROR";
21477
- constructor(operation, path2, cause) {
21478
- super(`File system ${operation} failed for "${path2}": ${cause.message}`, {
21602
+ constructor(operation, path3, cause) {
21603
+ super(`File system ${operation} failed for "${path3}": ${cause.message}`, {
21479
21604
  operation,
21480
- path: path2,
21605
+ path: path3,
21481
21606
  originalError: cause.name,
21482
21607
  originalMessage: cause.message
21483
21608
  });
@@ -21550,8 +21675,8 @@ var ErrorFactory = {
21550
21675
  envVar(envKey, envValue, expectedType, configName) {
21551
21676
  return new EnvVarError(envKey, envValue, expectedType, configName);
21552
21677
  },
21553
- fileSystem(operation, path2, cause) {
21554
- return new FileSystemError(operation, path2, cause);
21678
+ fileSystem(operation, path3, cause) {
21679
+ return new FileSystemError(operation, path3, cause);
21555
21680
  },
21556
21681
  typeGeneration(configDir, outputPath, cause) {
21557
21682
  return new TypeGenerationError(configDir, outputPath, cause);
@@ -21687,9 +21812,9 @@ class EnvProcessor {
21687
21812
  }
21688
21813
  return key.replace(/([A-Z])/g, "_$1").toUpperCase();
21689
21814
  }
21690
- processObject(obj, path2, envPrefix, options2) {
21815
+ processObject(obj, path3, envPrefix, options2) {
21691
21816
  for (const [key, value] of Object.entries(obj)) {
21692
- const envPath = [...path2, key];
21817
+ const envPath = [...path3, key];
21693
21818
  const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
21694
21819
  const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
21695
21820
  const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
@@ -21774,9 +21899,9 @@ class EnvProcessor {
21774
21899
  return this.formatAsText(envVars, configName);
21775
21900
  }
21776
21901
  }
21777
- extractEnvVarInfo(obj, path2, prefix, envVars) {
21902
+ extractEnvVarInfo(obj, path3, prefix, envVars) {
21778
21903
  for (const [key, value] of Object.entries(obj)) {
21779
- const envPath = [...path2, key];
21904
+ const envPath = [...path3, key];
21780
21905
  const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
21781
21906
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
21782
21907
  this.extractEnvVarInfo(value, envPath, prefix, envVars);
@@ -22191,15 +22316,15 @@ class ConfigFileLoader {
22191
22316
  }
22192
22317
  async preloadConfigurations(configPaths, options2 = {}) {
22193
22318
  const preloaded = new Map;
22194
- await Promise.allSettled(configPaths.map(async (path2) => {
22319
+ await Promise.allSettled(configPaths.map(async (path3) => {
22195
22320
  try {
22196
- const result = await this.loadFromPath(path2, {}, options2);
22321
+ const result = await this.loadFromPath(path3, {}, options2);
22197
22322
  if (result) {
22198
- preloaded.set(path2, result.config);
22323
+ preloaded.set(path3, result.config);
22199
22324
  }
22200
22325
  } catch (error45) {
22201
22326
  if (options2.verbose) {
22202
- console.warn(`Failed to preload ${path2}:`, error45);
22327
+ console.warn(`Failed to preload ${path3}:`, error45);
22203
22328
  }
22204
22329
  }
22205
22330
  }));
@@ -22278,13 +22403,13 @@ class ConfigValidator {
22278
22403
  warnings
22279
22404
  };
22280
22405
  }
22281
- validateObjectAgainstSchema(value, schema, path2, errors3, warnings, options2) {
22406
+ validateObjectAgainstSchema(value, schema, path3, errors3, warnings, options2) {
22282
22407
  if (options2.validateTypes && schema.type) {
22283
22408
  const actualType = Array.isArray(value) ? "array" : typeof value;
22284
22409
  const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
22285
22410
  if (!expectedTypes.includes(actualType)) {
22286
22411
  errors3.push({
22287
- path: path2,
22412
+ path: path3,
22288
22413
  message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
22289
22414
  expected: expectedTypes.join(" or "),
22290
22415
  actual: actualType,
@@ -22296,7 +22421,7 @@ class ConfigValidator {
22296
22421
  }
22297
22422
  if (schema.enum && !schema.enum.includes(value)) {
22298
22423
  errors3.push({
22299
- path: path2,
22424
+ path: path3,
22300
22425
  message: `Value must be one of: ${schema.enum.join(", ")}`,
22301
22426
  expected: schema.enum.join(", "),
22302
22427
  actual: value,
@@ -22308,7 +22433,7 @@ class ConfigValidator {
22308
22433
  if (typeof value === "string") {
22309
22434
  if (schema.minLength !== undefined && value.length < schema.minLength) {
22310
22435
  errors3.push({
22311
- path: path2,
22436
+ path: path3,
22312
22437
  message: `String length must be at least ${schema.minLength}`,
22313
22438
  expected: `>= ${schema.minLength}`,
22314
22439
  actual: value.length,
@@ -22317,7 +22442,7 @@ class ConfigValidator {
22317
22442
  }
22318
22443
  if (schema.maxLength !== undefined && value.length > schema.maxLength) {
22319
22444
  errors3.push({
22320
- path: path2,
22445
+ path: path3,
22321
22446
  message: `String length must not exceed ${schema.maxLength}`,
22322
22447
  expected: `<= ${schema.maxLength}`,
22323
22448
  actual: value.length,
@@ -22328,7 +22453,7 @@ class ConfigValidator {
22328
22453
  const regex = new RegExp(schema.pattern);
22329
22454
  if (!regex.test(value)) {
22330
22455
  errors3.push({
22331
- path: path2,
22456
+ path: path3,
22332
22457
  message: `String does not match pattern ${schema.pattern}`,
22333
22458
  expected: schema.pattern,
22334
22459
  actual: value,
@@ -22340,7 +22465,7 @@ class ConfigValidator {
22340
22465
  if (typeof value === "number") {
22341
22466
  if (schema.minimum !== undefined && value < schema.minimum) {
22342
22467
  errors3.push({
22343
- path: path2,
22468
+ path: path3,
22344
22469
  message: `Value must be at least ${schema.minimum}`,
22345
22470
  expected: `>= ${schema.minimum}`,
22346
22471
  actual: value,
@@ -22349,7 +22474,7 @@ class ConfigValidator {
22349
22474
  }
22350
22475
  if (schema.maximum !== undefined && value > schema.maximum) {
22351
22476
  errors3.push({
22352
- path: path2,
22477
+ path: path3,
22353
22478
  message: `Value must not exceed ${schema.maximum}`,
22354
22479
  expected: `<= ${schema.maximum}`,
22355
22480
  actual: value,
@@ -22359,7 +22484,7 @@ class ConfigValidator {
22359
22484
  }
22360
22485
  if (Array.isArray(value) && schema.items) {
22361
22486
  for (let i = 0;i < value.length; i++) {
22362
- const itemPath = path2 ? `${path2}[${i}]` : `[${i}]`;
22487
+ const itemPath = path3 ? `${path3}[${i}]` : `[${i}]`;
22363
22488
  this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
22364
22489
  if (options2.stopOnFirstError && errors3.length > 0)
22365
22490
  return;
@@ -22371,7 +22496,7 @@ class ConfigValidator {
22371
22496
  for (const requiredProp of schema.required) {
22372
22497
  if (!(requiredProp in obj)) {
22373
22498
  errors3.push({
22374
- path: path2 ? `${path2}.${requiredProp}` : requiredProp,
22499
+ path: path3 ? `${path3}.${requiredProp}` : requiredProp,
22375
22500
  message: `Missing required property '${requiredProp}'`,
22376
22501
  expected: "required",
22377
22502
  rule: "required"
@@ -22384,7 +22509,7 @@ class ConfigValidator {
22384
22509
  if (schema.properties) {
22385
22510
  for (const [propName, propSchema] of Object.entries(schema.properties)) {
22386
22511
  if (propName in obj) {
22387
- const propPath = path2 ? `${path2}.${propName}` : propName;
22512
+ const propPath = path3 ? `${path3}.${propName}` : propName;
22388
22513
  this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
22389
22514
  if (options2.stopOnFirstError && errors3.length > 0)
22390
22515
  return;
@@ -22396,7 +22521,7 @@ class ConfigValidator {
22396
22521
  for (const propName of Object.keys(obj)) {
22397
22522
  if (!allowedProps.has(propName)) {
22398
22523
  warnings.push({
22399
- path: path2 ? `${path2}.${propName}` : propName,
22524
+ path: path3 ? `${path3}.${propName}` : propName,
22400
22525
  message: `Additional property '${propName}' is not allowed`,
22401
22526
  rule: "additionalProperties"
22402
22527
  });
@@ -22430,12 +22555,12 @@ class ConfigValidator {
22430
22555
  warnings
22431
22556
  };
22432
22557
  }
22433
- validateWithRule(value, rule, path2) {
22558
+ validateWithRule(value, rule, path3) {
22434
22559
  const errors3 = [];
22435
22560
  if (rule.required && (value === undefined || value === null)) {
22436
22561
  errors3.push({
22437
- path: path2,
22438
- message: rule.message || `Property '${path2}' is required`,
22562
+ path: path3,
22563
+ message: rule.message || `Property '${path3}' is required`,
22439
22564
  expected: "required",
22440
22565
  rule: "required"
22441
22566
  });
@@ -22448,7 +22573,7 @@ class ConfigValidator {
22448
22573
  const actualType = Array.isArray(value) ? "array" : typeof value;
22449
22574
  if (actualType !== rule.type) {
22450
22575
  errors3.push({
22451
- path: path2,
22576
+ path: path3,
22452
22577
  message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
22453
22578
  expected: rule.type,
22454
22579
  actual: actualType,
@@ -22460,7 +22585,7 @@ class ConfigValidator {
22460
22585
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22461
22586
  if (length < rule.min) {
22462
22587
  errors3.push({
22463
- path: path2,
22588
+ path: path3,
22464
22589
  message: rule.message || `Value must be at least ${rule.min}`,
22465
22590
  expected: `>= ${rule.min}`,
22466
22591
  actual: length,
@@ -22472,7 +22597,7 @@ class ConfigValidator {
22472
22597
  const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
22473
22598
  if (length > rule.max) {
22474
22599
  errors3.push({
22475
- path: path2,
22600
+ path: path3,
22476
22601
  message: rule.message || `Value must not exceed ${rule.max}`,
22477
22602
  expected: `<= ${rule.max}`,
22478
22603
  actual: length,
@@ -22483,7 +22608,7 @@ class ConfigValidator {
22483
22608
  if (rule.pattern && typeof value === "string") {
22484
22609
  if (!rule.pattern.test(value)) {
22485
22610
  errors3.push({
22486
- path: path2,
22611
+ path: path3,
22487
22612
  message: rule.message || `Value does not match pattern ${rule.pattern}`,
22488
22613
  expected: rule.pattern.toString(),
22489
22614
  actual: value,
@@ -22493,7 +22618,7 @@ class ConfigValidator {
22493
22618
  }
22494
22619
  if (rule.enum && !rule.enum.includes(value)) {
22495
22620
  errors3.push({
22496
- path: path2,
22621
+ path: path3,
22497
22622
  message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
22498
22623
  expected: rule.enum.join(", "),
22499
22624
  actual: value,
@@ -22504,7 +22629,7 @@ class ConfigValidator {
22504
22629
  const customError = rule.validator(value);
22505
22630
  if (customError) {
22506
22631
  errors3.push({
22507
- path: path2,
22632
+ path: path3,
22508
22633
  message: rule.message || customError,
22509
22634
  rule: "custom"
22510
22635
  });
@@ -22512,10 +22637,10 @@ class ConfigValidator {
22512
22637
  }
22513
22638
  return errors3;
22514
22639
  }
22515
- getValueByPath(obj, path2) {
22516
- if (!path2)
22640
+ getValueByPath(obj, path3) {
22641
+ if (!path3)
22517
22642
  return obj;
22518
- const keys = path2.split(".");
22643
+ const keys = path3.split(".");
22519
22644
  let current = obj;
22520
22645
  for (const key of keys) {
22521
22646
  if (current && typeof current === "object" && key in current) {
@@ -22941,10 +23066,10 @@ async function loadConfig5(options2) {
22941
23066
  function applyEnvVarsToConfig2(name, config4, verbose = false) {
22942
23067
  const _envProcessor = new EnvProcessor;
22943
23068
  const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
22944
- function processConfigLevel(obj, path2 = []) {
23069
+ function processConfigLevel(obj, path3 = []) {
22945
23070
  const result = { ...obj };
22946
23071
  for (const [key, value] of Object.entries(obj)) {
22947
- const currentPath = [...path2, key];
23072
+ const currentPath = [...path3, key];
22948
23073
  const envKeys = [
22949
23074
  `${envPrefix}_${currentPath.join("_").toUpperCase()}`,
22950
23075
  `${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
@@ -23010,14 +23135,14 @@ function getOpenCodeConfigPaths() {
23010
23135
  paths.push(join4(home, ".opencode"));
23011
23136
  return paths;
23012
23137
  }
23013
- function expandTildePath(path2) {
23014
- if (path2 === "~") {
23138
+ function expandTildePath(path3) {
23139
+ if (path3 === "~") {
23015
23140
  return homedir3();
23016
23141
  }
23017
- if (path2.startsWith("~/")) {
23018
- return join4(homedir3(), path2.slice(2));
23142
+ if (path3.startsWith("~/")) {
23143
+ return join4(homedir3(), path3.slice(2));
23019
23144
  }
23020
- return path2;
23145
+ return path3;
23021
23146
  }
23022
23147
  var createPathKey = (absolutePath) => {
23023
23148
  const normalizedPath = normalize(absolutePath);
@@ -23090,7 +23215,7 @@ var options2 = {
23090
23215
  promptRenderer: "xml",
23091
23216
  modelRenderers: {},
23092
23217
  slashCommandName: "skill",
23093
- enableSkillAliases: false
23218
+ enableSkillAliases: true
23094
23219
  }
23095
23220
  };
23096
23221
  async function getPluginConfig(ctx) {
@@ -23106,7 +23231,7 @@ async function getPluginConfig(ctx) {
23106
23231
  // src/commands/SlashCommand.ts
23107
23232
  var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
23108
23233
  var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
23109
- function normalizeSkillSelector(selector) {
23234
+ function normalizeSkillSelector3(selector) {
23110
23235
  return selector.trim().toLowerCase().replace(/[/-]/g, "_");
23111
23236
  }
23112
23237
  function parseSlashCommand(text, slashCommandName) {
@@ -23143,30 +23268,26 @@ function findSkillBySelector(registry2, selector) {
23143
23268
  if (directMatch) {
23144
23269
  return directMatch;
23145
23270
  }
23146
- const normalizedSelector = normalizeSkillSelector(selector);
23271
+ const normalizedSelector = normalizeSkillSelector3(selector);
23147
23272
  for (const skill of registry2.controller.skills) {
23148
- if (normalizeSkillSelector(skill.toolName) === normalizedSelector) {
23273
+ if (normalizeSkillSelector3(skill.toolName) === normalizedSelector) {
23149
23274
  return skill;
23150
23275
  }
23151
- if (normalizeSkillSelector(skill.name) === normalizedSelector) {
23276
+ if (normalizeSkillSelector3(skill.name) === normalizedSelector) {
23152
23277
  return skill;
23153
23278
  }
23154
23279
  }
23155
23280
  return null;
23156
23281
  }
23157
23282
  function renderSlashSkillPrompt(args) {
23158
- const userPrompt = args.userPrompt || "Apply this skill to the current request.";
23159
23283
  return [
23160
23284
  SLASH_COMMAND_SENTINEL,
23161
- `Slash command: /${args.invocationName}`,
23162
- `Load the following dynamic skill and use it for this request.`,
23163
- `Skill identifier: ${args.skill.toolName}`,
23164
- `Skill root directory: ${args.skill.fullPath}`,
23165
- `Resolve every file reference inside the skill relative to the skill root directory.`,
23166
- `If you need a supporting file, use skill_resource with the exact root-relative path.`,
23167
- `User request: ${userPrompt}`,
23168
- "",
23169
- args.renderedSkill
23285
+ formatLoadedSkill({
23286
+ invocationName: args.invocationName,
23287
+ skill: args.skill,
23288
+ userMessage: args.userPrompt || "Apply this skill to the current request."
23289
+ }),
23290
+ ""
23170
23291
  ].join(`
23171
23292
  `);
23172
23293
  }
@@ -23214,49 +23335,11 @@ async function rewriteSlashCommandText(args) {
23214
23335
  }
23215
23336
  return renderSlashSkillPrompt({
23216
23337
  invocationName: parsedCommand.invocationName,
23217
- renderedSkill: args.renderSkill(skill),
23218
23338
  skill,
23219
23339
  userPrompt: parsedCommand.userPrompt
23220
23340
  });
23221
23341
  }
23222
23342
 
23223
- // src/lib/SkillLinks.ts
23224
- import path2 from "path";
23225
- var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
23226
- function normalizeLinkedSkillPath(target) {
23227
- const trimmedTarget = target.trim();
23228
- if (trimmedTarget.length === 0 || trimmedTarget.startsWith("#") || trimmedTarget.startsWith("/") || SCHEME_PATTERN.test(trimmedTarget)) {
23229
- return null;
23230
- }
23231
- const [pathWithoutFragment] = trimmedTarget.split("#", 1);
23232
- const [pathWithoutQuery] = pathWithoutFragment.split("?", 1);
23233
- const normalizedPath = path2.posix.normalize(pathWithoutQuery.replace(/\\/g, "/")).replace(/^\.\//, "");
23234
- if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
23235
- return null;
23236
- }
23237
- return normalizedPath;
23238
- }
23239
- function extractSkillLinks(content) {
23240
- const links = new Map;
23241
- for (const match of content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
23242
- const label = match[1]?.trim();
23243
- const originalPath = match[2]?.trim();
23244
- if (!label || !originalPath) {
23245
- continue;
23246
- }
23247
- const resourcePath = normalizeLinkedSkillPath(originalPath);
23248
- if (!resourcePath) {
23249
- continue;
23250
- }
23251
- links.set(`${label}:${resourcePath}`, {
23252
- label,
23253
- originalPath,
23254
- resourcePath
23255
- });
23256
- }
23257
- return Array.from(links.values());
23258
- }
23259
-
23260
23343
  // src/lib/renderers/JsonPromptRenderer.ts
23261
23344
  var createJsonPromptRenderer = () => {
23262
23345
  const renderer = {
@@ -23715,7 +23798,7 @@ var SkillsPlugin = async (ctx) => {
23715
23798
  providerId: input.model?.providerID,
23716
23799
  config: config3
23717
23800
  });
23718
- const renderSkill = promptRenderer.getFormatter(format);
23801
+ promptRenderer.getFormatter(format);
23719
23802
  for (const part of output.parts) {
23720
23803
  if (part.type !== "text") {
23721
23804
  continue;
@@ -23728,7 +23811,6 @@ var SkillsPlugin = async (ctx) => {
23728
23811
  const rewrittenText = await rewriteSlashCommandText({
23729
23812
  text: part.text,
23730
23813
  registry: api2.registry,
23731
- renderSkill: (skill) => renderSkill({ data: skill, type: "Skill" }),
23732
23814
  slashCommandName: config3.slashCommandName,
23733
23815
  enableSkillAliases: config3.enableSkillAliases
23734
23816
  });
@@ -23750,6 +23832,7 @@ var SkillsPlugin = async (ctx) => {
23750
23832
  }
23751
23833
  },
23752
23834
  tool: {
23835
+ skill: api2.skillTool,
23753
23836
  skill_use: tool({
23754
23837
  description: "Load one or more skills into the chat. Provide an array of skill names to load them as user messages.",
23755
23838
  args: {
@@ -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.2",
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": {