opencode-plugin-preload-skills 1.2.0 → 1.3.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 +247 -139
- package/dist/index.cjs +283 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +285 -58
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
package/dist/index.d.cts
CHANGED
|
@@ -3,18 +3,37 @@ import { Plugin } from '@opencode-ai/plugin';
|
|
|
3
3
|
interface PreloadSkillsConfig {
|
|
4
4
|
skills: string[];
|
|
5
5
|
fileTypeSkills?: Record<string, string[]>;
|
|
6
|
+
agentSkills?: Record<string, string[]>;
|
|
7
|
+
pathPatterns?: Record<string, string[]>;
|
|
8
|
+
contentTriggers?: Record<string, string[]>;
|
|
9
|
+
groups?: Record<string, string[]>;
|
|
10
|
+
conditionalSkills?: ConditionalSkill[];
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
useSummaries?: boolean;
|
|
13
|
+
analytics?: boolean;
|
|
6
14
|
persistAfterCompaction?: boolean;
|
|
7
15
|
debug?: boolean;
|
|
8
16
|
}
|
|
17
|
+
interface ConditionalSkill {
|
|
18
|
+
skill: string;
|
|
19
|
+
if: ConditionCheck;
|
|
20
|
+
}
|
|
21
|
+
interface ConditionCheck {
|
|
22
|
+
fileExists?: string;
|
|
23
|
+
packageHasDependency?: string;
|
|
24
|
+
envVar?: string;
|
|
25
|
+
}
|
|
9
26
|
interface ParsedSkill {
|
|
10
27
|
name: string;
|
|
11
28
|
description: string;
|
|
29
|
+
summary?: string;
|
|
12
30
|
content: string;
|
|
13
31
|
filePath: string;
|
|
32
|
+
tokenCount: number;
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
declare function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[];
|
|
17
|
-
declare function formatSkillsForInjection(skills: ParsedSkill[]): string;
|
|
36
|
+
declare function formatSkillsForInjection(skills: ParsedSkill[], useSummaries?: boolean): string;
|
|
18
37
|
|
|
19
38
|
declare const PreloadSkillsPlugin: Plugin;
|
|
20
39
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,18 +3,37 @@ import { Plugin } from '@opencode-ai/plugin';
|
|
|
3
3
|
interface PreloadSkillsConfig {
|
|
4
4
|
skills: string[];
|
|
5
5
|
fileTypeSkills?: Record<string, string[]>;
|
|
6
|
+
agentSkills?: Record<string, string[]>;
|
|
7
|
+
pathPatterns?: Record<string, string[]>;
|
|
8
|
+
contentTriggers?: Record<string, string[]>;
|
|
9
|
+
groups?: Record<string, string[]>;
|
|
10
|
+
conditionalSkills?: ConditionalSkill[];
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
useSummaries?: boolean;
|
|
13
|
+
analytics?: boolean;
|
|
6
14
|
persistAfterCompaction?: boolean;
|
|
7
15
|
debug?: boolean;
|
|
8
16
|
}
|
|
17
|
+
interface ConditionalSkill {
|
|
18
|
+
skill: string;
|
|
19
|
+
if: ConditionCheck;
|
|
20
|
+
}
|
|
21
|
+
interface ConditionCheck {
|
|
22
|
+
fileExists?: string;
|
|
23
|
+
packageHasDependency?: string;
|
|
24
|
+
envVar?: string;
|
|
25
|
+
}
|
|
9
26
|
interface ParsedSkill {
|
|
10
27
|
name: string;
|
|
11
28
|
description: string;
|
|
29
|
+
summary?: string;
|
|
12
30
|
content: string;
|
|
13
31
|
filePath: string;
|
|
32
|
+
tokenCount: number;
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
declare function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[];
|
|
17
|
-
declare function formatSkillsForInjection(skills: ParsedSkill[]): string;
|
|
36
|
+
declare function formatSkillsForInjection(skills: ParsedSkill[], useSummaries?: boolean): string;
|
|
18
37
|
|
|
19
38
|
declare const PreloadSkillsPlugin: Plugin;
|
|
20
39
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { extname, join } from 'path';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { extname, join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
+
function estimateTokens(text) {
|
|
7
|
+
return Math.ceil(text.length / 4);
|
|
8
|
+
}
|
|
9
|
+
function matchGlobPattern(filePath, pattern) {
|
|
10
|
+
let regexPattern = pattern.replace(/\*\*\//g, "\0DOUBLESTARSLASH\0").replace(/\*\*/g, "\0DOUBLESTAR\0").replace(/\*/g, "\0SINGLESTAR\0").replace(/\?/g, "\0QUESTION\0");
|
|
11
|
+
regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
regexPattern = regexPattern.replace(/\x00DOUBLESTARSLASH\x00/g, "(?:[^/]+/)*").replace(/\x00DOUBLESTAR\x00/g, ".*").replace(/\x00SINGLESTAR\x00/g, "[^/]*").replace(/\x00QUESTION\x00/g, "[^/]");
|
|
13
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
14
|
+
return regex.test(filePath);
|
|
15
|
+
}
|
|
16
|
+
function checkCondition(condition, projectDir) {
|
|
17
|
+
if (condition.fileExists) {
|
|
18
|
+
const fullPath = join(projectDir, condition.fileExists);
|
|
19
|
+
if (!existsSync(fullPath)) return false;
|
|
20
|
+
}
|
|
21
|
+
if (condition.packageHasDependency) {
|
|
22
|
+
const packageJsonPath = join(projectDir, "package.json");
|
|
23
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
24
|
+
try {
|
|
25
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
26
|
+
const deps = {
|
|
27
|
+
...packageJson.dependencies,
|
|
28
|
+
...packageJson.devDependencies,
|
|
29
|
+
...packageJson.peerDependencies
|
|
30
|
+
};
|
|
31
|
+
if (!deps[condition.packageHasDependency]) return false;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (condition.envVar) {
|
|
37
|
+
if (!process.env[condition.envVar]) return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function textContainsKeyword(text, keywords) {
|
|
42
|
+
const lowerText = text.toLowerCase();
|
|
43
|
+
return keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/skill-loader.ts
|
|
6
47
|
var SKILL_FILENAME = "SKILL.md";
|
|
7
48
|
var SKILL_SEARCH_PATHS = [
|
|
8
49
|
(dir) => join(dir, ".opencode", "skills"),
|
|
@@ -35,8 +76,19 @@ function parseFrontmatter(content) {
|
|
|
35
76
|
if (descMatch?.[1]) {
|
|
36
77
|
result.description = descMatch[1].trim();
|
|
37
78
|
}
|
|
79
|
+
const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
|
|
80
|
+
if (summaryMatch?.[1]) {
|
|
81
|
+
result.summary = summaryMatch[1].trim();
|
|
82
|
+
}
|
|
38
83
|
return result;
|
|
39
84
|
}
|
|
85
|
+
function extractAutoSummary(content, maxLength = 500) {
|
|
86
|
+
const withoutFrontmatter = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
|
87
|
+
const firstSection = withoutFrontmatter.split(/\n##\s/)[0] ?? "";
|
|
88
|
+
const cleaned = firstSection.replace(/^#\s+.+\n?/, "").replace(/\n+/g, " ").trim();
|
|
89
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
90
|
+
return cleaned.slice(0, maxLength).replace(/\s+\S*$/, "") + "...";
|
|
91
|
+
}
|
|
40
92
|
function loadSkill(skillName, projectDir) {
|
|
41
93
|
const filePath = findSkillFile(skillName, projectDir);
|
|
42
94
|
if (!filePath) {
|
|
@@ -44,12 +96,14 @@ function loadSkill(skillName, projectDir) {
|
|
|
44
96
|
}
|
|
45
97
|
try {
|
|
46
98
|
const content = readFileSync(filePath, "utf-8");
|
|
47
|
-
const { name, description } = parseFrontmatter(content);
|
|
99
|
+
const { name, description, summary } = parseFrontmatter(content);
|
|
48
100
|
return {
|
|
49
101
|
name: name ?? skillName,
|
|
50
102
|
description: description ?? "",
|
|
103
|
+
summary: summary ?? extractAutoSummary(content),
|
|
51
104
|
content,
|
|
52
|
-
filePath
|
|
105
|
+
filePath,
|
|
106
|
+
tokenCount: estimateTokens(content)
|
|
53
107
|
};
|
|
54
108
|
} catch {
|
|
55
109
|
return null;
|
|
@@ -68,28 +122,52 @@ function loadSkills(skillNames, projectDir) {
|
|
|
68
122
|
}
|
|
69
123
|
return skills;
|
|
70
124
|
}
|
|
71
|
-
function formatSkillsForInjection(skills) {
|
|
125
|
+
function formatSkillsForInjection(skills, useSummaries = false) {
|
|
72
126
|
if (!Array.isArray(skills) || skills.length === 0) {
|
|
73
127
|
return "";
|
|
74
128
|
}
|
|
75
|
-
const parts = skills.map(
|
|
76
|
-
|
|
77
|
-
${skill.
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
const parts = skills.map((skill) => {
|
|
130
|
+
const content = useSummaries && skill.summary ? skill.summary : skill.content;
|
|
131
|
+
return `<preloaded-skill name="${skill.name}">
|
|
132
|
+
${content}
|
|
133
|
+
</preloaded-skill>`;
|
|
134
|
+
});
|
|
80
135
|
return `<preloaded-skills>
|
|
81
136
|
The following skills have been automatically loaded for this session:
|
|
82
137
|
|
|
83
138
|
${parts.join("\n\n")}
|
|
84
139
|
</preloaded-skills>`;
|
|
85
140
|
}
|
|
141
|
+
function calculateTotalTokens(skills) {
|
|
142
|
+
return skills.reduce((sum, skill) => sum + skill.tokenCount, 0);
|
|
143
|
+
}
|
|
144
|
+
function filterSkillsByTokenBudget(skills, maxTokens) {
|
|
145
|
+
const result = [];
|
|
146
|
+
let totalTokens = 0;
|
|
147
|
+
for (const skill of skills) {
|
|
148
|
+
if (totalTokens + skill.tokenCount <= maxTokens) {
|
|
149
|
+
result.push(skill);
|
|
150
|
+
totalTokens += skill.tokenCount;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
86
155
|
|
|
87
156
|
// src/index.ts
|
|
88
157
|
var CONFIG_FILENAME = "preload-skills.json";
|
|
158
|
+
var ANALYTICS_FILENAME = "preload-skills-analytics.json";
|
|
89
159
|
var FILE_TOOLS = ["read", "edit", "write", "glob", "grep"];
|
|
90
160
|
var DEFAULT_CONFIG = {
|
|
91
161
|
skills: [],
|
|
92
162
|
fileTypeSkills: {},
|
|
163
|
+
agentSkills: {},
|
|
164
|
+
pathPatterns: {},
|
|
165
|
+
contentTriggers: {},
|
|
166
|
+
groups: {},
|
|
167
|
+
conditionalSkills: [],
|
|
168
|
+
maxTokens: void 0,
|
|
169
|
+
useSummaries: false,
|
|
170
|
+
analytics: false,
|
|
93
171
|
persistAfterCompaction: true,
|
|
94
172
|
debug: false
|
|
95
173
|
};
|
|
@@ -106,7 +184,7 @@ function findConfigFile(projectDir) {
|
|
|
106
184
|
}
|
|
107
185
|
return null;
|
|
108
186
|
}
|
|
109
|
-
function
|
|
187
|
+
function parseStringArrayRecord(raw) {
|
|
110
188
|
if (!raw || typeof raw !== "object") return {};
|
|
111
189
|
const result = {};
|
|
112
190
|
for (const [key, value] of Object.entries(raw)) {
|
|
@@ -116,6 +194,12 @@ function parseFileTypeSkills(raw) {
|
|
|
116
194
|
}
|
|
117
195
|
return result;
|
|
118
196
|
}
|
|
197
|
+
function parseConditionalSkills(raw) {
|
|
198
|
+
if (!Array.isArray(raw)) return [];
|
|
199
|
+
return raw.filter(
|
|
200
|
+
(item) => typeof item === "object" && item !== null && typeof item.skill === "string" && typeof item.if === "object"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
119
203
|
function loadConfigFile(projectDir) {
|
|
120
204
|
const configPath = findConfigFile(projectDir);
|
|
121
205
|
if (!configPath) {
|
|
@@ -126,7 +210,15 @@ function loadConfigFile(projectDir) {
|
|
|
126
210
|
const parsed = JSON.parse(content);
|
|
127
211
|
return {
|
|
128
212
|
skills: Array.isArray(parsed.skills) ? parsed.skills : [],
|
|
129
|
-
fileTypeSkills:
|
|
213
|
+
fileTypeSkills: parseStringArrayRecord(parsed.fileTypeSkills),
|
|
214
|
+
agentSkills: parseStringArrayRecord(parsed.agentSkills),
|
|
215
|
+
pathPatterns: parseStringArrayRecord(parsed.pathPatterns),
|
|
216
|
+
contentTriggers: parseStringArrayRecord(parsed.contentTriggers),
|
|
217
|
+
groups: parseStringArrayRecord(parsed.groups),
|
|
218
|
+
conditionalSkills: parseConditionalSkills(parsed.conditionalSkills),
|
|
219
|
+
maxTokens: typeof parsed.maxTokens === "number" ? parsed.maxTokens : void 0,
|
|
220
|
+
useSummaries: typeof parsed.useSummaries === "boolean" ? parsed.useSummaries : void 0,
|
|
221
|
+
analytics: typeof parsed.analytics === "boolean" ? parsed.analytics : void 0,
|
|
130
222
|
persistAfterCompaction: typeof parsed.persistAfterCompaction === "boolean" ? parsed.persistAfterCompaction : void 0,
|
|
131
223
|
debug: typeof parsed.debug === "boolean" ? parsed.debug : void 0
|
|
132
224
|
};
|
|
@@ -150,8 +242,29 @@ function getSkillsForExtension(ext, fileTypeSkills) {
|
|
|
150
242
|
}
|
|
151
243
|
return [...new Set(skills)];
|
|
152
244
|
}
|
|
245
|
+
function getSkillsForPath(filePath, pathPatterns) {
|
|
246
|
+
const skills = [];
|
|
247
|
+
for (const [pattern, skillNames] of Object.entries(pathPatterns)) {
|
|
248
|
+
if (matchGlobPattern(filePath, pattern)) {
|
|
249
|
+
skills.push(...skillNames);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return [...new Set(skills)];
|
|
253
|
+
}
|
|
254
|
+
function resolveSkillGroups(skillNames, groups) {
|
|
255
|
+
const resolved = [];
|
|
256
|
+
for (const name of skillNames) {
|
|
257
|
+
if (name.startsWith("@") && groups[name.slice(1)]) {
|
|
258
|
+
resolved.push(...groups[name.slice(1)]);
|
|
259
|
+
} else {
|
|
260
|
+
resolved.push(name);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return [...new Set(resolved)];
|
|
264
|
+
}
|
|
153
265
|
var PreloadSkillsPlugin = async (ctx) => {
|
|
154
266
|
const sessionStates = /* @__PURE__ */ new Map();
|
|
267
|
+
const analyticsData = /* @__PURE__ */ new Map();
|
|
155
268
|
const fileConfig = loadConfigFile(ctx.directory);
|
|
156
269
|
const config = {
|
|
157
270
|
...DEFAULT_CONFIG,
|
|
@@ -168,57 +281,176 @@ var PreloadSkillsPlugin = async (ctx) => {
|
|
|
168
281
|
}
|
|
169
282
|
});
|
|
170
283
|
};
|
|
284
|
+
const trackSkillUsage = (sessionID, skillName, triggerType) => {
|
|
285
|
+
if (!config.analytics) return;
|
|
286
|
+
if (!analyticsData.has(sessionID)) {
|
|
287
|
+
analyticsData.set(sessionID, {
|
|
288
|
+
sessionId: sessionID,
|
|
289
|
+
skillUsage: /* @__PURE__ */ new Map()
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const data = analyticsData.get(sessionID);
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
if (data.skillUsage.has(skillName)) {
|
|
295
|
+
const stats = data.skillUsage.get(skillName);
|
|
296
|
+
stats.loadCount++;
|
|
297
|
+
stats.lastLoaded = now;
|
|
298
|
+
} else {
|
|
299
|
+
data.skillUsage.set(skillName, {
|
|
300
|
+
skillName,
|
|
301
|
+
loadCount: 1,
|
|
302
|
+
triggerType,
|
|
303
|
+
firstLoaded: now,
|
|
304
|
+
lastLoaded: now
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const saveAnalytics = () => {
|
|
309
|
+
if (!config.analytics) return;
|
|
310
|
+
try {
|
|
311
|
+
const analyticsPath = join(ctx.directory, ".opencode", ANALYTICS_FILENAME);
|
|
312
|
+
const dir = dirname(analyticsPath);
|
|
313
|
+
if (!existsSync(dir)) {
|
|
314
|
+
mkdirSync(dir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
const serializable = {};
|
|
317
|
+
for (const [sessionId, data] of analyticsData) {
|
|
318
|
+
serializable[sessionId] = {
|
|
319
|
+
sessionId: data.sessionId,
|
|
320
|
+
skillUsage: Object.fromEntries(data.skillUsage)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
writeFileSync(analyticsPath, JSON.stringify(serializable, null, 2));
|
|
324
|
+
} catch {
|
|
325
|
+
log("warn", "Failed to save analytics");
|
|
326
|
+
}
|
|
327
|
+
};
|
|
171
328
|
const skillCache = /* @__PURE__ */ new Map();
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
329
|
+
const loadSkillsWithBudget = (skillNames, currentTokens, triggerType, sessionID) => {
|
|
330
|
+
const resolved = resolveSkillGroups(skillNames, config.groups ?? {});
|
|
331
|
+
let skills = loadSkills(resolved, ctx.directory);
|
|
332
|
+
for (const skill of skills) {
|
|
333
|
+
skillCache.set(skill.name, skill);
|
|
175
334
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
335
|
+
if (config.maxTokens) {
|
|
336
|
+
const remainingBudget = config.maxTokens - currentTokens;
|
|
337
|
+
skills = filterSkillsByTokenBudget(skills, remainingBudget);
|
|
338
|
+
}
|
|
339
|
+
for (const skill of skills) {
|
|
340
|
+
trackSkillUsage(sessionID, skill.name, triggerType);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
skills,
|
|
344
|
+
tokensUsed: calculateTotalTokens(skills)
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
const resolveConditionalSkills = () => {
|
|
348
|
+
if (!config.conditionalSkills?.length) return [];
|
|
349
|
+
const resolved = [];
|
|
350
|
+
for (const { skill, if: condition } of config.conditionalSkills) {
|
|
351
|
+
if (checkCondition(condition, ctx.directory)) {
|
|
352
|
+
resolved.push(skill);
|
|
353
|
+
}
|
|
179
354
|
}
|
|
180
|
-
return
|
|
355
|
+
return resolved;
|
|
181
356
|
};
|
|
182
357
|
let initialSkills = [];
|
|
183
358
|
let initialFormattedContent = "";
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
359
|
+
let initialTokensUsed = 0;
|
|
360
|
+
const allInitialSkillNames = [
|
|
361
|
+
...config.skills,
|
|
362
|
+
...resolveConditionalSkills()
|
|
363
|
+
];
|
|
364
|
+
if (allInitialSkillNames.length > 0) {
|
|
365
|
+
const result = loadSkillsWithBudget(
|
|
366
|
+
allInitialSkillNames,
|
|
367
|
+
0,
|
|
368
|
+
"initial",
|
|
369
|
+
"__init__"
|
|
370
|
+
);
|
|
371
|
+
initialSkills = result.skills;
|
|
372
|
+
initialTokensUsed = result.tokensUsed;
|
|
373
|
+
initialFormattedContent = formatSkillsForInjection(
|
|
374
|
+
initialSkills,
|
|
375
|
+
config.useSummaries
|
|
376
|
+
);
|
|
190
377
|
const loadedNames = initialSkills.map((s) => s.name);
|
|
191
|
-
const missingNames =
|
|
192
|
-
|
|
378
|
+
const missingNames = allInitialSkillNames.filter(
|
|
379
|
+
(s) => !loadedNames.includes(s) && !s.startsWith("@")
|
|
380
|
+
);
|
|
381
|
+
log("info", `Loaded ${initialSkills.length} initial skills`, {
|
|
193
382
|
loaded: loadedNames,
|
|
383
|
+
tokens: initialTokensUsed,
|
|
194
384
|
missing: missingNames.length > 0 ? missingNames : void 0
|
|
195
385
|
});
|
|
196
|
-
if (missingNames.length > 0) {
|
|
197
|
-
log("warn", "Some configured skills were not found", {
|
|
198
|
-
missing: missingNames
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
386
|
}
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
387
|
+
const hasTriggeredSkills = Object.keys(config.fileTypeSkills ?? {}).length > 0 || Object.keys(config.agentSkills ?? {}).length > 0 || Object.keys(config.pathPatterns ?? {}).length > 0 || Object.keys(config.contentTriggers ?? {}).length > 0;
|
|
388
|
+
if (allInitialSkillNames.length === 0 && !hasTriggeredSkills) {
|
|
204
389
|
log("warn", "No skills configured. Create .opencode/preload-skills.json");
|
|
205
390
|
}
|
|
206
391
|
const getSessionState = (sessionID) => {
|
|
207
392
|
if (!sessionStates.has(sessionID)) {
|
|
208
393
|
sessionStates.set(sessionID, {
|
|
209
394
|
initialSkillsInjected: false,
|
|
210
|
-
loadedSkills: new Set(initialSkills.map((s) => s.name))
|
|
395
|
+
loadedSkills: new Set(initialSkills.map((s) => s.name)),
|
|
396
|
+
totalTokensUsed: initialTokensUsed
|
|
211
397
|
});
|
|
212
398
|
}
|
|
213
399
|
return sessionStates.get(sessionID);
|
|
214
400
|
};
|
|
215
401
|
const pendingSkillInjections = /* @__PURE__ */ new Map();
|
|
402
|
+
const queueSkillsForInjection = (sessionID, skillNames, triggerType, state) => {
|
|
403
|
+
const newSkillNames = skillNames.filter((name) => !state.loadedSkills.has(name));
|
|
404
|
+
if (newSkillNames.length === 0) return;
|
|
405
|
+
const result = loadSkillsWithBudget(
|
|
406
|
+
newSkillNames,
|
|
407
|
+
state.totalTokensUsed,
|
|
408
|
+
triggerType,
|
|
409
|
+
sessionID
|
|
410
|
+
);
|
|
411
|
+
if (result.skills.length > 0) {
|
|
412
|
+
for (const skill of result.skills) {
|
|
413
|
+
state.loadedSkills.add(skill.name);
|
|
414
|
+
}
|
|
415
|
+
state.totalTokensUsed += result.tokensUsed;
|
|
416
|
+
const existing = pendingSkillInjections.get(sessionID) ?? [];
|
|
417
|
+
pendingSkillInjections.set(sessionID, [...existing, ...result.skills]);
|
|
418
|
+
log("debug", `Queued ${triggerType} skills for injection`, {
|
|
419
|
+
sessionID,
|
|
420
|
+
skills: result.skills.map((s) => s.name),
|
|
421
|
+
tokens: result.tokensUsed
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
};
|
|
216
425
|
return {
|
|
217
426
|
"chat.message": async (input, output) => {
|
|
218
427
|
if (!input.sessionID) return;
|
|
219
428
|
const state = getSessionState(input.sessionID);
|
|
220
429
|
const firstTextPart = output.parts.find((p) => p.type === "text");
|
|
221
430
|
if (!firstTextPart || !("text" in firstTextPart)) return;
|
|
431
|
+
const messageText = firstTextPart.text;
|
|
432
|
+
if (input.agent && config.agentSkills?.[input.agent]) {
|
|
433
|
+
queueSkillsForInjection(
|
|
434
|
+
input.sessionID,
|
|
435
|
+
config.agentSkills[input.agent],
|
|
436
|
+
"agent",
|
|
437
|
+
state
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (config.contentTriggers) {
|
|
441
|
+
for (const [keyword, skillNames] of Object.entries(
|
|
442
|
+
config.contentTriggers
|
|
443
|
+
)) {
|
|
444
|
+
if (textContainsKeyword(messageText, [keyword])) {
|
|
445
|
+
queueSkillsForInjection(
|
|
446
|
+
input.sessionID,
|
|
447
|
+
skillNames,
|
|
448
|
+
"content",
|
|
449
|
+
state
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
222
454
|
const contentToInject = [];
|
|
223
455
|
if (!state.initialSkillsInjected && initialFormattedContent) {
|
|
224
456
|
contentToInject.push(initialFormattedContent);
|
|
@@ -230,10 +462,10 @@ var PreloadSkillsPlugin = async (ctx) => {
|
|
|
230
462
|
}
|
|
231
463
|
const pending = pendingSkillInjections.get(input.sessionID);
|
|
232
464
|
if (pending && pending.length > 0) {
|
|
233
|
-
const formatted = formatSkillsForInjection(pending);
|
|
465
|
+
const formatted = formatSkillsForInjection(pending, config.useSummaries);
|
|
234
466
|
if (formatted) {
|
|
235
467
|
contentToInject.push(formatted);
|
|
236
|
-
log("info", "Injected
|
|
468
|
+
log("info", "Injected triggered skills", {
|
|
237
469
|
sessionID: input.sessionID,
|
|
238
470
|
skills: pending.map((s) => s.name)
|
|
239
471
|
});
|
|
@@ -249,7 +481,6 @@ ${firstTextPart.text}`;
|
|
|
249
481
|
}
|
|
250
482
|
},
|
|
251
483
|
"tool.execute.after": async (input, _output) => {
|
|
252
|
-
if (!hasFileTypeSkills) return;
|
|
253
484
|
if (!FILE_TOOLS.includes(input.tool)) return;
|
|
254
485
|
if (!input.sessionID) return;
|
|
255
486
|
const state = getSessionState(input.sessionID);
|
|
@@ -258,27 +489,17 @@ ${firstTextPart.text}`;
|
|
|
258
489
|
const filePath = getFilePathFromArgs(toolArgs);
|
|
259
490
|
if (!filePath) return;
|
|
260
491
|
const ext = extname(filePath);
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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);
|
|
492
|
+
if (ext && config.fileTypeSkills) {
|
|
493
|
+
const extSkills = getSkillsForExtension(ext, config.fileTypeSkills);
|
|
494
|
+
if (extSkills.length > 0) {
|
|
495
|
+
queueSkillsForInjection(input.sessionID, extSkills, "fileType", state);
|
|
271
496
|
}
|
|
272
497
|
}
|
|
273
|
-
if (
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
filePath,
|
|
279
|
-
extension: ext,
|
|
280
|
-
skills: newSkills.map((s) => s.name)
|
|
281
|
-
});
|
|
498
|
+
if (config.pathPatterns) {
|
|
499
|
+
const pathSkills = getSkillsForPath(filePath, config.pathPatterns);
|
|
500
|
+
if (pathSkills.length > 0) {
|
|
501
|
+
queueSkillsForInjection(input.sessionID, pathSkills, "path", state);
|
|
502
|
+
}
|
|
282
503
|
}
|
|
283
504
|
},
|
|
284
505
|
"experimental.session.compacting": async (input, output) => {
|
|
@@ -291,7 +512,10 @@ ${firstTextPart.text}`;
|
|
|
291
512
|
if (skill) allLoadedSkills.push(skill);
|
|
292
513
|
}
|
|
293
514
|
if (allLoadedSkills.length === 0) return;
|
|
294
|
-
const formatted = formatSkillsForInjection(
|
|
515
|
+
const formatted = formatSkillsForInjection(
|
|
516
|
+
allLoadedSkills,
|
|
517
|
+
config.useSummaries
|
|
518
|
+
);
|
|
295
519
|
output.context.push(
|
|
296
520
|
`## Preloaded Skills
|
|
297
521
|
|
|
@@ -304,13 +528,16 @@ ${formatted}`
|
|
|
304
528
|
sessionID: input.sessionID,
|
|
305
529
|
skillCount: allLoadedSkills.length
|
|
306
530
|
});
|
|
531
|
+
saveAnalytics();
|
|
307
532
|
},
|
|
308
533
|
event: async ({ event }) => {
|
|
309
534
|
if (event.type === "session.deleted" && "sessionID" in event.properties) {
|
|
310
535
|
const sessionID = event.properties.sessionID;
|
|
311
536
|
sessionStates.delete(sessionID);
|
|
312
537
|
pendingSkillInjections.delete(sessionID);
|
|
538
|
+
analyticsData.delete(sessionID);
|
|
313
539
|
log("debug", "Cleaned up session state", { sessionID });
|
|
540
|
+
saveAnalytics();
|
|
314
541
|
}
|
|
315
542
|
}
|
|
316
543
|
};
|