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/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 { readFileSync, existsSync } from 'fs';
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
- (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";
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 parseFileTypeSkills(raw) {
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: parseFileTypeSkills(parsed.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 getOrLoadSkill = (skillName) => {
173
- if (skillCache.has(skillName)) {
174
- return skillCache.get(skillName) ?? null;
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
- const skill = loadSkill(skillName, ctx.directory);
177
- if (skill) {
178
- skillCache.set(skillName, skill);
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 skill;
355
+ return resolved;
181
356
  };
182
357
  let initialSkills = [];
183
358
  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
- }
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 = config.skills.filter((s) => !loadedNames.includes(s));
192
- log("info", `Loaded ${initialSkills.length} skills for preloading`, {
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 hasFileTypeSkills = Object.keys(config.fileTypeSkills ?? {}).length > 0;
203
- if (config.skills.length === 0 && !hasFileTypeSkills) {
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 file-type triggered skills", {
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 (!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);
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 (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
- });
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(allLoadedSkills);
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
  };