opencode-plugin-preload-skills 1.1.4 → 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,6 +1,6 @@
1
1
  # opencode-plugin-preload-skills
2
2
 
3
- > Automatically load skills into agent memory at session start
3
+ > Automatically load skills into agent memory at session start or when touching specific file types
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/opencode-plugin-preload-skills.svg)](https://www.npmjs.com/package/opencode-plugin-preload-skills)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -13,10 +13,11 @@ A plugin for [OpenCode](https://opencode.ai) that preloads specified skills into
13
13
  ## Features
14
14
 
15
15
  - **Automatic Loading** — Skills are injected on the first message of each session
16
+ - **File-Type Triggers** — Load skills automatically when agent touches specific file types
16
17
  - **Compaction Persistence** — Skills survive context compaction and remain available
17
18
  - **Multiple Skill Sources** — Searches project and global skill directories
18
19
  - **Debug Logging** — Optional verbose logging for troubleshooting
19
- - **Zero Runtime Overhead** — Skills loaded once per session
20
+ - **Smart Caching** — Skills loaded once per session, no duplicates
20
21
 
21
22
  > **⚠️ Warning:** Preloaded skills consume context window tokens on every session. Large skills or many skills can significantly reduce available context for your conversation. Keep skills concise and only preload what's truly needed for every session.
22
23
 
@@ -79,7 +80,8 @@ Create `preload-skills.json` in one of these locations:
79
80
 
80
81
  | Option | Type | Default | Description |
81
82
  |--------|------|---------|-------------|
82
- | `skills` | `string[]` | `[]` | Skill names to auto-load |
83
+ | `skills` | `string[]` | `[]` | Skills to always load at session start |
84
+ | `fileTypeSkills` | `Record<string, string[]>` | `{}` | Skills to load when specific file types are accessed |
83
85
  | `persistAfterCompaction` | `boolean` | `true` | Re-inject skills after context compaction |
84
86
  | `debug` | `boolean` | `false` | Enable debug logging |
85
87
 
@@ -88,15 +90,28 @@ Create `preload-skills.json` in one of these locations:
88
90
  ```json
89
91
  {
90
92
  "skills": [
91
- "coding-standards",
92
- "api-patterns",
93
- "testing-guide"
93
+ "coding-standards"
94
94
  ],
95
+ "fileTypeSkills": {
96
+ ".py": ["flask", "python-best-practices"],
97
+ ".ts,.tsx": ["typescript-advanced-types", "react-patterns"],
98
+ ".go": ["golang-patterns"],
99
+ ".rs": ["rust-idioms"]
100
+ },
95
101
  "persistAfterCompaction": true,
96
102
  "debug": false
97
103
  }
98
104
  ```
99
105
 
106
+ ### File-Type Skills
107
+
108
+ The `fileTypeSkills` option maps file extensions to skills that should be loaded when the agent reads, edits, or writes files with those extensions.
109
+
110
+ - **Keys**: Comma-separated file extensions (e.g., `".ts,.tsx"`)
111
+ - **Values**: Array of skill names to load
112
+ - **Trigger**: When agent uses `read`, `edit`, `write`, `glob`, or `grep` on matching files
113
+ - **Deduplication**: Each skill is only loaded once per session, even if multiple files match
114
+
100
115
  ---
101
116
 
102
117
  ## Skill Locations
@@ -155,19 +170,30 @@ Session Start
155
170
 
156
171
 
157
172
  ┌─────────────────────┐
158
- │ First message │──▶ Skills injected as synthetic content
173
+ │ First message │──▶ Initial skills injected
159
174
  │ in session │
160
175
  └─────────────────────┘
161
176
 
162
177
 
163
178
  ┌─────────────────────┐
164
- Context │──▶ Skills added to compaction context
165
- compaction (if persistAfterCompaction: true)
179
+ Agent touches │──▶ File-type skills loaded if extension
180
+ a file matches and skill not yet loaded
181
+ └─────────────────────┘
182
+
183
+
184
+ ┌─────────────────────┐
185
+ │ Next message │──▶ Pending file-type skills injected
186
+ └─────────────────────┘
187
+
188
+
189
+ ┌─────────────────────┐
190
+ │ Context │──▶ All loaded skills added to
191
+ │ compaction │ compaction context
166
192
  └─────────────────────┘
167
193
 
168
194
 
169
195
  ┌─────────────────────┐
170
- │ Session │──▶ Cleanup session tracking
196
+ │ Session │──▶ Cleanup session state
171
197
  │ deleted │
172
198
  └─────────────────────┘
173
199
  ```
@@ -177,8 +203,8 @@ Session Start
177
203
  ## Best Practices
178
204
 
179
205
  - **Keep skills concise** — Every token counts against your context window
180
- - **Preload sparingly** — Only include skills needed for *every* session
181
- - **Use on-demand loading** — For situational skills, use OpenCode's native `skill` tool instead
206
+ - **Use `fileTypeSkills` for language-specific knowledge** — Only load Python skills when working with `.py` files
207
+ - **Preload sparingly** — Only include truly universal skills in `skills` array
182
208
  - **Monitor token usage** — Large skills can reduce conversation capacity by thousands of tokens
183
209
 
184
210
  ---
package/dist/index.cjs CHANGED
@@ -90,8 +90,10 @@ ${parts.join("\n\n")}
90
90
 
91
91
  // src/index.ts
92
92
  var CONFIG_FILENAME = "preload-skills.json";
93
+ var FILE_TOOLS = ["read", "edit", "write", "glob", "grep"];
93
94
  var DEFAULT_CONFIG = {
94
95
  skills: [],
96
+ fileTypeSkills: {},
95
97
  persistAfterCompaction: true,
96
98
  debug: false
97
99
  };
@@ -108,6 +110,16 @@ function findConfigFile(projectDir) {
108
110
  }
109
111
  return null;
110
112
  }
113
+ function parseFileTypeSkills(raw) {
114
+ if (!raw || typeof raw !== "object") return {};
115
+ const result = {};
116
+ for (const [key, value] of Object.entries(raw)) {
117
+ if (Array.isArray(value)) {
118
+ result[key] = value.filter((v) => typeof v === "string");
119
+ }
120
+ }
121
+ return result;
122
+ }
111
123
  function loadConfigFile(projectDir) {
112
124
  const configPath = findConfigFile(projectDir);
113
125
  if (!configPath) {
@@ -118,6 +130,7 @@ function loadConfigFile(projectDir) {
118
130
  const parsed = JSON.parse(content);
119
131
  return {
120
132
  skills: Array.isArray(parsed.skills) ? parsed.skills : [],
133
+ fileTypeSkills: parseFileTypeSkills(parsed.fileTypeSkills),
121
134
  persistAfterCompaction: typeof parsed.persistAfterCompaction === "boolean" ? parsed.persistAfterCompaction : void 0,
122
135
  debug: typeof parsed.debug === "boolean" ? parsed.debug : void 0
123
136
  };
@@ -125,8 +138,24 @@ function loadConfigFile(projectDir) {
125
138
  return {};
126
139
  }
127
140
  }
141
+ function getFilePathFromArgs(args) {
142
+ if (typeof args.filePath === "string") return args.filePath;
143
+ if (typeof args.path === "string") return args.path;
144
+ if (typeof args.file === "string") return args.file;
145
+ return null;
146
+ }
147
+ function getSkillsForExtension(ext, fileTypeSkills) {
148
+ const skills = [];
149
+ for (const [pattern, skillNames] of Object.entries(fileTypeSkills)) {
150
+ const extensions = pattern.split(",").map((e) => e.trim().toLowerCase());
151
+ if (extensions.includes(ext.toLowerCase())) {
152
+ skills.push(...skillNames);
153
+ }
154
+ }
155
+ return [...new Set(skills)];
156
+ }
128
157
  var PreloadSkillsPlugin = async (ctx) => {
129
- const injectedSessions = /* @__PURE__ */ new Set();
158
+ const sessionStates = /* @__PURE__ */ new Map();
130
159
  const fileConfig = loadConfigFile(ctx.directory);
131
160
  const config = {
132
161
  ...DEFAULT_CONFIG,
@@ -143,16 +172,28 @@ var PreloadSkillsPlugin = async (ctx) => {
143
172
  }
144
173
  });
145
174
  };
146
- let loadedSkills = [];
147
- let formattedContent = "";
148
- if (config.skills.length === 0) {
149
- log("warn", "No skills configured for preloading. Create .opencode/preload-skills.json");
150
- } else {
151
- loadedSkills = loadSkills(config.skills, ctx.directory);
152
- formattedContent = formatSkillsForInjection(loadedSkills);
153
- const loadedNames = loadedSkills.map((s) => s.name);
175
+ const skillCache = /* @__PURE__ */ new Map();
176
+ const getOrLoadSkill = (skillName) => {
177
+ if (skillCache.has(skillName)) {
178
+ return skillCache.get(skillName) ?? null;
179
+ }
180
+ const skill = loadSkill(skillName, ctx.directory);
181
+ if (skill) {
182
+ skillCache.set(skillName, skill);
183
+ }
184
+ return skill;
185
+ };
186
+ let initialSkills = [];
187
+ let initialFormattedContent = "";
188
+ if (config.skills.length > 0) {
189
+ initialSkills = loadSkills(config.skills, ctx.directory);
190
+ initialFormattedContent = formatSkillsForInjection(initialSkills);
191
+ for (const skill of initialSkills) {
192
+ skillCache.set(skill.name, skill);
193
+ }
194
+ const loadedNames = initialSkills.map((s) => s.name);
154
195
  const missingNames = config.skills.filter((s) => !loadedNames.includes(s));
155
- log("info", `Loaded ${loadedSkills.length} skills for preloading`, {
196
+ log("info", `Loaded ${initialSkills.length} skills for preloading`, {
156
197
  loaded: loadedNames,
157
198
  missing: missingNames.length > 0 ? missingNames : void 0
158
199
  });
@@ -162,57 +203,118 @@ var PreloadSkillsPlugin = async (ctx) => {
162
203
  });
163
204
  }
164
205
  }
206
+ const hasFileTypeSkills = Object.keys(config.fileTypeSkills ?? {}).length > 0;
207
+ if (config.skills.length === 0 && !hasFileTypeSkills) {
208
+ log("warn", "No skills configured. Create .opencode/preload-skills.json");
209
+ }
210
+ const getSessionState = (sessionID) => {
211
+ if (!sessionStates.has(sessionID)) {
212
+ sessionStates.set(sessionID, {
213
+ initialSkillsInjected: false,
214
+ loadedSkills: new Set(initialSkills.map((s) => s.name))
215
+ });
216
+ }
217
+ return sessionStates.get(sessionID);
218
+ };
219
+ const pendingSkillInjections = /* @__PURE__ */ new Map();
165
220
  return {
166
221
  "chat.message": async (input, output) => {
167
- if (loadedSkills.length === 0 || !formattedContent) {
168
- return;
169
- }
170
- if (!input.sessionID) {
171
- return;
172
- }
173
- if (injectedSessions.has(input.sessionID)) {
174
- log("debug", "Skills already injected for session", {
175
- sessionID: input.sessionID
222
+ if (!input.sessionID) return;
223
+ const state = getSessionState(input.sessionID);
224
+ const firstTextPart = output.parts.find((p) => p.type === "text");
225
+ if (!firstTextPart || !("text" in firstTextPart)) return;
226
+ const contentToInject = [];
227
+ if (!state.initialSkillsInjected && initialFormattedContent) {
228
+ contentToInject.push(initialFormattedContent);
229
+ state.initialSkillsInjected = true;
230
+ log("info", "Injected initial preloaded skills", {
231
+ sessionID: input.sessionID,
232
+ skills: initialSkills.map((s) => s.name)
176
233
  });
177
- return;
178
234
  }
179
- injectedSessions.add(input.sessionID);
180
- const firstTextPart = output.parts.find((p) => p.type === "text");
181
- if (firstTextPart && "text" in firstTextPart) {
182
- firstTextPart.text = `${formattedContent}
235
+ const pending = pendingSkillInjections.get(input.sessionID);
236
+ if (pending && pending.length > 0) {
237
+ const formatted = formatSkillsForInjection(pending);
238
+ if (formatted) {
239
+ contentToInject.push(formatted);
240
+ log("info", "Injected file-type triggered skills", {
241
+ sessionID: input.sessionID,
242
+ skills: pending.map((s) => s.name)
243
+ });
244
+ }
245
+ pendingSkillInjections.delete(input.sessionID);
246
+ }
247
+ if (contentToInject.length > 0) {
248
+ firstTextPart.text = `${contentToInject.join("\n\n")}
183
249
 
184
250
  ---
185
251
 
186
252
  ${firstTextPart.text}`;
187
253
  }
188
- log("info", "Injected preloaded skills into session", {
189
- sessionID: input.sessionID,
190
- skillCount: loadedSkills.length,
191
- skills: loadedSkills.map((s) => s.name)
192
- });
254
+ },
255
+ "tool.execute.after": async (input, _output) => {
256
+ if (!hasFileTypeSkills) return;
257
+ if (!FILE_TOOLS.includes(input.tool)) return;
258
+ if (!input.sessionID) return;
259
+ const state = getSessionState(input.sessionID);
260
+ const toolArgs = _output.metadata?.args;
261
+ if (!toolArgs) return;
262
+ const filePath = getFilePathFromArgs(toolArgs);
263
+ if (!filePath) return;
264
+ const ext = path.extname(filePath);
265
+ if (!ext) return;
266
+ const skillNames = getSkillsForExtension(ext, config.fileTypeSkills ?? {});
267
+ if (skillNames.length === 0) return;
268
+ const newSkills = [];
269
+ for (const name of skillNames) {
270
+ if (state.loadedSkills.has(name)) continue;
271
+ const skill = getOrLoadSkill(name);
272
+ if (skill) {
273
+ newSkills.push(skill);
274
+ state.loadedSkills.add(name);
275
+ }
276
+ }
277
+ if (newSkills.length > 0) {
278
+ const existing = pendingSkillInjections.get(input.sessionID) ?? [];
279
+ pendingSkillInjections.set(input.sessionID, [...existing, ...newSkills]);
280
+ log("debug", "Queued file-type skills for injection", {
281
+ sessionID: input.sessionID,
282
+ filePath,
283
+ extension: ext,
284
+ skills: newSkills.map((s) => s.name)
285
+ });
286
+ }
193
287
  },
