opencode-plugin-preload-skills 1.0.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 ADDED
@@ -0,0 +1,188 @@
1
+ # opencode-plugin-preload-skills
2
+
3
+ > Automatically load skills into agent memory at session start
4
+
5
+ [![npm version](https://img.shields.io/npm/v/opencode-plugin-preload-skills.svg)](https://www.npmjs.com/package/opencode-plugin-preload-skills)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ A plugin for [OpenCode](https://opencode.ai) that preloads specified skills into the agent's context automatically when a session starts. Skills persist across context compaction, ensuring your agent always has access to the knowledge it needs.
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - **Automatic Loading** — Skills are injected on the first message of each session
15
+ - **Compaction Persistence** — Skills survive context compaction and remain available
16
+ - **Multiple Skill Sources** — Searches project and global skill directories
17
+ - **Debug Logging** — Optional verbose logging for troubleshooting
18
+ - **Zero Runtime Overhead** — Skills loaded once per session
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install opencode-plugin-preload-skills
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ **1. Add the plugin to your `opencode.json`:**
33
+
34
+ ```json
35
+ {
36
+ "$schema": "https://opencode.ai/config.json",
37
+ "plugin": ["opencode-plugin-preload-skills"],
38
+ "opencode-plugin-preload-skills": {
39
+ "skills": ["my-coding-standards", "project-architecture"]
40
+ }
41
+ }
42
+ ```
43
+
44
+ **2. Create a skill file:**
45
+
46
+ ```
47
+ .opencode/skills/my-coding-standards/SKILL.md
48
+ ```
49
+
50
+ ```markdown
51
+ ---
52
+ name: my-coding-standards
53
+ description: Coding standards and conventions for this project
54
+ ---
55
+
56
+ ## Code Style
57
+
58
+ - Use 2 spaces for indentation
59
+ - Prefer `const` over `let`
60
+ - Use TypeScript strict mode
61
+ ...
62
+ ```
63
+
64
+ **3. Start OpenCode** — your skills are automatically loaded!
65
+
66
+ ---
67
+
68
+ ## Configuration
69
+
70
+ | Option | Type | Default | Description |
71
+ |--------|------|---------|-------------|
72
+ | `skills` | `string[]` | `[]` | Skill names to auto-load |
73
+ | `persistAfterCompaction` | `boolean` | `true` | Re-inject skills after context compaction |
74
+ | `debug` | `boolean` | `false` | Enable debug logging |
75
+
76
+ ### Full Example
77
+
78
+ ```json
79
+ {
80
+ "$schema": "https://opencode.ai/config.json",
81
+ "plugin": ["opencode-plugin-preload-skills"],
82
+ "opencode-plugin-preload-skills": {
83
+ "skills": [
84
+ "coding-standards",
85
+ "api-patterns",
86
+ "testing-guide"
87
+ ],
88
+ "persistAfterCompaction": true,
89
+ "debug": false
90
+ }
91
+ }
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Skill Locations
97
+
98
+ The plugin searches for skills in the following locations (in order):
99
+
100
+ | Priority | Path | Scope |
101
+ |----------|------|-------|
102
+ | 1 | `.opencode/skills/<name>/SKILL.md` | Project |
103
+ | 2 | `.claude/skills/<name>/SKILL.md` | Project (Claude-compatible) |
104
+ | 3 | `~/.config/opencode/skills/<name>/SKILL.md` | Global |
105
+ | 4 | `~/.claude/skills/<name>/SKILL.md` | Global (Claude-compatible) |
106
+
107
+ The first matching skill file is used.
108
+
109
+ ---
110
+
111
+ ## Skill File Format
112
+
113
+ Skills use markdown with YAML frontmatter:
114
+
115
+ ```markdown
116
+ ---
117
+ name: skill-name
118
+ description: Brief description shown in logs
119
+ ---
120
+
121
+ # Skill Content
122
+
123
+ Your skill instructions here. This entire content
124
+ is injected into the agent's context.
125
+
126
+ ## Sections
127
+
128
+ Organize with headers, code blocks, lists, etc.
129
+ ```
130
+
131
+ ### Required Fields
132
+
133
+ - `name` — Must match the directory name (lowercase, hyphen-separated)
134
+ - `description` — Brief description for logging
135
+
136
+ ---
137
+
138
+ ## How It Works
139
+
140
+ ```
141
+ Session Start
142
+
143
+
144
+ ┌─────────────────────┐
145
+ │ Plugin loads │
146
+ │ configured skills │
147
+ │ from disk │
148
+ └─────────────────────┘
149
+
150
+
151
+ ┌─────────────────────┐
152
+ │ First message │──▶ Skills injected as synthetic content
153
+ │ in session │
154
+ └─────────────────────┘
155
+
156
+
157
+ ┌─────────────────────┐
158
+ │ Context │──▶ Skills added to compaction context
159
+ │ compaction │ (if persistAfterCompaction: true)
160
+ └─────────────────────┘
161
+
162
+
163
+ ┌─────────────────────┐
164
+ │ Session │──▶ Cleanup session tracking
165
+ │ deleted │
166
+ └─────────────────────┘
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Troubleshooting
172
+
173
+ ### Skills not loading?
174
+
175
+ 1. **Check the skill path** — Ensure `SKILL.md` exists in the correct directory
176
+ 2. **Verify frontmatter** — Both `name` and `description` are required
177
+ 3. **Enable debug mode** — Set `"debug": true` in config
178
+ 4. **Check logs** — Look for `preload-skills` service messages
179
+
180
+ ### Skills lost after compaction?
181
+
182
+ Ensure `persistAfterCompaction` is `true` (this is the default).
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var os = require('os');
8
+
9
+ // src/skill-loader.ts
10
+ var SKILL_FILENAME = "SKILL.md";
11
+ var SKILL_SEARCH_PATHS = [
12
+ (dir) => path.join(dir, ".opencode", "skills"),
13
+ (dir) => path.join(dir, ".claude", "skills"),
14
+ () => path.join(os.homedir(), ".config", "opencode", "skills"),
15
+ () => path.join(os.homedir(), ".claude", "skills")
16
+ ];
17
+ function findSkillFile(skillName, projectDir) {
18
+ for (const getPath of SKILL_SEARCH_PATHS) {
19
+ const skillDir = getPath(projectDir);
20
+ const skillPath = path.join(skillDir, skillName, SKILL_FILENAME);
21
+ if (fs.existsSync(skillPath)) {
22
+ return skillPath;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ function parseFrontmatter(content) {
28
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
29
+ if (!frontmatterMatch?.[1]) {
30
+ return {};
31
+ }
32
+ const frontmatter = frontmatterMatch[1];
33
+ const result = {};
34
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
35
+ if (nameMatch?.[1]) {
36
+ result.name = nameMatch[1].trim();
37
+ }
38
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
39
+ if (descMatch?.[1]) {
40
+ result.description = descMatch[1].trim();
41
+ }
42
+ return result;
43
+ }
44
+ function loadSkill(skillName, projectDir) {
45
+ const filePath = findSkillFile(skillName, projectDir);
46
+ if (!filePath) {
47
+ return null;
48
+ }
49
+ try {
50
+ const content = fs.readFileSync(filePath, "utf-8");
51
+ const { name, description } = parseFrontmatter(content);
52
+ return {
53
+ name: name ?? skillName,
54
+ description: description ?? "",
55
+ content,
56
+ filePath
57
+ };
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ function loadSkills(skillNames, projectDir) {
63
+ const skills = [];
64
+ for (const name of skillNames) {
65
+ const skill = loadSkill(name, projectDir);
66
+ if (skill) {
67
+ skills.push(skill);
68
+ }
69
+ }
70
+ return skills;
71
+ }
72
+ function formatSkillsForInjection(skills) {
73
+ if (skills.length === 0) {
74
+ return "";
75
+ }
76
+ const parts = skills.map(
77
+ (skill) => `<preloaded-skill name="${skill.name}">
78
+ ${skill.content}
79
+ </preloaded-skill>`
80
+ );
81
+ return `<preloaded-skills>
82
+ The following skills have been automatically loaded for this session:
83
+
84
+ ${parts.join("\n\n")}
85
+ </preloaded-skills>`;
86
+ }
87
+
88
+ // src/index.ts
89
+ var DEFAULT_CONFIG = {
90
+ skills: [],
91
+ persistAfterCompaction: true,
92
+ debug: false
93
+ };
94
+ var PreloadSkillsPlugin = async (ctx) => {
95
+ const injectedSessions = /* @__PURE__ */ new Set();
96
+ let loadedSkills = [];
97
+ let formattedContent = "";
98
+ let config = DEFAULT_CONFIG;
99
+ const log = (level, message, extra) => {
100
+ if (level === "debug" && !config.debug) return;
101
+ ctx.client.app.log({
102
+ body: {
103
+ service: "preload-skills",
104
+ level,
105
+ message,
106
+ extra
107
+ }
108
+ });
109
+ };
110
+ return {
111
+ config: async (openCodeConfig) => {
112
+ const pluginConfig = openCodeConfig["opencode-plugin-preload-skills"];
113
+ config = {
114
+ ...DEFAULT_CONFIG,
115
+ ...pluginConfig
116
+ };
117
+ if (config.skills.length === 0) {
118
+ log("warn", "No skills configured for preloading");
119
+ return;
120
+ }
121
+ loadedSkills = loadSkills(config.skills, ctx.directory);
122
+ formattedContent = formatSkillsForInjection(loadedSkills);
123
+ const loadedNames = loadedSkills.map((s) => s.name);
124
+ const missingNames = config.skills.filter((s) => !loadedNames.includes(s));
125
+ log("info", `Loaded ${loadedSkills.length} skills for preloading`, {
126
+ loaded: loadedNames,
127
+ missing: missingNames.length > 0 ? missingNames : void 0
128
+ });
129
+ if (missingNames.length > 0) {
130
+ log("warn", "Some configured skills were not found", {
131
+ missing: missingNames
132
+ });
133
+ }
134
+ },
135
+ "chat.message": async (input, output) => {
136
+ if (loadedSkills.length === 0 || !formattedContent) {
137
+ return;
138
+ }
139
+ if (injectedSessions.has(input.sessionID)) {
140
+ log("debug", "Skills already injected for session", {
141
+ sessionID: input.sessionID
142
+ });
143
+ return;
144
+ }
145
+ injectedSessions.add(input.sessionID);
146
+ const syntheticPart = {
147
+ type: "text",
148
+ text: formattedContent
149
+ };
150
+ output.parts.unshift(syntheticPart);
151
+ log("info", "Injected preloaded skills into session", {
152
+ sessionID: input.sessionID,
153
+ skillCount: loadedSkills.length,
154
+ skills: loadedSkills.map((s) => s.name)
155
+ });
156
+ },
157
+ "experimental.session.compacting": async (input, output) => {
158
+ if (!config.persistAfterCompaction || loadedSkills.length === 0) {
159
+ return;
160
+ }
161
+ output.context.push(
162
+ `## Preloaded Skills
163
+
164
+ The following skills were auto-loaded at session start and should persist:
165
+
166
+ ${formattedContent}`
167
+ );
168
+ injectedSessions.delete(input.sessionID);
169
+ log("info", "Added preloaded skills to compaction context", {
170
+ sessionID: input.sessionID,
171
+ skillCount: loadedSkills.length
172
+ });
173
+ },
174
+ event: async ({ event }) => {
175
+ if (event.type === "session.deleted" && "sessionID" in event.properties) {
176
+ const sessionID = event.properties.sessionID;
177
+ injectedSessions.delete(sessionID);
178
+ log("debug", "Cleaned up session tracking", { sessionID });
179
+ }
180
+ }
181
+ };
182
+ };
183
+ var index_default = PreloadSkillsPlugin;
184
+
185
+ exports.PreloadSkillsPlugin = PreloadSkillsPlugin;
186
+ exports.default = index_default;
187
+ exports.formatSkillsForInjection = formatSkillsForInjection;
188
+ exports.loadSkills = loadSkills;
189
+ //# sourceMappingURL=index.cjs.map
190
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/skill-loader.ts","../src/index.ts"],"names":["join","homedir","existsSync","readFileSync"],"mappings":"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","file":"index.cjs","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport type { ParsedSkill } from \"./types.js\"\n\nconst SKILL_FILENAME = \"SKILL.md\"\n\nconst SKILL_SEARCH_PATHS = [\n (dir: string) => join(dir, \".opencode\", \"skills\"),\n (dir: string) => join(dir, \".claude\", \"skills\"),\n () => join(homedir(), \".config\", \"opencode\", \"skills\"),\n () => join(homedir(), \".claude\", \"skills\"),\n]\n\nfunction findSkillFile(skillName: string, projectDir: string): string | null {\n for (const getPath of SKILL_SEARCH_PATHS) {\n const skillDir = getPath(projectDir)\n const skillPath = join(skillDir, skillName, SKILL_FILENAME)\n\n if (existsSync(skillPath)) {\n return skillPath\n }\n }\n return null\n}\n\nfunction parseFrontmatter(content: string): { name?: string; description?: string } {\n const frontmatterMatch = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---/)\n if (!frontmatterMatch?.[1]) {\n return {}\n }\n\n const frontmatter = frontmatterMatch[1]\n const result: { name?: string; description?: string } = {}\n\n const nameMatch = frontmatter.match(/^name:\\s*(.+)$/m)\n if (nameMatch?.[1]) {\n result.name = nameMatch[1].trim()\n }\n\n const descMatch = frontmatter.match(/^description:\\s*(.+)$/m)\n if (descMatch?.[1]) {\n result.description = descMatch[1].trim()\n }\n\n return result\n}\n\nexport function loadSkill(skillName: string, projectDir: string): ParsedSkill | null {\n const filePath = findSkillFile(skillName, projectDir)\n\n if (!filePath) {\n return null\n }\n\n try {\n const content = readFileSync(filePath, \"utf-8\")\n const { name, description } = parseFrontmatter(content)\n\n return {\n name: name ?? skillName,\n description: description ?? \"\",\n content,\n filePath,\n }\n } catch {\n return null\n }\n}\n\nexport function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[] {\n const skills: ParsedSkill[] = []\n\n for (const name of skillNames) {\n const skill = loadSkill(name, projectDir)\n if (skill) {\n skills.push(skill)\n }\n }\n\n return skills\n}\n\nexport function formatSkillsForInjection(skills: ParsedSkill[]): string {\n if (skills.length === 0) {\n return \"\"\n }\n\n const parts = skills.map(\n (skill) =>\n `<preloaded-skill name=\"${skill.name}\">\\n${skill.content}\\n</preloaded-skill>`\n )\n\n return `<preloaded-skills>\nThe following skills have been automatically loaded for this session:\n\n${parts.join(\"\\n\\n\")}\n</preloaded-skills>`\n}\n","import type { Plugin, PluginInput } from \"@opencode-ai/plugin\"\nimport type { Event, UserMessage, Part, Config } from \"@opencode-ai/sdk\"\nimport type { PreloadSkillsConfig, ParsedSkill } from \"./types.js\"\nimport { loadSkills, formatSkillsForInjection } from \"./skill-loader.js\"\n\nexport type { PreloadSkillsConfig, ParsedSkill }\nexport { loadSkills, formatSkillsForInjection }\n\nconst DEFAULT_CONFIG: PreloadSkillsConfig = {\n skills: [],\n persistAfterCompaction: true,\n debug: false,\n}\n\nexport const PreloadSkillsPlugin: Plugin = async (ctx: PluginInput) => {\n const injectedSessions = new Set<string>()\n let loadedSkills: ParsedSkill[] = []\n let formattedContent = \"\"\n let config: PreloadSkillsConfig = DEFAULT_CONFIG\n\n const log = (\n level: \"debug\" | \"info\" | \"warn\" | \"error\",\n message: string,\n extra?: Record<string, unknown>\n ) => {\n if (level === \"debug\" && !config.debug) return\n\n ctx.client.app.log({\n body: {\n service: \"preload-skills\",\n level,\n message,\n extra,\n },\n })\n }\n\n return {\n config: async (openCodeConfig: Config) => {\n const pluginConfig = (openCodeConfig as Record<string, unknown>)[\n \"opencode-plugin-preload-skills\"\n ] as Partial<PreloadSkillsConfig> | undefined\n\n config = {\n ...DEFAULT_CONFIG,\n ...pluginConfig,\n }\n\n if (config.skills.length === 0) {\n log(\"warn\", \"No skills configured for preloading\")\n return\n }\n\n loadedSkills = loadSkills(config.skills, ctx.directory)\n formattedContent = formatSkillsForInjection(loadedSkills)\n\n const loadedNames = loadedSkills.map((s) => s.name)\n const missingNames = config.skills.filter((s) => !loadedNames.includes(s))\n\n log(\"info\", `Loaded ${loadedSkills.length} skills for preloading`, {\n loaded: loadedNames,\n missing: missingNames.length > 0 ? missingNames : undefined,\n })\n\n if (missingNames.length > 0) {\n log(\"warn\", \"Some configured skills were not found\", {\n missing: missingNames,\n })\n }\n },\n\n \"chat.message\": async (\n input: {\n sessionID: string\n agent?: string\n model?: { providerID: string; modelID: string }\n messageID?: string\n variant?: string\n },\n output: { message: UserMessage; parts: Part[] }\n ): Promise<void> => {\n if (loadedSkills.length === 0 || !formattedContent) {\n return\n }\n\n if (injectedSessions.has(input.sessionID)) {\n log(\"debug\", \"Skills already injected for session\", {\n sessionID: input.sessionID,\n })\n return\n }\n\n injectedSessions.add(input.sessionID)\n\n const syntheticPart = {\n type: \"text\",\n text: formattedContent,\n } as Part\n\n output.parts.unshift(syntheticPart)\n\n log(\"info\", \"Injected preloaded skills into session\", {\n sessionID: input.sessionID,\n skillCount: loadedSkills.length,\n skills: loadedSkills.map((s) => s.name),\n })\n },\n\n \"experimental.session.compacting\": async (\n input: { sessionID: string },\n output: { context: string[]; prompt?: string }\n ): Promise<void> => {\n if (!config.persistAfterCompaction || loadedSkills.length === 0) {\n return\n }\n\n output.context.push(\n `## Preloaded Skills\\n\\nThe following skills were auto-loaded at session start and should persist:\\n\\n${formattedContent}`\n )\n\n injectedSessions.delete(input.sessionID)\n\n log(\"info\", \"Added preloaded skills to compaction context\", {\n sessionID: input.sessionID,\n skillCount: loadedSkills.length,\n })\n },\n\n event: async ({ event }: { event: Event }): Promise<void> => {\n if (\n event.type === \"session.deleted\" &&\n \"sessionID\" in event.properties\n ) {\n const sessionID = event.properties.sessionID as string\n injectedSessions.delete(sessionID)\n log(\"debug\", \"Cleaned up session tracking\", { sessionID })\n }\n },\n }\n}\n\nexport default PreloadSkillsPlugin\n"]}
@@ -0,0 +1,20 @@
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+
3
+ interface PreloadSkillsConfig {
4
+ skills: string[];
5
+ persistAfterCompaction?: boolean;
6
+ debug?: boolean;
7
+ }
8
+ interface ParsedSkill {
9
+ name: string;
10
+ description: string;
11
+ content: string;
12
+ filePath: string;
13
+ }
14
+
15
+ declare function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[];
16
+ declare function formatSkillsForInjection(skills: ParsedSkill[]): string;
17
+
18
+ declare const PreloadSkillsPlugin: Plugin;
19
+
20
+ export { type ParsedSkill, type PreloadSkillsConfig, PreloadSkillsPlugin, PreloadSkillsPlugin as default, formatSkillsForInjection, loadSkills };
@@ -0,0 +1,20 @@
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+
3
+ interface PreloadSkillsConfig {
4
+ skills: string[];
5
+ persistAfterCompaction?: boolean;
6
+ debug?: boolean;
7
+ }
8
+ interface ParsedSkill {
9
+ name: string;
10
+ description: string;
11
+ content: string;
12
+ filePath: string;
13
+ }
14
+
15
+ declare function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[];
16
+ declare function formatSkillsForInjection(skills: ParsedSkill[]): string;
17
+
18
+ declare const PreloadSkillsPlugin: Plugin;
19
+
20
+ export { type ParsedSkill, type PreloadSkillsConfig, PreloadSkillsPlugin, PreloadSkillsPlugin as default, formatSkillsForInjection, loadSkills };
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ // src/skill-loader.ts
6
+ var SKILL_FILENAME = "SKILL.md";
7
+ var SKILL_SEARCH_PATHS = [
8
+ (dir) => join(dir, ".opencode", "skills"),
9
+ (dir) => join(dir, ".claude", "skills"),
10
+ () => join(homedir(), ".config", "opencode", "skills"),
11
+ () => join(homedir(), ".claude", "skills")
12
+ ];
13
+ function findSkillFile(skillName, projectDir) {
14
+ for (const getPath of SKILL_SEARCH_PATHS) {
15
+ const skillDir = getPath(projectDir);
16
+ const skillPath = join(skillDir, skillName, SKILL_FILENAME);
17
+ if (existsSync(skillPath)) {
18
+ return skillPath;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ function parseFrontmatter(content) {
24
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
25
+ if (!frontmatterMatch?.[1]) {
26
+ return {};
27
+ }
28
+ const frontmatter = frontmatterMatch[1];
29
+ const result = {};
30
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
31
+ if (nameMatch?.[1]) {
32
+ result.name = nameMatch[1].trim();
33
+ }
34
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
35
+ if (descMatch?.[1]) {
36
+ result.description = descMatch[1].trim();
37
+ }
38
+ return result;
39
+ }
40
+ function loadSkill(skillName, projectDir) {
41
+ const filePath = findSkillFile(skillName, projectDir);
42
+ if (!filePath) {
43
+ return null;
44
+ }
45
+ try {
46
+ const content = readFileSync(filePath, "utf-8");
47
+ const { name, description } = parseFrontmatter(content);
48
+ return {
49
+ name: name ?? skillName,
50
+ description: description ?? "",
51
+ content,
52
+ filePath
53
+ };
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function loadSkills(skillNames, projectDir) {
59
+ const skills = [];
60
+ for (const name of skillNames) {
61
+ const skill = loadSkill(name, projectDir);
62
+ if (skill) {
63
+ skills.push(skill);
64
+ }
65
+ }
66
+ return skills;
67
+ }
68
+ function formatSkillsForInjection(skills) {
69
+ if (skills.length === 0) {
70
+ return "";
71
+ }
72
+ const parts = skills.map(
73
+ (skill) => `<preloaded-skill name="${skill.name}">
74
+ ${skill.content}
75
+ </preloaded-skill>`
76
+ );
77
+ return `<preloaded-skills>
78
+ The following skills have been automatically loaded for this session:
79
+
80
+ ${parts.join("\n\n")}
81
+ </preloaded-skills>`;
82
+ }
83
+
84
+ // src/index.ts
85
+ var DEFAULT_CONFIG = {
86
+ skills: [],
87
+ persistAfterCompaction: true,
88
+ debug: false
89
+ };
90
+ var PreloadSkillsPlugin = async (ctx) => {
91
+ const injectedSessions = /* @__PURE__ */ new Set();
92
+ let loadedSkills = [];
93
+ let formattedContent = "";
94
+ let config = DEFAULT_CONFIG;
95
+ const log = (level, message, extra) => {
96
+ if (level === "debug" && !config.debug) return;
97
+ ctx.client.app.log({
98
+ body: {
99
+ service: "preload-skills",
100
+ level,
101
+ message,
102
+ extra
103
+ }
104
+ });
105
+ };
106
+ return {
107
+ config: async (openCodeConfig) => {
108
+ const pluginConfig = openCodeConfig["opencode-plugin-preload-skills"];
109
+ config = {
110
+ ...DEFAULT_CONFIG,
111
+ ...pluginConfig
112
+ };
113
+ if (config.skills.length === 0) {
114
+ log("warn", "No skills configured for preloading");
115
+ return;
116
+ }
117
+ loadedSkills = loadSkills(config.skills, ctx.directory);
118
+ formattedContent = formatSkillsForInjection(loadedSkills);
119
+ const loadedNames = loadedSkills.map((s) => s.name);
120
+ const missingNames = config.skills.filter((s) => !loadedNames.includes(s));
121
+ log("info", `Loaded ${loadedSkills.length} skills for preloading`, {
122
+ loaded: loadedNames,
123
+ missing: missingNames.length > 0 ? missingNames : void 0
124
+ });
125
+ if (missingNames.length > 0) {
126
+ log("warn", "Some configured skills were not found", {
127
+ missing: missingNames
128
+ });
129
+ }
130
+ },
131
+ "chat.message": async (input, output) => {
132
+ if (loadedSkills.length === 0 || !formattedContent) {
133
+ return;
134
+ }
135
+ if (injectedSessions.has(input.sessionID)) {
136
+ log("debug", "Skills already injected for session", {
137
+ sessionID: input.sessionID
138
+ });
139
+ return;
140
+ }
141
+ injectedSessions.add(input.sessionID);
142
+ const syntheticPart = {
143
+ type: "text",
144
+ text: formattedContent
145
+ };
146
+ output.parts.unshift(syntheticPart);
147
+ log("info", "Injected preloaded skills into session", {
148
+ sessionID: input.sessionID,
149
+ skillCount: loadedSkills.length,
150
+ skills: loadedSkills.map((s) => s.name)
151
+ });
152
+ },
153
+ "experimental.session.compacting": async (input, output) => {
154
+ if (!config.persistAfterCompaction || loadedSkills.length === 0) {
155
+ return;
156
+ }
157
+ output.context.push(
158
+ `## Preloaded Skills
159
+
160
+ The following skills were auto-loaded at session start and should persist:
161
+
162
+ ${formattedContent}`
163
+ );
164
+ injectedSessions.delete(input.sessionID);
165
+ log("info", "Added preloaded skills to compaction context", {
166
+ sessionID: input.sessionID,
167
+ skillCount: loadedSkills.length
168
+ });
169
+ },
170
+ event: async ({ event }) => {
171
+ if (event.type === "session.deleted" && "sessionID" in event.properties) {
172
+ const sessionID = event.properties.sessionID;
173
+ injectedSessions.delete(sessionID);
174
+ log("debug", "Cleaned up session tracking", { sessionID });
175
+ }
176
+ }
177
+ };
178
+ };
179
+ var index_default = PreloadSkillsPlugin;
180
+
181
+ export { PreloadSkillsPlugin, index_default as default, formatSkillsForInjection, loadSkills };
182
+ //# sourceMappingURL=index.js.map
183
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/skill-loader.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport type { ParsedSkill } from \"./types.js\"\n\nconst SKILL_FILENAME = \"SKILL.md\"\n\nconst SKILL_SEARCH_PATHS = [\n (dir: string) => join(dir, \".opencode\", \"skills\"),\n (dir: string) => join(dir, \".claude\", \"skills\"),\n () => join(homedir(), \".config\", \"opencode\", \"skills\"),\n () => join(homedir(), \".claude\", \"skills\"),\n]\n\nfunction findSkillFile(skillName: string, projectDir: string): string | null {\n for (const getPath of SKILL_SEARCH_PATHS) {\n const skillDir = getPath(projectDir)\n const skillPath = join(skillDir, skillName, SKILL_FILENAME)\n\n if (existsSync(skillPath)) {\n return skillPath\n }\n }\n return null\n}\n\nfunction parseFrontmatter(content: string): { name?: string; description?: string } {\n const frontmatterMatch = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---/)\n if (!frontmatterMatch?.[1]) {\n return {}\n }\n\n const frontmatter = frontmatterMatch[1]\n const result: { name?: string; description?: string } = {}\n\n const nameMatch = frontmatter.match(/^name:\\s*(.+)$/m)\n if (nameMatch?.[1]) {\n result.name = nameMatch[1].trim()\n }\n\n const descMatch = frontmatter.match(/^description:\\s*(.+)$/m)\n if (descMatch?.[1]) {\n result.description = descMatch[1].trim()\n }\n\n return result\n}\n\nexport function loadSkill(skillName: string, projectDir: string): ParsedSkill | null {\n const filePath = findSkillFile(skillName, projectDir)\n\n if (!filePath) {\n return null\n }\n\n try {\n const content = readFileSync(filePath, \"utf-8\")\n const { name, description } = parseFrontmatter(content)\n\n return {\n name: name ?? skillName,\n description: description ?? \"\",\n content,\n filePath,\n }\n } catch {\n return null\n }\n}\n\nexport function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[] {\n const skills: ParsedSkill[] = []\n\n for (const name of skillNames) {\n const skill = loadSkill(name, projectDir)\n if (skill) {\n skills.push(skill)\n }\n }\n\n return skills\n}\n\nexport function formatSkillsForInjection(skills: ParsedSkill[]): string {\n if (skills.length === 0) {\n return \"\"\n }\n\n const parts = skills.map(\n (skill) =>\n `<preloaded-skill name=\"${skill.name}\">\\n${skill.content}\\n</preloaded-skill>`\n )\n\n return `<preloaded-skills>\nThe following skills have been automatically loaded for this session:\n\n${parts.join(\"\\n\\n\")}\n</preloaded-skills>`\n}\n","import type { Plugin, PluginInput } from \"@opencode-ai/plugin\"\nimport type { Event, UserMessage, Part, Config } from \"@opencode-ai/sdk\"\nimport type { PreloadSkillsConfig, ParsedSkill } from \"./types.js\"\nimport { loadSkills, formatSkillsForInjection } from \"./skill-loader.js\"\n\nexport type { PreloadSkillsConfig, ParsedSkill }\nexport { loadSkills, formatSkillsForInjection }\n\nconst DEFAULT_CONFIG: PreloadSkillsConfig = {\n skills: [],\n persistAfterCompaction: true,\n debug: false,\n}\n\nexport const PreloadSkillsPlugin: Plugin = async (ctx: PluginInput) => {\n const injectedSessions = new Set<string>()\n let loadedSkills: ParsedSkill[] = []\n let formattedContent = \"\"\n let config: PreloadSkillsConfig = DEFAULT_CONFIG\n\n const log = (\n level: \"debug\" | \"info\" | \"warn\" | \"error\",\n message: string,\n extra?: Record<string, unknown>\n ) => {\n if (level === \"debug\" && !config.debug) return\n\n ctx.client.app.log({\n body: {\n service: \"preload-skills\",\n level,\n message,\n extra,\n },\n })\n }\n\n return {\n config: async (openCodeConfig: Config) => {\n const pluginConfig = (openCodeConfig as Record<string, unknown>)[\n \"opencode-plugin-preload-skills\"\n ] as Partial<PreloadSkillsConfig> | undefined\n\n config = {\n ...DEFAULT_CONFIG,\n ...pluginConfig,\n }\n\n if (config.skills.length === 0) {\n log(\"warn\", \"No skills configured for preloading\")\n return\n }\n\n loadedSkills = loadSkills(config.skills, ctx.directory)\n formattedContent = formatSkillsForInjection(loadedSkills)\n\n const loadedNames = loadedSkills.map((s) => s.name)\n const missingNames = config.skills.filter((s) => !loadedNames.includes(s))\n\n log(\"info\", `Loaded ${loadedSkills.length} skills for preloading`, {\n loaded: loadedNames,\n missing: missingNames.length > 0 ? missingNames : undefined,\n })\n\n if (missingNames.length > 0) {\n log(\"warn\", \"Some configured skills were not found\", {\n missing: missingNames,\n })\n }\n },\n\n \"chat.message\": async (\n input: {\n sessionID: string\n agent?: string\n model?: { providerID: string; modelID: string }\n messageID?: string\n variant?: string\n },\n output: { message: UserMessage; parts: Part[] }\n ): Promise<void> => {\n if (loadedSkills.length === 0 || !formattedContent) {\n return\n }\n\n if (injectedSessions.has(input.sessionID)) {\n log(\"debug\", \"Skills already injected for session\", {\n sessionID: input.sessionID,\n })\n return\n }\n\n injectedSessions.add(input.sessionID)\n\n const syntheticPart = {\n type: \"text\",\n text: formattedContent,\n } as Part\n\n output.parts.unshift(syntheticPart)\n\n log(\"info\", \"Injected preloaded skills into session\", {\n sessionID: input.sessionID,\n skillCount: loadedSkills.length,\n skills: loadedSkills.map((s) => s.name),\n })\n },\n\n \"experimental.session.compacting\": async (\n input: { sessionID: string },\n output: { context: string[]; prompt?: string }\n ): Promise<void> => {\n if (!config.persistAfterCompaction || loadedSkills.length === 0) {\n return\n }\n\n output.context.push(\n `## Preloaded Skills\\n\\nThe following skills were auto-loaded at session start and should persist:\\n\\n${formattedContent}`\n )\n\n injectedSessions.delete(input.sessionID)\n\n log(\"info\", \"Added preloaded skills to compaction context\", {\n sessionID: input.sessionID,\n skillCount: loadedSkills.length,\n })\n },\n\n event: async ({ event }: { event: Event }): Promise<void> => {\n if (\n event.type === \"session.deleted\" &&\n \"sessionID\" in event.properties\n ) {\n const sessionID = event.properties.sessionID as string\n injectedSessions.delete(sessionID)\n log(\"debug\", \"Cleaned up session tracking\", { sessionID })\n }\n },\n }\n}\n\nexport default PreloadSkillsPlugin\n"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "opencode-plugin-preload-skills",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode plugin that auto-loads specified skills into agent memory on session start",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "prepublishOnly": "npm run build",
28
+ "typecheck": "tsc --noEmit"
29
+ },
30
+ "keywords": [
31
+ "opencode",
32
+ "opencode-plugin",
33
+ "skills",
34
+ "preload",
35
+ "agent"
36
+ ],
37
+ "author": "",
38
+ "license": "MIT",
39
+ "peerDependencies": {
40
+ "@opencode-ai/plugin": ">=1.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@opencode-ai/plugin": "^1.0.85",
44
+ "@types/node": "^22.0.0",
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.7.0"
47
+ }
48
+ }