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 +188 -0
- package/dist/index.cjs +190 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
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
|
+
[](https://www.npmjs.com/package/opencode-plugin-preload-skills)
|
|
6
|
+
[](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":";;;;;;;;;AAKA,IAAM,cAAA,GAAiB,UAAA;AAEvB,IAAM,kBAAA,GAAqB;AAAA,EACzB,CAAC,GAAA,KAAgBA,SAAA,CAAK,GAAA,EAAK,aAAa,QAAQ,CAAA;AAAA,EAChD,CAAC,GAAA,KAAgBA,SAAA,CAAK,GAAA,EAAK,WAAW,QAAQ,CAAA;AAAA,EAC9C,MAAMA,SAAA,CAAKC,UAAA,EAAQ,EAAG,SAAA,EAAW,YAAY,QAAQ,CAAA;AAAA,EACrD,MAAMD,SAAA,CAAKC,UAAA,EAAQ,EAAG,WAAW,QAAQ;AAC3C,CAAA;AAEA,SAAS,aAAA,CAAc,WAAmB,UAAA,EAAmC;AAC3E,EAAA,KAAA,MAAW,WAAW,kBAAA,EAAoB;AACxC,IAAA,MAAM,QAAA,GAAW,QAAQ,UAAU,CAAA;AACnC,IAAA,MAAM,SAAA,GAAYD,SAAA,CAAK,QAAA,EAAU,SAAA,EAAW,cAAc,CAAA;AAE1D,IAAA,IAAIE,aAAA,CAAW,SAAS,CAAA,EAAG;AACzB,MAAA,OAAO,SAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,iBAAiB,OAAA,EAA0D;AAClF,EAAA,MAAM,gBAAA,GAAmB,OAAA,CAAQ,KAAA,CAAM,0BAA0B,CAAA;AACjE,EAAA,IAAI,CAAC,gBAAA,GAAmB,CAAC,CAAA,EAAG;AAC1B,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,WAAA,GAAc,iBAAiB,CAAC,CAAA;AACtC,EAAA,MAAM,SAAkD,EAAC;AAEzD,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,KAAA,CAAM,iBAAiB,CAAA;AACrD,EAAA,IAAI,SAAA,GAAY,CAAC,CAAA,EAAG;AAClB,IAAA,MAAA,CAAO,IAAA,GAAO,SAAA,CAAU,CAAC,CAAA,CAAE,IAAA,EAAK;AAAA,EAClC;AAEA,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,KAAA,CAAM,wBAAwB,CAAA;AAC5D,EAAA,IAAI,SAAA,GAAY,CAAC,CAAA,EAAG;AAClB,IAAA,MAAA,CAAO,WAAA,GAAc,SAAA,CAAU,CAAC,CAAA,CAAE,IAAA,EAAK;AAAA,EACzC;AAEA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,SAAA,CAAU,WAAmB,UAAA,EAAwC;AACnF,EAAA,MAAM,QAAA,GAAW,aAAA,CAAc,SAAA,EAAW,UAAU,CAAA;AAEpD,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAUC,eAAA,CAAa,QAAA,EAAU,OAAO,CAAA;AAC9C,IAAA,MAAM,EAAE,IAAA,EAAM,WAAA,EAAY,GAAI,iBAAiB,OAAO,CAAA;AAEtD,IAAA,OAAO;AAAA,MACL,MAAM,IAAA,IAAQ,SAAA;AAAA,MACd,aAAa,WAAA,IAAe,EAAA;AAAA,MAC5B,OAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,UAAA,CAAW,YAAsB,UAAA,EAAmC;AAClF,EAAA,MAAM,SAAwB,EAAC;AAE/B,EAAA,KAAA,MAAW,QAAQ,UAAA,EAAY;AAC7B,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,IAAA,EAAM,UAAU,CAAA;AACxC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACnB;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,yBAAyB,MAAA,EAA+B;AACtE,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,IAAA,OAAO,EAAA;AAAA,EACT;AAEA,EAAA,MAAM,QAAQ,MAAA,CAAO,GAAA;AAAA,IACnB,CAAC,KAAA,KACC,CAAA,uBAAA,EAA0B,KAAA,CAAM,IAAI,CAAA;AAAA,EAAO,MAAM,OAAO;AAAA,kBAAA;AAAA,GAC5D;AAEA,EAAA,OAAO,CAAA;AAAA;;AAAA,EAGP,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC;AAAA,mBAAA,CAAA;AAEpB;;;AC1FA,IAAM,cAAA,GAAsC;AAAA,EAC1C,QAAQ,EAAC;AAAA,EACT,sBAAA,EAAwB,IAAA;AAAA,EACxB,KAAA,EAAO;AACT,CAAA;AAEO,IAAM,mBAAA,GAA8B,OAAO,GAAA,KAAqB;AACrE,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAY;AACzC,EAAA,IAAI,eAA8B,EAAC;AACnC,EAAA,IAAI,gBAAA,GAAmB,EAAA;AACvB,EAAA,IAAI,MAAA,GAA8B,cAAA;AAElC,EAAA,MAAM,GAAA,GAAM,CACV,KAAA,EACA,OAAA,EACA,KAAA,KACG;AACH,IAAA,IAAI,KAAA,KAAU,OAAA,IAAW,CAAC,MAAA,CAAO,KAAA,EAAO;AAExC,IAAA,GAAA,CAAI,MAAA,CAAO,IAAI,GAAA,CAAI;AAAA,MACjB,IAAA,EAAM;AAAA,QACJ,OAAA,EAAS,gBAAA;AAAA,QACT,KAAA;AAAA,QACA,OAAA;AAAA,QACA;AAAA;AACF,KACD,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,OAAO,cAAA,KAA2B;AACxC,MAAA,MAAM,YAAA,GAAgB,eACpB,gCACF,CAAA;AAEA,MAAA,MAAA,GAAS;AAAA,QACP,GAAG,cAAA;AAAA,QACH,GAAG;AAAA,OACL;AAEA,MAAA,IAAI,MAAA,CAAO,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAC9B,QAAA,GAAA,CAAI,QAAQ,qCAAqC,CAAA;AACjD,QAAA;AAAA,MACF;AAEA,MAAA,YAAA,GAAe,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,GAAA,CAAI,SAAS,CAAA;AACtD,MAAA,gBAAA,GAAmB,yBAAyB,YAAY,CAAA;AAExD,MAAA,MAAM,cAAc,YAAA,CAAa,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAClD,MAAA,MAAM,YAAA,GAAe,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,CAAC,MAAM,CAAC,WAAA,CAAY,QAAA,CAAS,CAAC,CAAC,CAAA;AAEzE,MAAA,GAAA,CAAI,MAAA,EAAQ,CAAA,OAAA,EAAU,YAAA,CAAa,MAAM,CAAA,sBAAA,CAAA,EAA0B;AAAA,QACjE,MAAA,EAAQ,WAAA;AAAA,QACR,OAAA,EAAS,YAAA,CAAa,MAAA,GAAS,CAAA,GAAI,YAAA,GAAe;AAAA,OACnD,CAAA;AAED,MAAA,IAAI,YAAA,CAAa,SAAS,CAAA,EAAG;AAC3B,QAAA,GAAA,CAAI,QAAQ,uCAAA,EAAyC;AAAA,UACnD,OAAA,EAAS;AAAA,SACV,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IAEA,cAAA,EAAgB,OACd,KAAA,EAOA,MAAA,KACkB;AAClB,MAAA,IAAI,YAAA,CAAa,MAAA,KAAW,CAAA,IAAK,CAAC,gBAAA,EAAkB;AAClD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACzC,QAAA,GAAA,CAAI,SAAS,qCAAA,EAAuC;AAAA,UAClD,WAAW,KAAA,CAAM;AAAA,SAClB,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,gBAAA,CAAiB,GAAA,CAAI,MAAM,SAAS,CAAA;AAEpC,MAAA,MAAM,aAAA,GAAgB;AAAA,QACpB,IAAA,EAAM,MAAA;AAAA,QACN,IAAA,EAAM;AAAA,OACR;AAEA,MAAA,MAAA,CAAO,KAAA,CAAM,QAAQ,aAAa,CAAA;AAElC,MAAA,GAAA,CAAI,QAAQ,wCAAA,EAA0C;AAAA,QACpD,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,YAAA,CAAa,MAAA;AAAA,QACzB,QAAQ,YAAA,CAAa,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI;AAAA,OACvC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,iCAAA,EAAmC,OACjC,KAAA,EACA,MAAA,KACkB;AAClB,MAAA,IAAI,CAAC,MAAA,CAAO,sBAAA,IAA0B,YAAA,CAAa,WAAW,CAAA,EAAG;AAC/D,QAAA;AAAA,MACF;AAEA,MAAA,MAAA,CAAO,OAAA,CAAQ,IAAA;AAAA,QACb,CAAA;;AAAA;;AAAA,EAAwG,gBAAgB,CAAA;AAAA,OAC1H;AAEA,MAAA,gBAAA,CAAiB,MAAA,CAAO,MAAM,SAAS,CAAA;AAEvC,MAAA,GAAA,CAAI,QAAQ,8CAAA,EAAgD;AAAA,QAC1D,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,YAAA,CAAa;AAAA,OAC1B,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,KAAA,EAAO,OAAO,EAAE,KAAA,EAAM,KAAuC;AAC3D,MAAA,IACE,KAAA,CAAM,IAAA,KAAS,iBAAA,IACf,WAAA,IAAe,MAAM,UAAA,EACrB;AACA,QAAA,MAAM,SAAA,GAAY,MAAM,UAAA,CAAW,SAAA;AACnC,QAAA,gBAAA,CAAiB,OAAO,SAAS,CAAA;AACjC,QAAA,GAAA,CAAI,OAAA,EAAS,6BAAA,EAA+B,EAAE,SAAA,EAAW,CAAA;AAAA,MAC3D;AAAA,IACF;AAAA,GACF;AACF;AAEA,IAAO,aAAA,GAAQ","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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|