194
288
  "experimental.session.compacting": async (input, output) => {
195
- if (!config.persistAfterCompaction || loadedSkills.length === 0) {
196
- return;
289
+ if (!config.persistAfterCompaction) return;
290
+ const state = sessionStates.get(input.sessionID);
291
+ if (!state || state.loadedSkills.size === 0) return;
292
+ const allLoadedSkills = [];
293
+ for (const name of state.loadedSkills) {
294
+ const skill = skillCache.get(name);
295
+ if (skill) allLoadedSkills.push(skill);
197
296
  }
297
+ if (allLoadedSkills.length === 0) return;
298
+ const formatted = formatSkillsForInjection(allLoadedSkills);
198
299
  output.context.push(
199
300
  `## Preloaded Skills
200
301
 
201
- The following skills were auto-loaded at session start and should persist:
302
+ The following skills were loaded during this session and should persist:
202
303
 
203
- ${formattedContent}`
304
+ ${formatted}`
204
305
  );
205
- injectedSessions.delete(input.sessionID);
206
- log("info", "Added preloaded skills to compaction context", {
306
+ state.initialSkillsInjected = false;
307
+ log("info", "Added all loaded skills to compaction context", {
207
308
  sessionID: input.sessionID,
208
- skillCount: loadedSkills.length
309
+ skillCount: allLoadedSkills.length
209
310
  });
210
311
  },
211
312
  event: async ({ event }) => {
212
313
  if (event.type === "session.deleted" && "sessionID" in event.properties) {
213
314
  const sessionID = event.properties.sessionID;
214
- injectedSessions.delete(sessionID);
215
- log("debug", "Cleaned up session tracking", { sessionID });
315
+ sessionStates.delete(sessionID);
316
+ pendingSkillInjections.delete(sessionID);
317
+ log("debug", "Cleaned up session state", { sessionID });
216
318
  }
217
319
  }
218
320
  };
@@ -1 +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 if (!Array.isArray(skillNames)) {\n return []\n }\n\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 (!Array.isArray(skills) || 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 { existsSync, readFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport type { Plugin, PluginInput } from \"@opencode-ai/plugin\"\nimport type { Event, UserMessage, Part } 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 CONFIG_FILENAME = \"preload-skills.json\"\n\nconst DEFAULT_CONFIG: PreloadSkillsConfig = {\n skills: [],\n persistAfterCompaction: true,\n debug: false,\n}\n\nfunction findConfigFile(projectDir: string): string | null {\n const locations = [\n join(projectDir, \".opencode\", CONFIG_FILENAME),\n join(projectDir, CONFIG_FILENAME),\n join(homedir(), \".config\", \"opencode\", CONFIG_FILENAME),\n ]\n\n for (const path of locations) {\n if (existsSync(path)) {\n return path\n }\n }\n return null\n}\n\nfunction loadConfigFile(projectDir: string): Partial<PreloadSkillsConfig> {\n const configPath = findConfigFile(projectDir)\n if (!configPath) {\n return {}\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\")\n const parsed = JSON.parse(content) as Record<string, unknown>\n \n return {\n skills: Array.isArray(parsed.skills) ? parsed.skills : [],\n persistAfterCompaction: typeof parsed.persistAfterCompaction === \"boolean\" \n ? parsed.persistAfterCompaction \n : undefined,\n debug: typeof parsed.debug === \"boolean\" ? parsed.debug : undefined,\n }\n } catch {\n return {}\n }\n}\n\nexport const PreloadSkillsPlugin: Plugin = async (ctx: PluginInput) => {\n const injectedSessions = new Set<string>()\n\n const fileConfig = loadConfigFile(ctx.directory)\n const config: PreloadSkillsConfig = {\n ...DEFAULT_CONFIG,\n ...fileConfig,\n }\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 let loadedSkills: ParsedSkill[] = []\n let formattedContent = \"\"\n\n if (config.skills.length === 0) {\n log(\"warn\", \"No skills configured for preloading. Create .opencode/preload-skills.json\")\n } else {\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 return {\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 (!input.sessionID) {\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 firstTextPart = output.parts.find((p) => p.type === \"text\")\n if (firstTextPart && \"text\" in firstTextPart) {\n firstTextPart.text = `${formattedContent}\\n\\n---\\n\\n${firstTextPart.text}`\n }\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"]}
1
+ {"version":3,"sources":["../src/skill-loader.ts","../src/index.ts"],"names":["join","homedir","existsSync","readFileSync","extname"],"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 if (!Array.isArray(skillNames)) {\n return []\n }\n\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 (!Array.isArray(skills) || 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 { existsSync, readFileSync } from \"node:fs\"\nimport { join, extname } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport type { Plugin, PluginInput } from \"@opencode-ai/plugin\"\nimport type { Event, UserMessage, Part } from \"@opencode-ai/sdk\"\nimport type { PreloadSkillsConfig, ParsedSkill, SessionState } from \"./types.js\"\nimport { loadSkills, loadSkill, formatSkillsForInjection } from \"./skill-loader.js\"\n\nexport type { PreloadSkillsConfig, ParsedSkill }\nexport { loadSkills, formatSkillsForInjection }\n\nconst CONFIG_FILENAME = \"preload-skills.json\"\n\nconst FILE_TOOLS = [\"read\", \"edit\", \"write\", \"glob\", \"grep\"]\n\nconst DEFAULT_CONFIG: PreloadSkillsConfig = {\n skills: [],\n fileTypeSkills: {},\n persistAfterCompaction: true,\n debug: false,\n}\n\nfunction findConfigFile(projectDir: string): string | null {\n const locations = [\n join(projectDir, \".opencode\", CONFIG_FILENAME),\n join(projectDir, CONFIG_FILENAME),\n join(homedir(), \".config\", \"opencode\", CONFIG_FILENAME),\n ]\n\n for (const path of locations) {\n if (existsSync(path)) {\n return path\n }\n }\n return null\n}\n\nfunction parseFileTypeSkills(raw: unknown): Record<string, string[]> {\n if (!raw || typeof raw !== \"object\") return {}\n \n const result: Record<string, string[]> = {}\n for (const [key, value] of Object.entries(raw)) {\n if (Array.isArray(value)) {\n result[key] = value.filter((v) => typeof v === \"string\")\n }\n }\n return result\n}\n\nfunction loadConfigFile(projectDir: string): Partial<PreloadSkillsConfig> {\n const configPath = findConfigFile(projectDir)\n if (!configPath) {\n return {}\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\")\n const parsed = JSON.parse(content) as Record<string, unknown>\n \n return {\n skills: Array.isArray(parsed.skills) ? parsed.skills : [],\n fileTypeSkills: parseFileTypeSkills(parsed.fileTypeSkills),\n persistAfterCompaction: typeof parsed.persistAfterCompaction === \"boolean\" \n ? parsed.persistAfterCompaction \n : undefined,\n debug: typeof parsed.debug === \"boolean\" ? parsed.debug : undefined,\n }\n } catch {\n return {}\n }\n}\n\nfunction getFilePathFromArgs(args: Record<string, unknown>): string | null {\n if (typeof args.filePath === \"string\") return args.filePath\n if (typeof args.path === \"string\") return args.path\n if (typeof args.file === \"string\") return args.file\n return null\n}\n\nfunction getSkillsForExtension(\n ext: string,\n fileTypeSkills: Record<string, string[]>\n): string[] {\n const skills: string[] = []\n \n for (const [pattern, skillNames] of Object.entries(fileTypeSkills)) {\n const extensions = pattern.split(\",\").map((e) => e.trim().toLowerCase())\n if (extensions.includes(ext.toLowerCase())) {\n skills.push(...skillNames)\n }\n }\n \n return [...new Set(skills)]\n}\n\nexport const PreloadSkillsPlugin: Plugin = async (ctx: PluginInput) => {\n const sessionStates = new Map<string, SessionState>()\n\n const fileConfig = loadConfigFile(ctx.directory)\n const config: PreloadSkillsConfig = {\n ...DEFAULT_CONFIG,\n ...fileConfig,\n }\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 const skillCache = new Map<string, ParsedSkill>()\n\n const getOrLoadSkill = (skillName: string): ParsedSkill | null => {\n if (skillCache.has(skillName)) {\n return skillCache.get(skillName) ?? null\n }\n const skill = loadSkill(skillName, ctx.directory)\n if (skill) {\n skillCache.set(skillName, skill)\n }\n return skill\n }\n\n let initialSkills: ParsedSkill[] = []\n let initialFormattedContent = \"\"\n\n if (config.skills.length > 0) {\n initialSkills = loadSkills(config.skills, ctx.directory)\n initialFormattedContent = formatSkillsForInjection(initialSkills)\n\n for (const skill of initialSkills) {\n skillCache.set(skill.name, skill)\n }\n\n const loadedNames = initialSkills.map((s) => s.name)\n const missingNames = config.skills.filter((s) => !loadedNames.includes(s))\n\n log(\"info\", `Loaded ${initialSkills.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 const hasFileTypeSkills = Object.keys(config.fileTypeSkills ?? {}).length > 0\n\n if (config.skills.length === 0 && !hasFileTypeSkills) {\n log(\"warn\", \"No skills configured. Create .opencode/preload-skills.json\")\n }\n\n const getSessionState = (sessionID: string): SessionState => {\n if (!sessionStates.has(sessionID)) {\n sessionStates.set(sessionID, {\n initialSkillsInjected: false,\n loadedSkills: new Set(initialSkills.map((s) => s.name)),\n })\n }\n return sessionStates.get(sessionID)!\n }\n\n const pendingSkillInjections = new Map<string, ParsedSkill[]>()\n\n return {\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 (!input.sessionID) return\n\n const state = getSessionState(input.sessionID)\n const firstTextPart = output.parts.find((p) => p.type === \"text\")\n if (!firstTextPart || !(\"text\" in firstTextPart)) return\n\n const contentToInject: string[] = []\n\n if (!state.initialSkillsInjected && initialFormattedContent) {\n contentToInject.push(initialFormattedContent)\n state.initialSkillsInjected = true\n log(\"info\", \"Injected initial preloaded skills\", {\n sessionID: input.sessionID,\n skills: initialSkills.map((s) => s.name),\n })\n }\n\n const pending = pendingSkillInjections.get(input.sessionID)\n if (pending && pending.length > 0) {\n const formatted = formatSkillsForInjection(pending)\n if (formatted) {\n contentToInject.push(formatted)\n log(\"info\", \"Injected file-type triggered skills\", {\n sessionID: input.sessionID,\n skills: pending.map((s) => s.name),\n })\n }\n pendingSkillInjections.delete(input.sessionID)\n }\n\n if (contentToInject.length > 0) {\n firstTextPart.text = `${contentToInject.join(\"\\n\\n\")}\\n\\n---\\n\\n${firstTextPart.text}`\n }\n },\n\n \"tool.execute.after\": async (\n input: {\n tool: string\n sessionID: string\n callID: string\n },\n _output: {\n title: string\n output: string\n metadata: unknown\n }\n ): Promise<void> => {\n if (!hasFileTypeSkills) return\n if (!FILE_TOOLS.includes(input.tool)) return\n if (!input.sessionID) return\n\n const state = getSessionState(input.sessionID)\n\n const toolArgs = (_output.metadata as { args?: Record<string, unknown> })?.args\n if (!toolArgs) return\n\n const filePath = getFilePathFromArgs(toolArgs)\n if (!filePath) return\n\n const ext = extname(filePath)\n if (!ext) return\n\n const skillNames = getSkillsForExtension(ext, config.fileTypeSkills ?? {})\n if (skillNames.length === 0) return\n\n const newSkills: ParsedSkill[] = []\n for (const name of skillNames) {\n if (state.loadedSkills.has(name)) continue\n\n const skill = getOrLoadSkill(name)\n if (skill) {\n newSkills.push(skill)\n state.loadedSkills.add(name)\n }\n }\n\n if (newSkills.length > 0) {\n const existing = pendingSkillInjections.get(input.sessionID) ?? []\n pendingSkillInjections.set(input.sessionID, [...existing, ...newSkills])\n\n log(\"debug\", \"Queued file-type skills for injection\", {\n sessionID: input.sessionID,\n filePath,\n extension: ext,\n skills: newSkills.map((s) => s.name),\n })\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) return\n\n const state = sessionStates.get(input.sessionID)\n if (!state || state.loadedSkills.size === 0) return\n\n const allLoadedSkills: ParsedSkill[] = []\n for (const name of state.loadedSkills) {\n const skill = skillCache.get(name)\n if (skill) allLoadedSkills.push(skill)\n }\n\n if (allLoadedSkills.length === 0) return\n\n const formatted = formatSkillsForInjection(allLoadedSkills)\n output.context.push(\n `## Preloaded Skills\\n\\nThe following skills were loaded during this session and should persist:\\n\\n${formatted}`\n )\n\n state.initialSkillsInjected = false\n\n log(\"info\", \"Added all loaded skills to compaction context\", {\n sessionID: input.sessionID,\n skillCount: allLoadedSkills.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 sessionStates.delete(sessionID)\n pendingSkillInjections.delete(sessionID)\n log(\"debug\", \"Cleaned up session state\", { sessionID })\n }\n },\n }\n}\n\nexport default PreloadSkillsPlugin\n"]}
package/dist/index.d.cts CHANGED
@@ -2,6 +2,7 @@ import { Plugin } from '@opencode-ai/plugin';
2
2
 
3
3
  interface PreloadSkillsConfig {
4
4
  skills: string[];
5
+ fileTypeSkills?: Record<string, string[]>;
5
6
  persistAfterCompaction?: boolean;
6
7
  debug?: boolean;
7
8
  }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Plugin } from '@opencode-ai/plugin';
2
2
 
3
3
  interface PreloadSkillsConfig {
4
4
  skills: string[];
5
+ fileTypeSkills?: Record<string, string[]>;
5
6
  persistAfterCompaction?: boolean;
6
7
  debug?: boolean;
7
8
  }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
- import { join } from 'path';
2
+ import { extname, join } from 'path';
3
3
  import { homedir } from 'os';
4
4
 
5
5
  // src/index.ts
@@ -86,8 +86,10 @@ ${parts.join("\n\n")}
86
86
 
87
87
  // src/index.ts
88
88
  var CONFIG_FILENAME = "preload-skills.json";
89
+ var FILE_TOOLS = ["read", "edit", "write", "glob", "grep"];
89
90
  var DEFAULT_CONFIG = {
90
91
  skills: [],
92
+ fileTypeSkills: {},
91
93
  persistAfterCompaction: true,
92
94
  debug: false
93
95
  };
@@ -104,6 +106,16 @@ function findConfigFile(projectDir) {
104
106
  }
105
107
  return null;
106
108
  }
109
+ function parseFileTypeSkills(raw) {
110
+ if (!raw || typeof raw !== "object") return {};
111
+ const result = {};
112
+ for (const [key, value] of Object.entries(raw)) {
113
+ if (Array.isArray(value)) {
114
+ result[key] = value.filter((v) => typeof v === "string");
115
+ }
116
+ }
117
+ return result;
118
+ }
107
119
  function loadConfigFile(projectDir) {
108
120
  const configPath = findConfigFile(projectDir);
109
121
  if (!configPath) {
@@ -114,6 +126,7 @@ function loadConfigFile(projectDir) {
114
126
  const parsed = JSON.parse(content);
115
127
  return {
116
128
  skills: Array.isArray(parsed.skills) ? parsed.skills : [],
129
+ fileTypeSkills: parseFileTypeSkills(parsed.fileTypeSkills),
117
130
  persistAfterCompaction: typeof parsed.persistAfterCompaction === "boolean" ? parsed.persistAfterCompaction : void 0,
118
131
  debug: typeof parsed.debug === "boolean" ? parsed.debug : void 0
119
132
  };
@@ -121,8 +134,24 @@ function loadConfigFile(projectDir) {
121
134
  return {};
122
135
  }
123
136
  }
137
+ function getFilePathFromArgs(args) {
138
+ if (typeof args.filePath === "string") return args.filePath;
139
+ if (typeof args.path === "string") return args.path;
140
+ if (typeof args.file === "string") return args.file;
141
+ return null;
142
+ }
143
+ function getSkillsForExtension(ext, fileTypeSkills) {
144
+ const skills = [];
145
+ for (const [pattern, skillNames] of Object.entries(fileTypeSkills)) {
146
+ const extensions = pattern.split(",").map((e) => e.trim().toLowerCase());
147
+ if (extensions.includes(ext.toLowerCase())) {
148
+ skills.push(...skillNames);
149
+ }
150
+ }
151
+ return [...new Set(skills)];
152
+ }
124
153
  var PreloadSkillsPlugin = async (ctx) => {
125
- const injectedSessions = /* @__PURE__ */ new Set();
154
+ const sessionStates = /* @__PURE__ */ new Map();
126
155
  const fileConfig = loadConfigFile(ctx.directory);
127
156
  const config = {
128
157
  ...DEFAULT_CONFIG,
@@ -139,16 +168,28 @@ var PreloadSkillsPlugin = async (ctx) => {
139
168
  }
140
169
  });
141
170
  };
142
- let loadedSkills = [];
143
- let formattedContent = "";
144
- if (config.skills.length === 0) {
145
- log("warn", "No skills configured for preloading. Create .opencode/preload-skills.json");
146
- } else {
147
- loadedSkills = loadSkills(config.skills, ctx.directory);
148
- formattedContent = formatSkillsForInjection(loadedSkills);
149
- const loadedNames = loadedSkills.map((s) => s.name);
171
+ const skillCache = /* @__PURE__ */ new Map();
172
+ const getOrLoadSkill = (skillName) => {
173
+ if (skillCache.has(skillName)) {
174
+ return skillCache.get(skillName) ?? null;
175
+ }
176
+ const skill = loadSkill(skillName, ctx.directory);
177
+ if (skill) {
178
+ skillCache.set(skillName, skill);
179
+ }
180
+ return skill;
181
+ };
182
+ let initialSkills = [];
183
+ let initialFormattedContent = "";
184
+ if (config.skills.length > 0) {
185
+ initialSkills = loadSkills(config.skills, ctx.directory);
186
+ initialFormattedContent = formatSkillsForInjection(initialSkills);
187
+ for (const skill of initialSkills) {
188
+ skillCache.set(skill.name, skill);
189
+ }
190
+ const loadedNames = initialSkills.map((s) => s.name);
150
191
  const missingNames = config.skills.filter((s) => !loadedNames.includes(s));
151
- log("info", `Loaded ${loadedSkills.length} skills for preloading`, {
192
+ log("info", `Loaded ${initialSkills.length} skills for preloading`, {
152
193
  loaded: loadedNames,
153
194
  missing: missingNames.length > 0 ? missingNames : void 0
154
195
  });
@@ -158,57 +199,118 @@ var PreloadSkillsPlugin = async (ctx) => {
158
199
  });
159
200
  }
160
201
  }
202
+ const hasFileTypeSkills = Object.keys(config.fileTypeSkills ?? {}).length > 0;
203
+ if (config.skills.length === 0 && !hasFileTypeSkills) {
204
+ log("warn", "No skills configured. Create .opencode/preload-skills.json");
205
+ }
206
+ const getSessionState = (sessionID) => {
207
+ if (!sessionStates.has(sessionID)) {
208
+ sessionStates.set(sessionID, {
209
+ initialSkillsInjected: false,
210
+ loadedSkills: new Set(initialSkills.map((s) => s.name))
211
+ });
212
+ }
213
+ return sessionStates.get(sessionID);
214
+ };
215
+ const pendingSkillInjections = /* @__PURE__ */ new Map();
161
216
  return {
162
217
  "chat.message": async (input, output) => {
163
- if (loadedSkills.length === 0 || !formattedContent) {
164
- return;
165
- }
166
- if (!input.sessionID) {
167
- return;
168
- }
169
- if (injectedSessions.has(input.sessionID)) {
170
- log("debug", "Skills already injected for session", {
171
- sessionID: input.sessionID
218
+ if (!input.sessionID) return;
219
+ const state = getSessionState(input.sessionID);
220
+ const firstTextPart = output.parts.find((p) => p.type === "text");
221
+ if (!firstTextPart || !("text" in firstTextPart)) return;
222
+ const contentToInject = [];
223
+ if (!state.initialSkillsInjected && initialFormattedContent) {
224
+ contentToInject.push(initialFormattedContent);
225
+ state.initialSkillsInjected = true;
226
+ log("info", "Injected initial preloaded skills", {
227
+ sessionID: input.sessionID,
228
+ skills: initialSkills.map((s) => s.name)
172
229
  });
173
- return;
174
230
  }
175
- injectedSessions.add(input.sessionID);
176
- const firstTextPart = output.parts.find((p) => p.type === "text");
177
- if (firstTextPart && "text" in firstTextPart) {
178
- firstTextPart.text = `${formattedContent}
231
+ const pending = pendingSkillInjections.get(input.sessionID);
232
+ if (pending && pending.length > 0) {
233
+ const formatted = formatSkillsForInjection(pending);
234
+ if (formatted) {
235
+ contentToInject.push(formatted);
236
+ log("info", "Injected file-type triggered skills", {
237
+ sessionID: input.sessionID,
238
+ skills: pending.map((s) => s.name)
239
+ });
240
+ }
241
+ pendingSkillInjections.delete(input.sessionID);
242
+ }
243
+ if (contentToInject.length > 0) {
244
+ firstTextPart.text = `${contentToInject.join("\n\n")}
179
245
 
180
246
  ---
181
247
 
182
248
  ${firstTextPart.text}`;
183
249
  }
184
- log("info", "Injected preloaded skills into session", {
185
- sessionID: input.sessionID,
186
- skillCount: loadedSkills.length,
187
- skills: loadedSkills.map((s) => s.name)
188
- });
250
+ },
251
+ "tool.execute.after": async (input, _output) => {
252
+ if (!hasFileTypeSkills) return;
253
+ if (!FILE_TOOLS.includes(input.tool)) return;
254
+ if (!input.sessionID) return;
255
+ const state = getSessionState(input.sessionID);
256
+ const toolArgs = _output.metadata?.args;
257
+ if (!toolArgs) return;
258
+ const filePath = getFilePathFromArgs(toolArgs);
259
+ if (!filePath) return;
260
+ const ext = extname(filePath);
261
+ if (!ext) return;
262
+ const skillNames = getSkillsForExtension(ext, config.fileTypeSkills ?? {});
263
+ if (skillNames.length === 0) return;
264
+ const newSkills = [];
265
+ for (const name of skillNames) {
266
+ if (state.loadedSkills.has(name)) continue;
267
+ const skill = getOrLoadSkill(name);
268
+ if (skill) {
269
+ newSkills.push(skill);
270
+ state.loadedSkills.add(name);
271
+ }
272
+ }
273
+ if (newSkills.length > 0) {
274
+ const existing = pendingSkillInjections.get(input.sessionID) ?? [];
275
+ pendingSkillInjections.set(input.sessionID, [...existing, ...newSkills]);
276
+ log("debug", "Queued file-type skills for injection", {
277
+ sessionID: input.sessionID,
278
+ filePath,
279
+ extension: ext,
280
+ skills: newSkills.map((s) => s.name)
281
+ });
282
+ }
189
283
  },
190
284
  "experimental.session.compacting": async (input, output) => {
191
- if (!config.persistAfterCompaction || loadedSkills.length === 0) {
192
- return;
285
+ if (!config.persistAfterCompaction) return;
286
+ const state = sessionStates.get(input.sessionID);
287
+ if (!state || state.loadedSkills.size === 0) return;
288
+ const allLoadedSkills = [];
289
+ for (const name of state.loadedSkills) {
290
+ const skill = skillCache.get(name);
291
+ if (skill) allLoadedSkills.push(skill);
193
292
  }
293
+ if (allLoadedSkills.length === 0) return;
294
+ const formatted = formatSkillsForInjection(allLoadedSkills);
194
295
  output.context.push(
195
296
  `## Preloaded Skills
196
297
 
197
- The following skills were auto-loaded at session start and should persist:
298
+ The following skills were loaded during this session and should persist:
198
299
 
199
- ${formattedContent}`
300
+ ${formatted}`
200
301
  );
201
- injectedSessions.delete(input.sessionID);
202
- log("info", "Added preloaded skills to compaction context", {
302
+ state.initialSkillsInjected = false;
303
+ log("info", "Added all loaded skills to compaction context", {
203
304
  sessionID: input.sessionID,
204
- skillCount: loadedSkills.length
305
+ skillCount: allLoadedSkills.length
205
306
  });
206
307
  },
207
308
  event: async ({ event }) => {
208
309
  if (event.type === "session.deleted" && "sessionID" in event.properties) {
209
310
  const sessionID = event.properties.sessionID;
210
- injectedSessions.delete(sessionID);
211
- log("debug", "Cleaned up session tracking", { sessionID });
311
+ sessionStates.delete(sessionID);
312
+ pendingSkillInjections.delete(sessionID);
313
+ log("debug", "Cleaned up session state", { sessionID });
212
314
  }
213
315
  }
214
316
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/skill-loader.ts","../src/index.ts"],"names":["join","homedir","existsSync","readFileSync"],"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 if (!Array.isArray(skillNames)) {\n return []\n }\n\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 (!Array.isArray(skills) || 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 { existsSync, readFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport type { Plugin, PluginInput } from \"@opencode-ai/plugin\"\nimport type { Event, UserMessage, Part } 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 CONFIG_FILENAME = \"preload-skills.json\"\n\nconst DEFAULT_CONFIG: PreloadSkillsConfig = {\n skills: [],\n persistAfterCompaction: true,\n debug: false,\n}\n\nfunction findConfigFile(projectDir: string): string | null {\n const locations = [\n join(projectDir, \".opencode\", CONFIG_FILENAME),\n join(projectDir, CONFIG_FILENAME),\n join(homedir(), \".config\", \"opencode\", CONFIG_FILENAME),\n ]\n\n for (const path of locations) {\n if (existsSync(path)) {\n return path\n }\n }\n return null\n}\n\nfunction loadConfigFile(projectDir: string): Partial<PreloadSkillsConfig> {\n const configPath = findConfigFile(projectDir)\n if (!configPath) {\n return {}\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\")\n const parsed = JSON.parse(content) as Record<string, unknown>\n \n return {\n skills: Array.isArray(parsed.skills) ? parsed.skills : [],\n persistAfterCompaction: typeof parsed.persistAfterCompaction === \"boolean\" \n ? parsed.persistAfterCompaction \n : undefined,\n debug: typeof parsed.debug === \"boolean\" ? parsed.debug : undefined,\n }\n } catch {\n return {}\n }\n}\n\nexport const PreloadSkillsPlugin: Plugin = async (ctx: PluginInput) => {\n const injectedSessions = new Set<string>()\n\n const fileConfig = loadConfigFile(ctx.directory)\n const config: PreloadSkillsConfig = {\n ...DEFAULT_CONFIG,\n ...fileConfig,\n }\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 let loadedSkills: ParsedSkill[] = []\n let formattedContent = \"\"\n\n if (config.skills.length === 0) {\n log(\"warn\", \"No skills configured for preloading. Create .opencode/preload-skills.json\")\n } else {\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 return {\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 (!input.sessionID) {\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 firstTextPart = output.parts.find((p) => p.type === \"text\")\n if (firstTextPart && \"text\" in firstTextPart) {\n firstTextPart.text = `${formattedContent}\\n\\n---\\n\\n${firstTextPart.text}`\n }\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"]}
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,KAAgB,IAAA,CAAK,GAAA,EAAK,aAAa,QAAQ,CAAA;AAAA,EAChD,CAAC,GAAA,KAAgB,IAAA,CAAK,GAAA,EAAK,WAAW,QAAQ,CAAA;AAAA,EAC9C,MAAM,IAAA,CAAK,OAAA,EAAQ,EAAG,SAAA,EAAW,YAAY,QAAQ,CAAA;AAAA,EACrD,MAAM,IAAA,CAAK,OAAA,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,GAAY,IAAA,CAAK,QAAA,EAAU,SAAA,EAAW,cAAc,CAAA;AAE1D,IAAA,IAAI,UAAA,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,GAAU,YAAA,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,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC9B,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,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,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,IAAK,MAAA,CAAO,WAAW,CAAA,EAAG;AACjD,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;;;AC3FA,IAAM,eAAA,GAAkB,qBAAA;AAExB,IAAM,aAAa,CAAC,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,QAAQ,MAAM,CAAA;AAE3D,IAAM,cAAA,GAAsC;AAAA,EAC1C,QAAQ,EAAC;AAAA,EACT,gBAAgB,EAAC;AAAA,EACjB,sBAAA,EAAwB,IAAA;AAAA,EACxB,KAAA,EAAO;AACT,CAAA;AAEA,SAAS,eAAe,UAAA,EAAmC;AACzD,EAAA,MAAM,SAAA,GAAY;AAAA,IAChBA,IAAAA,CAAK,UAAA,EAAY,WAAA,EAAa,eAAe,CAAA;AAAA,IAC7CA,IAAAA,CAAK,YAAY,eAAe,CAAA;AAAA,IAChCA,IAAAA,CAAKC,OAAAA,EAAQ,EAAG,SAAA,EAAW,YAAY,eAAe;AAAA,GACxD;AAEA,EAAA,KAAA,MAAW,QAAQ,SAAA,EAAW;AAC5B,IAAA,IAAIC,UAAAA,CAAW,IAAI,CAAA,EAAG;AACpB,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,oBAAoB,GAAA,EAAwC;AACnE,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,SAAiB,EAAC;AAE7C,EAAA,MAAM,SAAmC,EAAC;AAC1C,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,EAAG;AAC9C,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,MAAA,CAAO,GAAG,IAAI,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM,OAAO,MAAM,QAAQ,CAAA;AAAA,IACzD;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,eAAe,UAAA,EAAkD;AACxE,EAAA,MAAM,UAAA,GAAa,eAAe,UAAU,CAAA;AAC5C,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAUC,YAAAA,CAAa,UAAA,EAAY,OAAO,CAAA;AAChD,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAEjC,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,MAAM,OAAA,CAAQ,MAAA,CAAO,MAAM,CAAA,GAAI,MAAA,CAAO,SAAS,EAAC;AAAA,MACxD,cAAA,EAAgB,mBAAA,CAAoB,MAAA,CAAO,cAAc,CAAA;AAAA,MACzD,wBAAwB,OAAO,MAAA,CAAO,sBAAA,KAA2B,SAAA,GAC7D,OAAO,sBAAA,GACP,KAAA,CAAA;AAAA,MACJ,OAAO,OAAO,MAAA,CAAO,KAAA,KAAU,SAAA,GAAY,OAAO,KAAA,GAAQ,KAAA;AAAA,KAC5D;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAEA,SAAS,oBAAoB,IAAA,EAA8C;AACzE,EAAA,IAAI,OAAO,IAAA,CAAK,QAAA,KAAa,QAAA,SAAiB,IAAA,CAAK,QAAA;AACnD,EAAA,IAAI,OAAO,IAAA,CAAK,IAAA,KAAS,QAAA,SAAiB,IAAA,CAAK,IAAA;AAC/C,EAAA,IAAI,OAAO,IAAA,CAAK,IAAA,KAAS,QAAA,SAAiB,IAAA,CAAK,IAAA;AAC/C,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,qBAAA,CACP,KACA,cAAA,EACU;AACV,EAAA,MAAM,SAAmB,EAAC;AAE1B,EAAA,KAAA,MAAW,CAAC,OAAA,EAAS,UAAU,KAAK,MAAA,CAAO,OAAA,CAAQ,cAAc,CAAA,EAAG;AAClE,IAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAK,CAAE,WAAA,EAAa,CAAA;AACvE,IAAA,IAAI,UAAA,CAAW,QAAA,CAAS,GAAA,CAAI,WAAA,EAAa,CAAA,EAAG;AAC1C,MAAA,MAAA,CAAO,IAAA,CAAK,GAAG,UAAU,CAAA;AAAA,IAC3B;AAAA,EACF;AAEA,EAAA,OAAO,CAAC,GAAG,IAAI,GAAA,CAAI,MAAM,CAAC,CAAA;AAC5B;AAEO,IAAM,mBAAA,GAA8B,OAAO,GAAA,KAAqB;AACrE,EAAA,MAAM,aAAA,uBAAoB,GAAA,EAA0B;AAEpD,EAAA,MAAM,UAAA,GAAa,cAAA,CAAe,GAAA,CAAI,SAAS,CAAA;AAC/C,EAAA,MAAM,MAAA,GAA8B;AAAA,IAClC,GAAG,cAAA;AAAA,IACH,GAAG;AAAA,GACL;AAEA,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,MAAM,UAAA,uBAAiB,GAAA,EAAyB;AAEhD,EAAA,MAAM,cAAA,GAAiB,CAAC,SAAA,KAA0C;AAChE,IAAA,IAAI,UAAA,CAAW,GAAA,CAAI,SAAS,CAAA,EAAG;AAC7B,MAAA,OAAO,UAAA,CAAW,GAAA,CAAI,SAAS,CAAA,IAAK,IAAA;AAAA,IACtC;AACA,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,SAAA,EAAW,GAAA,CAAI,SAAS,CAAA;AAChD,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,UAAA,CAAW,GAAA,CAAI,WAAW,KAAK,CAAA;AAAA,IACjC;AACA,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAEA,EAAA,IAAI,gBAA+B,EAAC;AACpC,EAAA,IAAI,uBAAA,GAA0B,EAAA;AAE9B,EAAA,IAAI,MAAA,CAAO,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AAC5B,IAAA,aAAA,GAAgB,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,GAAA,CAAI,SAAS,CAAA;AACvD,IAAA,uBAAA,GAA0B,yBAAyB,aAAa,CAAA;AAEhE,IAAA,KAAA,MAAW,SAAS,aAAA,EAAe;AACjC,MAAA,UAAA,CAAW,GAAA,CAAI,KAAA,CAAM,IAAA,EAAM,KAAK,CAAA;AAAA,IAClC;AAEA,IAAA,MAAM,cAAc,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AACnD,IAAA,MAAM,YAAA,GAAe,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,CAAC,MAAM,CAAC,WAAA,CAAY,QAAA,CAAS,CAAC,CAAC,CAAA;AAEzE,IAAA,GAAA,CAAI,MAAA,EAAQ,CAAA,OAAA,EAAU,aAAA,CAAc,MAAM,CAAA,sBAAA,CAAA,EAA0B;AAAA,MAClE,MAAA,EAAQ,WAAA;AAAA,MACR,OAAA,EAAS,YAAA,CAAa,MAAA,GAAS,CAAA,GAAI,YAAA,GAAe;AAAA,KACnD,CAAA;AAED,IAAA,IAAI,YAAA,CAAa,SAAS,CAAA,EAAG;AAC3B,MAAA,GAAA,CAAI,QAAQ,uCAAA,EAAyC;AAAA,QACnD,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,iBAAA,GAAoB,OAAO,IAAA,CAAK,MAAA,CAAO,kBAAkB,EAAE,EAAE,MAAA,GAAS,CAAA;AAE5E,EAAA,IAAI,MAAA,CAAO,MAAA,CAAO,MAAA,KAAW,CAAA,IAAK,CAAC,iBAAA,EAAmB;AACpD,IAAA,GAAA,CAAI,QAAQ,4DAA4D,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,eAAA,GAAkB,CAAC,SAAA,KAAoC;AAC3D,IAAA,IAAI,CAAC,aAAA,CAAc,GAAA,CAAI,SAAS,CAAA,EAAG;AACjC,MAAA,aAAA,CAAc,IAAI,SAAA,EAAW;AAAA,QAC3B,qBAAA,EAAuB,KAAA;AAAA,QACvB,YAAA,EAAc,IAAI,GAAA,CAAI,aAAA,CAAc,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC;AAAA,OACvD,CAAA;AAAA,IACH;AACA,IAAA,OAAO,aAAA,CAAc,IAAI,SAAS,CAAA;AAAA,EACpC,CAAA;AAEA,EAAA,MAAM,sBAAA,uBAA6B,GAAA,EAA2B;AAE9D,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB,OACd,KAAA,EAOA,MAAA,KACkB;AAClB,MAAA,IAAI,CAAC,MAAM,SAAA,EAAW;AAEtB,MAAA,MAAM,KAAA,GAAQ,eAAA,CAAgB,KAAA,CAAM,SAAS,CAAA;AAC7C,MAAA,MAAM,aAAA,GAAgB,OAAO,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,MAAM,CAAA;AAChE,MAAA,IAAI,CAAC,aAAA,IAAiB,EAAE,MAAA,IAAU,aAAA,CAAA,EAAgB;AAElD,MAAA,MAAM,kBAA4B,EAAC;AAEnC,MAAA,IAAI,CAAC,KAAA,CAAM,qBAAA,IAAyB,uBAAA,EAAyB;AAC3D,QAAA,eAAA,CAAgB,KAAK,uBAAuB,CAAA;AAC5C,QAAA,KAAA,CAAM,qBAAA,GAAwB,IAAA;AAC9B,QAAA,GAAA,CAAI,QAAQ,mCAAA,EAAqC;AAAA,UAC/C,WAAW,KAAA,CAAM,SAAA;AAAA,UACjB,QAAQ,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI;AAAA,SACxC,CAAA;AAAA,MACH;AAEA,MAAA,MAAM,OAAA,GAAU,sBAAA,CAAuB,GAAA,CAAI,KAAA,CAAM,SAAS,CAAA;AAC1D,MAAA,IAAI,OAAA,IAAW,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG;AACjC,QAAA,MAAM,SAAA,GAAY,yBAAyB,OAAO,CAAA;AAClD,QAAA,IAAI,SAAA,EAAW;AACb,UAAA,eAAA,CAAgB,KAAK,SAAS,CAAA;AAC9B,UAAA,GAAA,CAAI,QAAQ,qCAAA,EAAuC;AAAA,YACjD,WAAW,KAAA,CAAM,SAAA;AAAA,YACjB,QAAQ,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI;AAAA,WAClC,CAAA;AAAA,QACH;AACA,QAAA,sBAAA,CAAuB,MAAA,CAAO,MAAM,SAAS,CAAA;AAAA,MAC/C;AAEA,MAAA,IAAI,eAAA,CAAgB,SAAS,CAAA,EAAG;AAC9B,QAAA,aAAA,CAAc,IAAA,GAAO,CAAA,EAAG,eAAA,CAAgB,IAAA,CAAK,MAAM,CAAC;;AAAA;;AAAA,EAAc,cAAc,IAAI,CAAA,CAAA;AAAA,MACtF;AAAA,IACF,CAAA;AAAA,IAEA,oBAAA,EAAsB,OACpB,KAAA,EAKA,OAAA,KAKkB;AAClB,MAAA,IAAI,CAAC,iBAAA,EAAmB;AACxB,MAAA,IAAI,CAAC,UAAA,CAAW,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,EAAG;AACtC,MAAA,IAAI,CAAC,MAAM,SAAA,EAAW;AAEtB,MAAA,MAAM,KAAA,GAAQ,eAAA,CAAgB,KAAA,CAAM,SAAS,CAAA;AAE7C,MAAA,MAAM,QAAA,GAAY,QAAQ,QAAA,EAAiD,IAAA;AAC3E,MAAA,IAAI,CAAC,QAAA,EAAU;AAEf,MAAA,MAAM,QAAA,GAAW,oBAAoB,QAAQ,CAAA;AAC7C,MAAA,IAAI,CAAC,QAAA,EAAU;AAEf,MAAA,MAAM,GAAA,GAAM,QAAQ,QAAQ,CAAA;AAC5B,MAAA,IAAI,CAAC,GAAA,EAAK;AAEV,MAAA,MAAM,aAAa,qBAAA,CAAsB,GAAA,EAAK,MAAA,CAAO,cAAA,IAAkB,EAAE,CAAA;AACzE,MAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAE7B,MAAA,MAAM,YAA2B,EAAC;AAClC,MAAA,KAAA,MAAW,QAAQ,UAAA,EAAY;AAC7B,QAAA,IAAI,KAAA,CAAM,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,EAAG;AAElC,QAAA,MAAM,KAAA,GAAQ,eAAe,IAAI,CAAA;AACjC,QAAA,IAAI,KAAA,EAAO;AACT,UAAA,SAAA,CAAU,KAAK,KAAK,CAAA;AACpB,UAAA,KAAA,CAAM,YAAA,CAAa,IAAI,IAAI,CAAA;AAAA,QAC7B;AAAA,MACF;AAEA,MAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG;AACxB,QAAA,MAAM,WAAW,sBAAA,CAAuB,GAAA,CAAI,KAAA,CAAM,SAAS,KAAK,EAAC;AACjE,QAAA,sBAAA,CAAuB,GAAA,CAAI,MAAM,SAAA,EAAW,CAAC,GAAG,QAAA,EAAU,GAAG,SAAS,CAAC,CAAA;AAEvE,QAAA,GAAA,CAAI,SAAS,uCAAA,EAAyC;AAAA,UACpD,WAAW,KAAA,CAAM,SAAA;AAAA,UACjB,QAAA;AAAA,UACA,SAAA,EAAW,GAAA;AAAA,UACX,QAAQ,SAAA,CAAU,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI;AAAA,SACpC,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IAEA,iCAAA,EAAmC,OACjC,KAAA,EACA,MAAA,KACkB;AAClB,MAAA,IAAI,CAAC,OAAO,sBAAA,EAAwB;AAEpC,MAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,GAAA,CAAI,KAAA,CAAM,SAAS,CAAA;AAC/C,MAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,YAAA,CAAa,SAAS,CAAA,EAAG;AAE7C,MAAA,MAAM,kBAAiC,EAAC;AACxC,MAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,YAAA,EAAc;AACrC,QAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,GAAA,CAAI,IAAI,CAAA;AACjC,QAAA,IAAI,KAAA,EAAO,eAAA,CAAgB,IAAA,CAAK,KAAK,CAAA;AAAA,MACvC;AAEA,MAAA,IAAI,eAAA,CAAgB,WAAW,CAAA,EAAG;AAElC,MAAA,MAAM,SAAA,GAAY,yBAAyB,eAAe,CAAA;AAC1D,MAAA,MAAA,CAAO,OAAA,CAAQ,IAAA;AAAA,QACb,CAAA;;AAAA;;AAAA,EAAsG,SAAS,CAAA;AAAA,OACjH;AAEA,MAAA,KAAA,CAAM,qBAAA,GAAwB,KAAA;AAE9B,MAAA,GAAA,CAAI,QAAQ,+CAAA,EAAiD;AAAA,QAC3D,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,eAAA,CAAgB;AAAA,OAC7B,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,aAAA,CAAc,OAAO,SAAS,CAAA;AAC9B,QAAA,sBAAA,CAAuB,OAAO,SAAS,CAAA;AACvC,QAAA,GAAA,CAAI,OAAA,EAAS,0BAAA,EAA4B,EAAE,SAAA,EAAW,CAAA;AAAA,MACxD;AAAA,IACF;AAAA,GACF;AACF;AAEA,IAAO,aAAA,GAAQ","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 if (!Array.isArray(skillNames)) {\n return []\n }\n\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 (!Array.isArray(skills) || 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 { existsSync, readFileSync } from \"node:fs\"\nimport { join, extname } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport type { Plugin, PluginInput } from \"@opencode-ai/plugin\"\nimport type { Event, UserMessage, Part } from \"@opencode-ai/sdk\"\nimport type { PreloadSkillsConfig, ParsedSkill, SessionState } from \"./types.js\"\nimport { loadSkills, loadSkill, formatSkillsForInjection } from \"./skill-loader.js\"\n\nexport type { PreloadSkillsConfig, ParsedSkill }\nexport { loadSkills, formatSkillsForInjection }\n\nconst CONFIG_FILENAME = \"preload-skills.json\"\n\nconst FILE_TOOLS = [\"read\", \"edit\", \"write\", \"glob\", \"grep\"]\n\nconst DEFAULT_CONFIG: PreloadSkillsConfig = {\n skills: [],\n fileTypeSkills: {},\n persistAfterCompaction: true,\n debug: false,\n}\n\nfunction findConfigFile(projectDir: string): string | null {\n const locations = [\n join(projectDir, \".opencode\", CONFIG_FILENAME),\n join(projectDir, CONFIG_FILENAME),\n join(homedir(), \".config\", \"opencode\", CONFIG_FILENAME),\n ]\n\n for (const path of locations) {\n if (existsSync(path)) {\n return path\n }\n }\n return null\n}\n\nfunction parseFileTypeSkills(raw: unknown): Record<string, string[]> {\n if (!raw || typeof raw !== \"object\") return {}\n \n const result: Record<string, string[]> = {}\n for (const [key, value] of Object.entries(raw)) {\n if (Array.isArray(value)) {\n result[key] = value.filter((v) => typeof v === \"string\")\n }\n }\n return result\n}\n\nfunction loadConfigFile(projectDir: string): Partial<PreloadSkillsConfig> {\n const configPath = findConfigFile(projectDir)\n if (!configPath) {\n return {}\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\")\n const parsed = JSON.parse(content) as Record<string, unknown>\n \n return {\n skills: Array.isArray(parsed.skills) ? parsed.skills : [],\n fileTypeSkills: parseFileTypeSkills(parsed.fileTypeSkills),\n persistAfterCompaction: typeof parsed.persistAfterCompaction === \"boolean\" \n ? parsed.persistAfterCompaction \n : undefined,\n debug: typeof parsed.debug === \"boolean\" ? parsed.debug : undefined,\n }\n } catch {\n return {}\n }\n}\n\nfunction getFilePathFromArgs(args: Record<string, unknown>): string | null {\n if (typeof args.filePath === \"string\") return args.filePath\n if (typeof args.path === \"string\") return args.path\n if (typeof args.file === \"string\") return args.file\n return null\n}\n\nfunction getSkillsForExtension(\n ext: string,\n fileTypeSkills: Record<string, string[]>\n): string[] {\n const skills: string[] = []\n \n for (const [pattern, skillNames] of Object.entries(fileTypeSkills)) {\n const extensions = pattern.split(\",\").map((e) => e.trim().toLowerCase())\n if (extensions.includes(ext.toLowerCase())) {\n skills.push(...skillNames)\n }\n }\n \n return [...new Set(skills)]\n}\n\nexport const PreloadSkillsPlugin: Plugin = async (ctx: PluginInput) => {\n const sessionStates = new Map<string, SessionState>()\n\n const fileConfig = loadConfigFile(ctx.directory)\n const config: PreloadSkillsConfig = {\n ...DEFAULT_CONFIG,\n ...fileConfig,\n }\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 const skillCache = new Map<string, ParsedSkill>()\n\n const getOrLoadSkill = (skillName: string): ParsedSkill | null => {\n if (skillCache.has(skillName)) {\n return skillCache.get(skillName) ?? null\n }\n const skill = loadSkill(skillName, ctx.directory)\n if (skill) {\n skillCache.set(skillName, skill)\n }\n return skill\n }\n\n let initialSkills: ParsedSkill[] = []\n let initialFormattedContent = \"\"\n\n if (config.skills.length > 0) {\n initialSkills = loadSkills(config.skills, ctx.directory)\n initialFormattedContent = formatSkillsForInjection(initialSkills)\n\n for (const skill of initialSkills) {\n skillCache.set(skill.name, skill)\n }\n\n const loadedNames = initialSkills.map((s) => s.name)\n const missingNames = config.skills.filter((s) => !loadedNames.includes(s))\n\n log(\"info\", `Loaded ${initialSkills.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 const hasFileTypeSkills = Object.keys(config.fileTypeSkills ?? {}).length > 0\n\n if (config.skills.length === 0 && !hasFileTypeSkills) {\n log(\"warn\", \"No skills configured. Create .opencode/preload-skills.json\")\n }\n\n const getSessionState = (sessionID: string): SessionState => {\n if (!sessionStates.has(sessionID)) {\n sessionStates.set(sessionID, {\n initialSkillsInjected: false,\n loadedSkills: new Set(initialSkills.map((s) => s.name)),\n })\n }\n return sessionStates.get(sessionID)!\n }\n\n const pendingSkillInjections = new Map<string, ParsedSkill[]>()\n\n return {\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 (!input.sessionID) return\n\n const state = getSessionState(input.sessionID)\n const firstTextPart = output.parts.find((p) => p.type === \"text\")\n if (!firstTextPart || !(\"text\" in firstTextPart)) return\n\n const contentToInject: string[] = []\n\n if (!state.initialSkillsInjected && initialFormattedContent) {\n contentToInject.push(initialFormattedContent)\n state.initialSkillsInjected = true\n log(\"info\", \"Injected initial preloaded skills\", {\n sessionID: input.sessionID,\n skills: initialSkills.map((s) => s.name),\n })\n }\n\n const pending = pendingSkillInjections.get(input.sessionID)\n if (pending && pending.length > 0) {\n const formatted = formatSkillsForInjection(pending)\n if (formatted) {\n contentToInject.push(formatted)\n log(\"info\", \"Injected file-type triggered skills\", {\n sessionID: input.sessionID,\n skills: pending.map((s) => s.name),\n })\n }\n pendingSkillInjections.delete(input.sessionID)\n }\n\n if (contentToInject.length > 0) {\n firstTextPart.text = `${contentToInject.join(\"\\n\\n\")}\\n\\n---\\n\\n${firstTextPart.text}`\n }\n },\n\n \"tool.execute.after\": async (\n input: {\n tool: string\n sessionID: string\n callID: string\n },\n _output: {\n title: string\n output: string\n metadata: unknown\n }\n ): Promise<void> => {\n if (!hasFileTypeSkills) return\n if (!FILE_TOOLS.includes(input.tool)) return\n if (!input.sessionID) return\n\n const state = getSessionState(input.sessionID)\n\n const toolArgs = (_output.metadata as { args?: Record<string, unknown> })?.args\n if (!toolArgs) return\n\n const filePath = getFilePathFromArgs(toolArgs)\n if (!filePath) return\n\n const ext = extname(filePath)\n if (!ext) return\n\n const skillNames = getSkillsForExtension(ext, config.fileTypeSkills ?? {})\n if (skillNames.length === 0) return\n\n const newSkills: ParsedSkill[] = []\n for (const name of skillNames) {\n if (state.loadedSkills.has(name)) continue\n\n const skill = getOrLoadSkill(name)\n if (skill) {\n newSkills.push(skill)\n state.loadedSkills.add(name)\n }\n }\n\n if (newSkills.length > 0) {\n const existing = pendingSkillInjections.get(input.sessionID) ?? []\n pendingSkillInjections.set(input.sessionID, [...existing, ...newSkills])\n\n log(\"debug\", \"Queued file-type skills for injection\", {\n sessionID: input.sessionID,\n filePath,\n extension: ext,\n skills: newSkills.map((s) => s.name),\n })\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) return\n\n const state = sessionStates.get(input.sessionID)\n if (!state || state.loadedSkills.size === 0) return\n\n const allLoadedSkills: ParsedSkill[] = []\n for (const name of state.loadedSkills) {\n const skill = skillCache.get(name)\n if (skill) allLoadedSkills.push(skill)\n }\n\n if (allLoadedSkills.length === 0) return\n\n const formatted = formatSkillsForInjection(allLoadedSkills)\n output.context.push(\n `## Preloaded Skills\\n\\nThe following skills were loaded during this session and should persist:\\n\\n${formatted}`\n )\n\n state.initialSkillsInjected = false\n\n log(\"info\", \"Added all loaded skills to compaction context\", {\n sessionID: input.sessionID,\n skillCount: allLoadedSkills.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 sessionStates.delete(sessionID)\n pendingSkillInjections.delete(sessionID)\n log(\"debug\", \"Cleaned up session state\", { sessionID })\n }\n },\n }\n}\n\nexport default PreloadSkillsPlugin\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-plugin-preload-skills",
3
- "version": "1.1.4",
4
- "description": "OpenCode plugin that auto-loads specified skills into agent memory on session start",
3
+ "version": "1.2.0",
4
+ "description": "OpenCode plugin that auto-loads skills into agent memory at session start or when touching specific file types",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",