assistme 0.2.9 → 0.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.js +391 -39
- package/package.json +1 -1
- package/src/agent/mcp-servers.ts +86 -3
- package/src/agent/processor.ts +59 -14
- package/src/agent/skill-evaluator.ts +258 -0
- package/src/agent/skills.ts +110 -14
package/src/agent/skills.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface Skill {
|
|
|
78
78
|
source: "bundled" | "manual" | "external" | "auto_extracted" | "auto_improved" | "job_generated";
|
|
79
79
|
dbId?: string;
|
|
80
80
|
sourceSkillId?: string;
|
|
81
|
+
invocationCount: number;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
function parseDbMetadata(raw: unknown): SkillMetadata {
|
|
@@ -101,6 +102,9 @@ export class SkillManager {
|
|
|
101
102
|
private idfCache: Map<string, number> = new Map();
|
|
102
103
|
private userId: string | null = null;
|
|
103
104
|
|
|
105
|
+
/** Cache for findRelevant() — keyed by prompt, invalidated on skill changes */
|
|
106
|
+
private relevanceCache: Map<string, { results: Skill[]; maxResults: number }> = new Map();
|
|
107
|
+
|
|
104
108
|
private readonly DESCRIPTION_BUDGET_CHARS = 16_000;
|
|
105
109
|
|
|
106
110
|
setUserId(userId: string): void {
|
|
@@ -146,9 +150,16 @@ export class SkillManager {
|
|
|
146
150
|
source: (row.source as Skill["source"]) || "manual",
|
|
147
151
|
dbId: row.id as string,
|
|
148
152
|
sourceSkillId: (row.source_skill_id as string) || undefined,
|
|
153
|
+
invocationCount: (row.invocation_count as number) || 0,
|
|
149
154
|
};
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
/** Invalidate caches when skills change (create, add, update, remove). */
|
|
158
|
+
private invalidateCaches(): void {
|
|
159
|
+
this.relevanceCache.clear();
|
|
160
|
+
this.buildIdfCache();
|
|
161
|
+
}
|
|
162
|
+
|
|
152
163
|
private buildIdfCache(): void {
|
|
153
164
|
this.idfCache.clear();
|
|
154
165
|
const docFreq = new Map<string, number>();
|
|
@@ -174,7 +185,14 @@ export class SkillManager {
|
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
findRelevant(prompt: string, maxResults = 3): Skill[] {
|
|
177
|
-
|
|
188
|
+
// Check cache (hit if same prompt and at least as many results cached)
|
|
189
|
+
const cacheKey = prompt.toLowerCase();
|
|
190
|
+
const cached = this.relevanceCache.get(cacheKey);
|
|
191
|
+
if (cached && cached.maxResults >= maxResults) {
|
|
192
|
+
return cached.results.slice(0, maxResults);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const lower = cacheKey;
|
|
178
196
|
const promptTokens = tokenize(lower);
|
|
179
197
|
const promptTokenSet = new Set(promptTokens);
|
|
180
198
|
const idf = (word: string): number => this.idfCache.get(word) || 1;
|
|
@@ -203,26 +221,53 @@ export class SkillManager {
|
|
|
203
221
|
if (score > 0) scored.push({ skill, score });
|
|
204
222
|
}
|
|
205
223
|
|
|
206
|
-
|
|
224
|
+
const results = scored
|
|
207
225
|
.sort((a, b) => b.score - a.score)
|
|
208
226
|
.slice(0, maxResults)
|
|
209
227
|
.map((s) => s.skill);
|
|
228
|
+
|
|
229
|
+
// Cache the result
|
|
230
|
+
this.relevanceCache.set(cacheKey, { results, maxResults });
|
|
231
|
+
return results;
|
|
210
232
|
}
|
|
211
233
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Build lightweight skill descriptions for the system prompt.
|
|
236
|
+
* When a taskPrompt is provided, relevant skills are prioritized to the top;
|
|
237
|
+
* remaining skills are sorted by usage frequency (invocationCount).
|
|
238
|
+
*/
|
|
239
|
+
buildSkillDescriptions(taskPrompt?: string): string {
|
|
240
|
+
const all = this.getAll().filter((s) => !s.disableModelInvocation);
|
|
241
|
+
if (all.length === 0) return "";
|
|
242
|
+
|
|
243
|
+
// Split into "always" skills and the rest
|
|
244
|
+
const alwaysSkills = all.filter((s) => s.metadata.always);
|
|
245
|
+
const rest = all.filter((s) => !s.metadata.always);
|
|
246
|
+
|
|
247
|
+
// If a task prompt is provided, find relevant skills and prioritize them
|
|
248
|
+
let relevantNames: Set<string> | null = null;
|
|
249
|
+
if (taskPrompt) {
|
|
250
|
+
const relevant = this.findRelevant(taskPrompt, 10);
|
|
251
|
+
relevantNames = new Set(relevant.map((s) => s.name));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Sort: relevant skills first (by match score, already sorted), then by invocation count
|
|
255
|
+
const sorted = rest.sort((a, b) => {
|
|
256
|
+
if (relevantNames) {
|
|
257
|
+
const aRelevant = relevantNames.has(a.name);
|
|
258
|
+
const bRelevant = relevantNames.has(b.name);
|
|
259
|
+
if (aRelevant && !bRelevant) return -1;
|
|
260
|
+
if (!aRelevant && bRelevant) return 1;
|
|
261
|
+
}
|
|
262
|
+
return (b.invocationCount || 0) - (a.invocationCount || 0);
|
|
263
|
+
});
|
|
220
264
|
|
|
221
|
-
|
|
265
|
+
const skills = [...alwaysSkills, ...sorted];
|
|
222
266
|
|
|
223
267
|
let budget = this.DESCRIPTION_BUDGET_CHARS;
|
|
224
268
|
let prompt = "\n\n## Your Skills\n";
|
|
225
|
-
prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n
|
|
269
|
+
prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n";
|
|
270
|
+
prompt += "If no skill matches but the task is a reusable pattern, consider creating one with skill_create.\n\n";
|
|
226
271
|
let included = 0;
|
|
227
272
|
|
|
228
273
|
for (const skill of skills) {
|
|
@@ -236,7 +281,7 @@ export class SkillManager {
|
|
|
236
281
|
}
|
|
237
282
|
|
|
238
283
|
if (included < skills.length) {
|
|
239
|
-
prompt += `\n_(${skills.length - included} additional skills available — use
|
|
284
|
+
prompt += `\n_(${skills.length - included} additional skills available — use skill_search to find more)_\n`;
|
|
240
285
|
}
|
|
241
286
|
|
|
242
287
|
return prompt;
|
|
@@ -294,6 +339,26 @@ export class SkillManager {
|
|
|
294
339
|
const id = (row.out_id || row.id) as string;
|
|
295
340
|
const skillName = (row.out_name || row.name) as string;
|
|
296
341
|
|
|
342
|
+
// Add to in-memory map so the skill is immediately searchable
|
|
343
|
+
this.skills.set(skillName, {
|
|
344
|
+
name: skillName,
|
|
345
|
+
description,
|
|
346
|
+
version: "1.0.0",
|
|
347
|
+
userInvocable: true,
|
|
348
|
+
disableModelInvocation: false,
|
|
349
|
+
keywords: options?.keywords || [],
|
|
350
|
+
allowedTools: [],
|
|
351
|
+
argumentHint: "",
|
|
352
|
+
metadata: options?.emoji ? { emoji: options.emoji } : {},
|
|
353
|
+
homepage: "",
|
|
354
|
+
content,
|
|
355
|
+
filePath: "",
|
|
356
|
+
source: (options?.source as Skill["source"]) || "manual",
|
|
357
|
+
dbId: id,
|
|
358
|
+
invocationCount: 0,
|
|
359
|
+
});
|
|
360
|
+
this.invalidateCaches();
|
|
361
|
+
|
|
297
362
|
log.info(`Skill "${skillName}" created in skills table (pending approval)`);
|
|
298
363
|
return { id, name: skillName };
|
|
299
364
|
} catch (err) {
|
|
@@ -323,7 +388,7 @@ export class SkillManager {
|
|
|
323
388
|
source_skill_id: skillId,
|
|
324
389
|
});
|
|
325
390
|
this.skills.set(skill.name, skill);
|
|
326
|
-
this.
|
|
391
|
+
this.invalidateCaches();
|
|
327
392
|
|
|
328
393
|
log.info(`Skill "${row.name}" added to user's collection`);
|
|
329
394
|
return skill;
|
|
@@ -337,6 +402,7 @@ export class SkillManager {
|
|
|
337
402
|
const skill = this.skills.get(name);
|
|
338
403
|
if (!skill) return false;
|
|
339
404
|
this.skills.delete(name);
|
|
405
|
+
this.invalidateCaches();
|
|
340
406
|
this.removeFromDb(name).catch(() => {});
|
|
341
407
|
return true;
|
|
342
408
|
}
|
|
@@ -387,6 +453,7 @@ export class SkillManager {
|
|
|
387
453
|
skill.content = newContent;
|
|
388
454
|
skill.description = newDescription;
|
|
389
455
|
skill.version = newVersion;
|
|
456
|
+
this.invalidateCaches();
|
|
390
457
|
|
|
391
458
|
this.syncToAgentSkills(name, newDescription, newContent, newVersion, {
|
|
392
459
|
source: "auto_improved",
|
|
@@ -604,6 +671,35 @@ export class SkillManager {
|
|
|
604
671
|
|
|
605
672
|
// ── Exported Utility Functions ─────────────────────────────────────
|
|
606
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Validate a skill name against the Agent Skills spec:
|
|
676
|
+
* 1-64 chars, lowercase kebab-case (a-z, 0-9, hyphens),
|
|
677
|
+
* no leading/trailing/consecutive hyphens.
|
|
678
|
+
* Returns null if valid, or an error message if invalid.
|
|
679
|
+
*/
|
|
680
|
+
export function validateSkillName(name: string): string | null {
|
|
681
|
+
if (!name || name.length === 0) return "name is empty";
|
|
682
|
+
if (name.length > 64) return `name too long (${name.length}/64 chars)`;
|
|
683
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
684
|
+
return `name must be lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens. Got: "${name}"`;
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Normalize a skill name to valid kebab-case.
|
|
691
|
+
* Converts to lowercase, replaces non-alphanumeric with hyphens,
|
|
692
|
+
* collapses consecutive hyphens, trims, and truncates to 64 chars.
|
|
693
|
+
*/
|
|
694
|
+
export function normalizeSkillName(name: string): string {
|
|
695
|
+
return name
|
|
696
|
+
.toLowerCase()
|
|
697
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
698
|
+
.replace(/^-+|-+$/g, "")
|
|
699
|
+
.replace(/-{2,}/g, "-")
|
|
700
|
+
.slice(0, 64);
|
|
701
|
+
}
|
|
702
|
+
|
|
607
703
|
export function substituteArguments(content: string, args: string): string {
|
|
608
704
|
const parts = args.split(/\s+/);
|
|
609
705
|
content = content.replace(/\$ARGUMENTS/g, args);
|