opencode-dynamic-skills 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,29 +1,31 @@
1
1
  # OpenCode Dynamic Skills
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/opencode-dynamic-skills?color=cb3837&logo=npm)](https://www.npmjs.com/package/opencode-dynamic-skills)
4
+ [![license](https://img.shields.io/npm/l/opencode-dynamic-skills)](./LICENSE)
5
+ [![bun](https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun)](https://bun.sh/)
6
+ [![OpenCode](https://img.shields.io/badge/plugin-OpenCode-111827)](https://opencode.ai/)
7
+
3
8
  [中文文档](./README.zh-CN.md)
4
9
 
5
10
  OpenCode plugin focused on three things:
6
11
 
7
12
  1. Dynamic skill loading
8
13
  2. Slash-style skill usage
9
- 3. Skill-root-relative file references
10
-
11
- ## Status
12
-
13
- This project is a reboot of the archived `opencode-skillful` codebase.
14
-
15
- - Package: `opencode-dynamic-skills`
16
- - Version: `1.0.0`
17
- - License: MIT
14
+ 3. Skill-root-relative file references and file manifests
18
15
 
19
16
  ## Attribution
20
17
 
21
18
  This project is derived from and adapted from:
22
19
 
23
20
  - https://github.com/zenobi-us/opencode-skillful
21
+ - https://github.com/code-yeongyu/oh-my-openagent
22
+ - Anthropic Agent Skills specification and skills ecosystem: https://github.com/anthropics/skills
24
23
 
25
24
  The current codebase restructures and extends that work for a fresh restart.
26
25
 
26
+ This project also references the slash-command and skill-loading ideas explored by oh-my-openagent.
27
+ Thanks to the maintainers and contributors of these projects for the public implementation ideas and prior art.
28
+
27
29
  ## Goals
28
30
 
29
31
  - Keep skills **lazy-loaded**, instead of injecting all skills into every session
@@ -37,11 +39,16 @@ The current codebase restructures and extends that work for a fresh restart.
37
39
 
38
40
  The plugin provides:
39
41
 
42
+ - `skill`
40
43
  - `skill_find`
41
44
  - `skill_recommend`
42
45
  - `skill_use`
43
46
  - `skill_resource`
44
47
 
48
+ The default design keeps the initial tool context small.
49
+ It does **not** inject the full skill catalog or every skill description up front.
50
+ Discovery is deferred to runtime through `skill_find`, `skill_recommend`, direct slash interception, or explicit `skill(name="...")` calls.
51
+
45
52
  ### Slash skill command
46
53
 
47
54
  Default form:
@@ -68,10 +75,20 @@ Optional aliases are also supported:
68
75
  /git-release draft a release checklist
69
76
  ```
70
77
 
71
- Aliases are disabled by default to avoid collisions with native or official OpenCode commands.
78
+ Aliases are enabled by default in the pure oh-my-openagent-style flow, so `/<skill-name>` can be intercepted after submit.
79
+ This does not mean OpenCode will show dynamic picker suggestions for those aliases.
72
80
 
73
81
  ### Skill-root-relative resources
74
82
 
83
+ When a skill is loaded, the plugin injects:
84
+
85
+ - the `SKILL.md` body
86
+ - the skill root path
87
+ - a complete root-relative file manifest for the skill directory, excluding `SKILL.md`
88
+
89
+ It does **not** inject every file's content by default.
90
+ Actual file contents stay lazy and are read on demand through `skill_resource`.
91
+
75
92
  `skill_resource` now resolves paths from the skill root, for example:
76
93
 
77
94
  ```txt
@@ -101,18 +118,31 @@ Register the plugin in `opencode.json`:
101
118
 
102
119
  ## Configuration
103
120
 
121
+ On startup the plugin ensures a complete commented JSONC config exists at:
122
+
123
+ ```text
124
+ ~/.config/opencode/opencode-dynamic-skills.config.jsonc
125
+ ```
126
+
104
127
  Example:
105
128
 
106
- ```json
129
+ ```jsonc
107
130
  {
131
+ // Enable verbose plugin logs.
108
132
  "debug": false,
109
- "promptRenderer": "xml",
110
- "modelRenderers": {
111
- "claude-3-5-sonnet": "xml",
112
- "gpt-4": "json"
113
- },
114
133
  "slashCommandName": "skill",
115
- "enableSkillAliases": false
134
+ "enableSkillAliases": true,
135
+ "reservedSlashCommands": ["help", "model", "skills"],
136
+ "notifications": {
137
+ "enabled": false,
138
+ "success": true,
139
+ "errors": true,
140
+ },
141
+ "skillRecommend": {
142
+ "strategy": "heuristic",
143
+ "model": "openai/gpt-5.2",
144
+ "systemPrompt": "You are selecting the most relevant dynamic skills for a user request. Return strict JSON only with this shape: {\"recommendations\":[{\"name\":\"skill_tool_name\",\"reason\":\"why it matches\"}]}. Only recommend skills from the provided catalog. Prefer the smallest set of high-confidence matches.",
145
+ },
116
146
  }
117
147
  ```
118
148
 
@@ -120,10 +150,18 @@ Fields:
120
150
 
121
151
  - `debug`
122
152
  - `basePaths`
123
- - `promptRenderer`
124
- - `modelRenderers`
125
153
  - `slashCommandName`
126
154
  - `enableSkillAliases`
155
+ - `reservedSlashCommands`
156
+ - `notifications`
157
+ - `skillRecommend`
158
+
159
+ Injected skill content now always uses a single XML renderer. The previous custom renderer selection feature was removed.
160
+
161
+ `skillRecommend.strategy` supports:
162
+
163
+ - `heuristic` (default): use the built-in ranker
164
+ - `model`: call an internal OpenCode session with the configured `provider/model` string
127
165
 
128
166
  ## Skill discovery
129
167
 
@@ -161,6 +199,7 @@ Rules:
161
199
  - `name` must match the directory name
162
200
  - `name` must satisfy OpenCode naming rules
163
201
  - `description` is required
202
+ - non-`SKILL.md` files under the skill directory are indexed and exposed as an available file manifest
164
203
 
165
204
  ## Development
166
205
 
package/README.zh-CN.md CHANGED
@@ -6,24 +6,21 @@
6
6
 
7
7
  1. 动态加载 skill
8
8
  2. 用 slash 方式使用 skill
9
- 3. 让 skill 内文件引用统一基于 skill 根目录
10
-
11
- ## 当前状态
12
-
13
- 这是对已归档 `opencode-skillful` 仓库的重启版本。
14
-
15
- - 包名:`opencode-dynamic-skills`
16
- - 版本:`1.0.0`
17
- - 协议:MIT
9
+ 3. 让 skill 内文件引用与文件清单统一基于 skill 根目录
18
10
 
19
11
  ## 来源说明
20
12
 
21
13
  本项目来自并改造于:
22
14
 
23
15
  - https://github.com/zenobi-us/opencode-skillful
16
+ - https://github.com/code-yeongyu/oh-my-openagent
17
+ - Anthropic Agent Skills 规范与 skills 生态:https://github.com/anthropics/skills
24
18
 
25
19
  当前版本在此基础上做了重新组织和重启开发。
26
20
 
21
+ 其中 slash 风格 skill 调用与 skill 装载思路,明确参考了 oh-my-openagent 的公开实现。
22
+ 感谢这些项目的维护者与贡献者。
23
+
27
24
  ## 目标
28
25
 
29
26
  - 保持 **按需加载**,不把所有 skill 预注入到每个会话
@@ -37,11 +34,16 @@
37
34
 
38
35
  插件提供:
39
36
 
37
+ - `skill`
40
38
  - `skill_find`
41
39
  - `skill_recommend`
42
40
  - `skill_use`
43
41
  - `skill_resource`
44
42
 
43
+ 默认设计会尽量保持初始工具上下文很小。
44
+ 它**不会**在一开始把全部 skill 目录和每个 skill 的 description 都注入进去。
45
+ 真正的发现与装载会延后到运行时,通过 `skill_find`、`skill_recommend`、直接 slash 拦截或显式 `skill(name="...")` 调用完成。
46
+
45
47
  ### Slash 技能命令
46
48
 
47
49
  默认形式:
@@ -68,10 +70,20 @@
68
70
  /git-release 帮我起草 release checklist
69
71
  ```
70
72
 
71
- alias 默认关闭,避免与 OpenCode 内置命令或官方 commands 冲突。
73
+ 在纯 oh-my-openagent 风格下,alias 默认开启,因此提交 `/<skill-name>` 后插件会尝试接管。
74
+ 但这不代表 OpenCode 会在输入阶段为这些 alias 提供动态候选列表。
72
75
 
73
76
  ### Skill 根目录相对资源
74
77
 
78
+ 当一个 skill 被加载时,插件会注入:
79
+
80
+ - `SKILL.md` 正文
81
+ - skill 根目录路径
82
+ - 一份完整的 skill 根目录相对文件清单(不包含 `SKILL.md`)
83
+
84
+ 它**不会**默认把所有文件内容都注入进上下文。
85
+ 真正的文件内容仍然通过 `skill_resource` 按需读取。
86
+
75
87
  `skill_resource` 现在按 skill 根目录解析路径,例如:
76
88
 
77
89
  ```txt
@@ -101,18 +113,31 @@ templates/pr.md
101
113
 
102
114
  ## 配置
103
115
 
116
+ 插件启动时会确保在下面位置存在一份带英文注释的完整 JSONC 配置:
117
+
118
+ ```text
119
+ ~/.config/opencode/opencode-dynamic-skills.config.jsonc
120
+ ```
121
+
104
122
  示例:
105
123
 
106
- ```json
124
+ ```jsonc
107
125
  {
126
+ // Enable verbose plugin logs.
108
127
  "debug": false,
109
- "promptRenderer": "xml",
110
- "modelRenderers": {
111
- "claude-3-5-sonnet": "xml",
112
- "gpt-4": "json"
113
- },
114
128
  "slashCommandName": "skill",
115
- "enableSkillAliases": false
129
+ "enableSkillAliases": true,
130
+ "reservedSlashCommands": ["help", "model", "skills"],
131
+ "notifications": {
132
+ "enabled": false,
133
+ "success": true,
134
+ "errors": true,
135
+ },
136
+ "skillRecommend": {
137
+ "strategy": "heuristic",
138
+ "model": "openai/gpt-5.2",
139
+ "systemPrompt": "You are selecting the most relevant dynamic skills for a user request. Return strict JSON only with this shape: {\"recommendations\":[{\"name\":\"skill_tool_name\",\"reason\":\"why it matches\"}]}. Only recommend skills from the provided catalog. Prefer the smallest set of high-confidence matches.",
140
+ },
116
141
  }
117
142
  ```
118
143
 
@@ -120,10 +145,18 @@ templates/pr.md
120
145
 
121
146
  - `debug`
122
147
  - `basePaths`
123
- - `promptRenderer`
124
- - `modelRenderers`
125
148
  - `slashCommandName`
126
149
  - `enableSkillAliases`
150
+ - `reservedSlashCommands`
151
+ - `notifications`
152
+ - `skillRecommend`
153
+
154
+ 当前注入的 skill 内容已固定使用单一 XML 渲染器,之前可自定义渲染格式的功能已移除。
155
+
156
+ `skillRecommend.strategy` 支持:
157
+
158
+ - `heuristic`(默认):使用内置规则排序
159
+ - `model`:使用配置的 `provider/model` 字符串发起一次内部 OpenCode session 调用
127
160
 
128
161
  ## Skill 发现路径
129
162
 
@@ -161,6 +194,7 @@ compatibility: opencode
161
194
  - `name` 必须和目录名一致
162
195
  - `name` 必须符合 OpenCode 命名规则
163
196
  - `description` 必填
197
+ - skill 目录下除 `SKILL.md` 外的文件会被索引,并作为可用文件清单暴露给模型
164
198
 
165
199
  ## 开发
166
200
 
package/dist/api.d.ts CHANGED
@@ -9,12 +9,12 @@
9
9
  * - Logger (for debug output)
10
10
  * - SkillRegistry (for discovery and parsing)
11
11
  * - Tool creators (functions that create skill_find, skill_use, skill_resource)
12
- * - PromptRenderer (for format selection and rendering)
12
+ * - Prompt rendering helpers
13
13
  *
14
14
  * INITIALIZATION TIMING (CRITICAL):
15
15
  * - createLogger(): synchronous, immediate
16
16
  * - createSkillRegistry(): synchronous factory call (returns a SkillRegistry object)
17
- * - createPromptRenderer(): synchronous, immediate (format selection at runtime)
17
+ * - XML prompt rendering is fixed and selected by the caller
18
18
  * - registry.initialise(): NOT called here, caller must do this separately
19
19
  *
20
20
  * WHY NOT CALL initialise(): The caller (index.ts) needs to await initialise()
@@ -23,7 +23,7 @@
23
23
  * RETURN VALUE: Object with:
24
24
  * - registry: SkillRegistry instance (must call .initialise() before use)
25
25
  * - logger: PluginLogger for debug output
26
- * - config: PluginConfig (needed for model-aware format selection)
26
+ * - config: PluginConfig
27
27
  * - findSkills: Tool creator function for skill search
28
28
  * - readResource: Tool creator function for resource reading
29
29
  * - loadSkill: Tool creator function for skill loading
@@ -34,63 +34,23 @@
34
34
  * // Note: registry is created but NOT yet initialized
35
35
  * // Must be done by caller: await registry.initialise()
36
36
  */
37
+ import type { PluginInput } from '@opencode-ai/plugin';
38
+ import { createLogger } from './services/logger';
39
+ import { createSkillRegistry } from './services/SkillRegistry';
40
+ import { createSkillFinder } from './tools/SkillFinder';
41
+ import { createSkillRecommender } from './tools/SkillRecommender';
42
+ import { createSkillResourceReader } from './tools/SkillResourceReader';
43
+ import { createSkillTool } from './tools/Skill';
44
+ import { createSkillLoader } from './tools/SkillUser';
37
45
  import type { PluginConfig } from './types';
38
- export declare const createApi: (config: PluginConfig) => Promise<{
39
- registry: import("./types").SkillRegistry;
40
- logger: import("./types").PluginLogger;
46
+ export type SkillsApi = {
47
+ registry: Awaited<ReturnType<typeof createSkillRegistry>>;
48
+ logger: ReturnType<typeof createLogger>;
41
49
  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
- }>;
50
+ findSkills: ReturnType<typeof createSkillFinder>;
51
+ recommendSkills: ReturnType<typeof createSkillRecommender>;
52
+ readResource: ReturnType<typeof createSkillResourceReader>;
53
+ loadSkill: ReturnType<typeof createSkillLoader>;
54
+ skillTool: ReturnType<typeof createSkillTool>;
55
+ };
56
+ export declare const createApi: (config: PluginConfig, client?: PluginInput["client"]) => 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,8 +18,8 @@ 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;
23
+ reservedSlashCommands?: string[];
25
24
  }): Promise<string | null>;
26
25
  export {};
package/dist/config.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
- import type { PluginConfig } from './types';
2
+ import type { NotificationConfig, PluginConfig, SkillRecommendConfig } from './types';
3
+ export declare const DEFAULT_RESERVED_SLASH_COMMANDS: readonly ["agent", "agents", "compact", "connect", "details", "editor", "exit", "export", "fork", "help", "init", "mcp", "model", "models", "new", "open", "redo", "sessions", "share", "skills", "terminal", "themes", "thinking", "undo", "unshare"];
4
+ export declare const DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT: string;
5
+ export declare const MANAGED_PLUGIN_CONFIG_DIRECTORY: string;
6
+ export declare const MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
7
+ export declare const MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
8
+ type PartialPluginConfig = Partial<Omit<PluginConfig, 'skillRecommend' | 'notifications'>> & {
9
+ skillRecommend?: Partial<SkillRecommendConfig>;
10
+ notifications?: Partial<NotificationConfig>;
11
+ };
3
12
  /**
4
13
  * Gets OpenCode-compatible config paths for the current platform.
5
14
  *
@@ -44,4 +53,15 @@ export declare function resolveBasePath(basePath: string, projectDirectory: stri
44
53
  */
45
54
  export declare function normalizeBasePaths(basePaths: string[], projectDirectory: string): string[];
46
55
  export declare function getProjectSkillBasePaths(directory: string, worktree?: string): string[];
56
+ export declare function createDefaultSkillRecommendConfig(): SkillRecommendConfig;
57
+ export declare function createDefaultNotificationConfig(): NotificationConfig;
58
+ export declare function createDefaultPluginConfig(): PluginConfig;
59
+ export declare function stripJsonComments(input: string): string;
60
+ export declare function removeTrailingJsonCommas(input: string): string;
61
+ export declare function parseJsonc<T>(input: string): T;
62
+ export declare function getManagedPluginConfigPaths(configDirectory?: string): string[];
63
+ export declare function renderManagedPluginConfigJsonc(config?: PluginConfig): string;
64
+ export declare function ensureManagedPluginConfigFile(configDirectory?: string): Promise<string>;
65
+ export declare function loadManagedPluginConfig(configDirectory?: string): Promise<PartialPluginConfig>;
47
66
  export declare function getPluginConfig(ctx: PluginInput): Promise<PluginConfig>;
67
+ export {};
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * - find_skills(): Search for skills by free-text query
12
12
  * - Delivers skill content via silent message insertion (noReply pattern)
13
13
  * - Supports nested skills with proper naming
14
- * - Supports multiple prompt formats (XML, JSON, Markdown) with model-aware selection
14
+ * - Uses a single XML prompt format for injected skill content
15
15
  *
16
16
  * Design Decisions:
17
17
  * - Consolidates 50+ individual skill tools into 2 unified tools (cleaner namespace)
@@ -21,7 +21,7 @@
21
21
  * - Message insertion pattern ensures skill content persists (user messages not purged)
22
22
  * - Base directory context enables relative path resolution
23
23
  * - Skills require restart to reload (acceptable trade-off)
24
- * - Prompt format selection: model-aware via modelRenderers config, default XML
24
+ * - Prompt rendering is fixed to XML for simpler, stable behavior
25
25
  *
26
26
  * @see https://github.com/anthropics/skills
27
27
  */