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/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 { readFileSync, existsSync } from 'fs';
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
- (skill) => `<preloaded-skill name="${skill.name}">
77
- ${skill.content}
78
- </preloaded-skill>`
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 injectedSessions = /* @__PURE__ */ new Set();
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
- 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);
150
- const missingNames = config.skills.filter((s) => !loadedNames.includes(s));
151
- log("info", `Loaded ${loadedSkills.length} skills for preloading`, {
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
- if (missingNames.length > 0) {
156
- log("warn", "Some configured skills were not found", {
157
- missing: missingNames
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 (loadedSkills.length === 0 || !formattedContent) {
164
- return;
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 (!input.sessionID) {
167
- return;
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
- if (injectedSessions.has(input.sessionID)) {
170
- log("debug", "Skills already injected for session", {
171
- sessionID: input.sessionID
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
- injectedSessions.add(input.sessionID);
176
- const firstTextPart = output.parts.find((p) => p.type === "text");
177
- if (firstTextPart && "text" in firstTextPart) {
178
- firstTextPart.text = `${formattedContent}
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
- log("info", "Injected preloaded skills into session", {
185
- sessionID: input.sessionID,
186
- skillCount: loadedSkills.length,
187
- skills: loadedSkills.map((s) => s.name)
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 || loadedSkills.length === 0) {
192
- return;
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 auto-loaded at session start and should persist:
522
+ The following skills were loaded during this session and should persist:
198
523
 
199
- ${formattedContent}`
524
+ ${formatted}`
200
525
  );
201
- injectedSessions.delete(input.sessionID);
202
- log("info", "Added preloaded skills to compaction context", {
526
+ state.initialSkillsInjected = false;
527
+ log("info", "Added all loaded skills to compaction context", {
203
528
  sessionID: input.sessionID,
204
- skillCount: loadedSkills.length
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
- injectedSessions.delete(sessionID);
211
- log("debug", "Cleaned up session tracking", { sessionID });
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
  };