opencode-plugin-preload-skills 1.1.4 → 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 +251 -117
- package/dist/index.cjs +378 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +380 -51
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
package/dist/index.d.cts
CHANGED
|
@@ -2,18 +2,38 @@ import { Plugin } from '@opencode-ai/plugin';
|
|
|
2
2
|
|
|
3
3
|
interface PreloadSkillsConfig {
|
|
4
4
|
skills: string[];
|
|
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;
|
|
5
14
|
persistAfterCompaction?: boolean;
|
|
6
15
|
debug?: boolean;
|
|
7
16
|
}
|
|
17
|
+
interface ConditionalSkill {
|
|
18
|
+
skill: string;
|
|
19
|
+
if: ConditionCheck;
|
|
20
|
+
}
|
|
21
|
+
interface ConditionCheck {
|
|
22
|
+
fileExists?: string;
|
|
23
|
+
packageHasDependency?: string;
|
|
24
|
+
envVar?: string;
|
|
25
|
+
}
|
|
8
26
|
interface ParsedSkill {
|
|
9
27
|
name: string;
|
|
10
28
|
description: string;
|
|
29
|
+
summary?: string;
|
|
11
30
|
content: string;
|
|
12
31
|
filePath: string;
|
|
32
|
+
tokenCount: number;
|
|
13
33
|
}
|
|
14
34
|
|
|
15
35
|
declare function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[];
|
|
16
|
-
declare function formatSkillsForInjection(skills: ParsedSkill[]): string;
|
|
36
|
+
declare function formatSkillsForInjection(skills: ParsedSkill[], useSummaries?: boolean): string;
|
|
17
37
|
|
|
18
38
|
declare const PreloadSkillsPlugin: Plugin;
|
|
19
39
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,18 +2,38 @@ import { Plugin } from '@opencode-ai/plugin';
|
|
|
2
2
|
|
|
3
3
|
interface PreloadSkillsConfig {
|
|
4
4
|
skills: string[];
|
|
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;
|
|
5
14
|
persistAfterCompaction?: boolean;
|
|
6
15
|
debug?: boolean;
|
|
7
16
|
}
|
|
17
|
+
interface ConditionalSkill {
|
|
18
|
+
skill: string;
|
|
19
|
+
if: ConditionCheck;
|
|
20
|
+
}
|
|
21
|
+
interface ConditionCheck {
|
|
22
|
+
fileExists?: string;
|
|
23
|
+
packageHasDependency?: string;
|
|
24
|
+
envVar?: string;
|
|
25
|
+
}
|
|
8
26
|
interface ParsedSkill {
|
|
9
27
|
name: string;
|
|
10
28
|
description: string;
|
|
29
|
+
summary?: string;
|
|
11
30
|
content: string;
|
|
12
31
|
filePath: string;
|
|
32
|
+
tokenCount: number;
|
|
13
33
|
}
|
|
14
34
|
|
|
15
35
|
declare function loadSkills(skillNames: string[], projectDir: string): ParsedSkill[];
|
|
16
|
-
declare function formatSkillsForInjection(skills: ParsedSkill[]): string;
|
|
36
|
+
declare function formatSkillsForInjection(skills: ParsedSkill[], useSummaries?: boolean): string;
|
|
17
37
|
|
|
18
38
|
declare const PreloadSkillsPlugin: Plugin;
|
|
19
39
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { 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,26 +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";
|
|
159
|
+
var FILE_TOOLS = ["read", "edit", "write", "glob", "grep"];
|
|
89
160
|
var DEFAULT_CONFIG = {
|
|
90
161
|
skills: [],
|
|
162
|
+
fileTypeSkills: {},
|
|
163
|
+
agentSkills: {},
|
|
164
|
+
pathPatterns: {},
|
|
165
|
+
contentTriggers: {},
|
|
166
|
+
groups: {},
|
|
167
|
+
conditionalSkills: [],
|
|
168
|
+
maxTokens: void 0,
|
|
169
|
+
useSummaries: false,
|
|
170
|
+
analytics: false,
|
|
91
171
|
persistAfterCompaction: true,
|
|
92
172
|
debug: false
|
|
93
173
|
};
|
|
@@ -104,6 +184,22 @@ function findConfigFile(projectDir) {
|
|
|
104
184
|
}
|
|
105
185
|
return null;
|
|
106
186
|
}
|
|
187
|
+
function parseStringArrayRecord(raw) {
|
|
188
|
+
if (!raw || typeof raw !== "object") return {};
|
|
189
|
+
const result = {};
|
|
190
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
191
|
+
if (Array.isArray(value)) {
|
|
192
|
+
result[key] = value.filter((v) => typeof v === "string");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
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
|
+
}
|
|
107
203
|
function loadConfigFile(projectDir) {
|
|
108
204
|
const configPath = findConfigFile(projectDir);
|
|
109
205
|
if (!configPath) {
|
|
@@ -114,6 +210,15 @@ function loadConfigFile(projectDir) {
|
|
|
114
210
|
const parsed = JSON.parse(content);
|
|
115
211
|
return {
|
|
116
212
|
skills: Array.isArray(parsed.skills) ? parsed.skills : [],
|
|
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,
|
|
117
222
|
persistAfterCompaction: typeof parsed.persistAfterCompaction === "boolean" ? parsed.persistAfterCompaction : void 0,
|
|
118
223
|
debug: typeof parsed.debug === "boolean" ? parsed.debug : void 0
|
|
119
224
|
};
|
|
@@ -121,8 +226,45 @@ function loadConfigFile(projectDir) {
|
|
|
121
226
|
return {};
|
|
122
227
|
}
|
|
123
228
|
}
|
|
229
|
+
function getFilePathFromArgs(args) {
|
|
230
|
+
if (typeof args.filePath === "string") return args.filePath;
|
|
231
|
+
if (typeof args.path === "string") return args.path;
|
|
232
|
+
if (typeof args.file === "string") return args.file;
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function getSkillsForExtension(ext, fileTypeSkills) {
|
|
236
|
+
const skills = [];
|
|
237
|
+
for (const [pattern, skillNames] of Object.entries(fileTypeSkills)) {
|
|
238
|
+
const extensions = pattern.split(",").map((e) => e.trim().toLowerCase());
|
|
239
|
+
if (extensions.includes(ext.toLowerCase())) {
|
|
240
|
+
skills.push(...skillNames);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return [...new Set(skills)];
|
|
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
|
+
}
|
|
124
265
|
var PreloadSkillsPlugin = async (ctx) => {
|
|
125
|
-
const
|
|
266
|
+
const sessionStates = /* @__PURE__ */ new Map();
|
|
267
|
+
const analyticsData = /* @__PURE__ */ new Map();
|
|
126
268
|
const fileConfig = loadConfigFile(ctx.directory);
|
|
127
269
|
const config = {
|
|
128
270
|
...DEFAULT_CONFIG,
|
|
@@ -139,76 +281,263 @@ var PreloadSkillsPlugin = async (ctx) => {
|
|
|
139
281
|
}
|
|
140
282
|
});
|
|
141
283
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
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
|
+
};
|
|
328
|
+
const skillCache = /* @__PURE__ */ new Map();
|
|
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);
|
|
334
|
+
}
|
|
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
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return resolved;
|
|
356
|
+
};
|
|
357
|
+
let initialSkills = [];
|
|
358
|
+
let initialFormattedContent = "";
|
|
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
|
+
);
|
|
377
|
+
const loadedNames = initialSkills.map((s) => s.name);
|
|
378
|
+
const missingNames = allInitialSkillNames.filter(
|
|
379
|
+
(s) => !loadedNames.includes(s) && !s.startsWith("@")
|
|
380
|
+
);
|
|
381
|
+
log("info", `Loaded ${initialSkills.length} initial skills`, {
|
|
152
382
|
loaded: loadedNames,
|
|
383
|
+
tokens: initialTokensUsed,
|
|
153
384
|
missing: missingNames.length > 0 ? missingNames : void 0
|
|
154
385
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
386
|
+
}
|
|
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) {
|
|
389
|
+
log("warn", "No skills configured. Create .opencode/preload-skills.json");
|
|
390
|
+
}
|
|
391
|
+
const getSessionState = (sessionID) => {
|
|
392
|
+
if (!sessionStates.has(sessionID)) {
|
|
393
|
+
sessionStates.set(sessionID, {
|
|
394
|
+
initialSkillsInjected: false,
|
|
395
|
+
loadedSkills: new Set(initialSkills.map((s) => s.name)),
|
|
396
|
+
totalTokensUsed: initialTokensUsed
|
|
158
397
|
});
|
|
159
398
|
}
|
|
160
|
-
|
|
399
|
+
return sessionStates.get(sessionID);
|
|
400
|
+
};
|
|
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
|
+
};
|
|
161
425
|
return {
|
|
162
426
|
"chat.message": async (input, output) => {
|
|
163
|
-
if (
|
|
164
|
-
|
|
427
|
+
if (!input.sessionID) return;
|
|
428
|
+
const state = getSessionState(input.sessionID);
|
|
429
|
+
const firstTextPart = output.parts.find((p) => p.type === "text");
|
|
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
|
+
);
|
|
165
439
|
}
|
|
166
|
-
if (
|
|
167
|
-
|
|
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
|
+
}
|
|
168
453
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
454
|
+
const contentToInject = [];
|
|
455
|
+
if (!state.initialSkillsInjected && initialFormattedContent) {
|
|
456
|
+
contentToInject.push(initialFormattedContent);
|
|
457
|
+
state.initialSkillsInjected = true;
|
|
458
|
+
log("info", "Injected initial preloaded skills", {
|
|
459
|
+
sessionID: input.sessionID,
|
|
460
|
+
skills: initialSkills.map((s) => s.name)
|
|
172
461
|
});
|
|
173
|
-
return;
|
|
174
462
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
463
|
+
const pending = pendingSkillInjections.get(input.sessionID);
|
|
464
|
+
if (pending && pending.length > 0) {
|
|
465
|
+
const formatted = formatSkillsForInjection(pending, config.useSummaries);
|
|
466
|
+
if (formatted) {
|
|
467
|
+
contentToInject.push(formatted);
|
|
468
|
+
log("info", "Injected triggered skills", {
|
|
469
|
+
sessionID: input.sessionID,
|
|
470
|
+
skills: pending.map((s) => s.name)
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
pendingSkillInjections.delete(input.sessionID);
|
|
474
|
+
}
|
|
475
|
+
if (contentToInject.length > 0) {
|
|
476
|
+
firstTextPart.text = `${contentToInject.join("\n\n")}
|
|
179
477
|
|
|
180
478
|
---
|
|
181
479
|
|
|
182
480
|
${firstTextPart.text}`;
|
|
183
481
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
482
|
+
},
|
|
483
|
+
"tool.execute.after": async (input, _output) => {
|
|
484
|
+
if (!FILE_TOOLS.includes(input.tool)) return;
|
|
485
|
+
if (!input.sessionID) return;
|
|
486
|
+
const state = getSessionState(input.sessionID);
|
|
487
|
+
const toolArgs = _output.metadata?.args;
|
|
488
|
+
if (!toolArgs) return;
|
|
489
|
+
const filePath = getFilePathFromArgs(toolArgs);
|
|
490
|
+
if (!filePath) return;
|
|
491
|
+
const ext = extname(filePath);
|
|
492
|
+
if (ext && config.fileTypeSkills) {
|
|
493
|
+
const extSkills = getSkillsForExtension(ext, config.fileTypeSkills);
|
|
494
|
+
if (extSkills.length > 0) {
|
|
495
|
+
queueSkillsForInjection(input.sessionID, extSkills, "fileType", state);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (config.pathPatterns) {
|
|
499
|
+
const pathSkills = getSkillsForPath(filePath, config.pathPatterns);
|
|
500
|
+
if (pathSkills.length > 0) {
|
|
501
|
+
queueSkillsForInjection(input.sessionID, pathSkills, "path", state);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
189
504
|
},
|
|
190
505
|
"experimental.session.compacting": async (input, output) => {
|
|
191
|
-
if (!config.persistAfterCompaction
|
|
192
|
-
|
|
506
|
+
if (!config.persistAfterCompaction) return;
|
|
507
|
+
const state = sessionStates.get(input.sessionID);
|
|
508
|
+
if (!state || state.loadedSkills.size === 0) return;
|
|
509
|
+
const allLoadedSkills = [];
|
|
510
|
+
for (const name of state.loadedSkills) {
|
|
511
|
+
const skill = skillCache.get(name);
|
|
512
|
+
if (skill) allLoadedSkills.push(skill);
|
|
193
513
|
}
|
|
514
|
+
if (allLoadedSkills.length === 0) return;
|
|
515
|
+
const formatted = formatSkillsForInjection(
|
|
516
|
+
allLoadedSkills,
|
|
517
|
+
config.useSummaries
|
|
518
|
+
);
|
|
194
519
|
output.context.push(
|
|
195
520
|
`## Preloaded Skills
|
|
196
521
|
|
|
197
|
-
The following skills were
|
|
522
|
+
The following skills were loaded during this session and should persist:
|
|
198
523
|
|
|
199
|
-
${
|
|
524
|
+
${formatted}`
|
|
200
525
|
);
|
|
201
|
-
|
|
202
|
-
log("info", "Added
|
|
526
|
+
state.initialSkillsInjected = false;
|
|
527
|
+
log("info", "Added all loaded skills to compaction context", {
|
|
203
528
|
sessionID: input.sessionID,
|
|
204
|
-
skillCount:
|
|
529
|
+
skillCount: allLoadedSkills.length
|
|
205
530
|
});
|
|
531
|
+
saveAnalytics();
|
|
206
532
|
},
|
|
207
533
|
event: async ({ event }) => {
|
|
208
534
|
if (event.type === "session.deleted" && "sessionID" in event.properties) {
|
|
209
535
|
const sessionID = event.properties.sessionID;
|
|
210
|
-
|
|
211
|
-
|
|
536
|
+
sessionStates.delete(sessionID);
|
|
537
|
+
pendingSkillInjections.delete(sessionID);
|
|
538
|
+
analyticsData.delete(sessionID);
|
|
539
|
+
log("debug", "Cleaned up session state", { sessionID });
|
|
540
|
+
saveAnalytics();
|
|
212
541
|
}
|
|
213
542
|
}
|
|
214
543
|
};
|