@minhpnq1807/contextos 0.5.45 → 0.5.49

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.
@@ -2,28 +2,20 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- import { enhanceRuleScoresWithEmbeddings, warmRuleEmbeddings } from "./embedding-scorer.js";
5
+ import {
6
+ enhanceRuleScoresWithEmbeddings,
7
+ searchIndexedEmbeddings,
8
+ warmIndexedEmbeddings
9
+ } from "./embedding-scorer.js";
10
+ import { fusedProjectQuery, workspacePackagePaths } from "./project-profiler.js";
6
11
 
7
12
  const DEFAULT_LIMIT = 3;
8
13
  const DEFAULT_MAX_SKILLS = 2000;
9
14
  const DEFAULT_EMBEDDING_CANDIDATES = 120;
10
- const DEFAULT_SEMANTIC_CATALOG_LIMIT = 300;
11
15
  const SCAN_CACHE_TTL_MS = 5 * 60 * 1000;
12
16
  const MAX_DESCRIPTION_CHARS = 500;
13
- const GENERIC_SKILL_TOKENS = new Set([
14
- "active", "agent", "agents", "code", "config", "configuration", "create", "development",
15
- "environment", "file", "files", "graph", "install", "integration", "local", "node", "package",
16
- "project", "refresh", "rebuild", "setup", "skill", "skills", "sync", "tool", "tools", "using",
17
- "build", "can", "not", "production", "show", "something", "https", "http", "com", "www",
18
- "a", "an", "and", "are", "as", "at", "be", "before", "after", "both", "by", "from", "for",
19
- "if", "in", "into", "is", "must", "of", "on", "or", "the", "then", "this", "to", "user",
20
- "users", "when", "where", "whether", "with"
21
- ]);
22
- const SPECIALIZED_SKILL_TOKENS = new Set([
23
- "android", "authorization", "cicd", "eas", "expo", "frontend", "ios", "next", "nextjs",
24
- "mcp", "modelcontextprotocol", "postgres", "postgresql", "react", "react-native", "tailwind",
25
- "typescript", "ui"
26
- ]);
17
+ const SKILL_EMBEDDING_THRESHOLD = 0.45;
18
+ const DEFAULT_SKILL_TIMEOUT_MS = 2000;
27
19
 
28
20
  const scanCache = new Map();
29
21
 
@@ -123,8 +115,9 @@ function monotonicNow() {
123
115
  }
124
116
 
125
117
  function cacheAndReturnSkills(cacheKey, skills) {
126
- scanCache.set(cacheKey, { createdAt: monotonicNow(), skills });
127
- return skills;
118
+ const deduped = dedupeSkills(skills);
119
+ scanCache.set(cacheKey, { createdAt: monotonicNow(), skills: deduped });
120
+ return deduped;
128
121
  }
129
122
 
130
123
  function findSkillFiles(root) {
@@ -170,18 +163,40 @@ export async function suggestSkills({
170
163
  dataDir,
171
164
  cwd = process.cwd(),
172
165
  limit = DEFAULT_LIMIT,
173
- timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
166
+ timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || DEFAULT_SKILL_TIMEOUT_MS),
167
+ indexedSearcher = searchIndexedEmbeddings,
168
+ embeddingEnhancer = enhanceRuleScoresWithEmbeddings
174
169
  } = {}) {
175
170
  if (!String(prompt || "").trim() || !skills.length) return [];
176
- const base = scoreSkillsByKeyword({ prompt, skills, projectHints: projectSkillHints({ cwd }) });
177
- if (skills.length > DEFAULT_SEMANTIC_CATALOG_LIMIT) {
178
- return finalizeSkillScores(base, limit, { minimumKeywordScore: 0.5 });
171
+ const catalog = dedupeSkills(skills);
172
+ const query = fusedProjectQuery({ prompt, cwd, dataDir });
173
+ const byId = new Map(catalog.map((skill) => [skillIndexId(skill), skill]));
174
+
175
+ if (dataDir) {
176
+ const indexed = await indexedSearcher({
177
+ kind: skillIndexKind(cwd),
178
+ task: query,
179
+ dataDir,
180
+ timeoutMs,
181
+ allowRemote: false
182
+ });
183
+ if (indexed.status === "enabled" && indexed.items.length) {
184
+ return finalizeSkillScores(indexed.items
185
+ .map((item) => {
186
+ const skill = byId.get(item.id);
187
+ if (!skill) return null;
188
+ return skillScoreFromEmbedding(skill, item.embeddingScore, [`embedding:${Number(item.embeddingScore || 0).toFixed(2)}`]);
189
+ })
190
+ .filter(Boolean), limit);
191
+ }
179
192
  }
180
193
 
181
- const embeddingCandidates = selectEmbeddingCandidates(base);
194
+ if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return [];
195
+
196
+ const embeddingCandidates = catalog.map((skill, index) => skillRule({ skill, index }));
182
197
  if (!embeddingCandidates.length) return [];
183
198
 
184
- const embedding = await enhanceRuleScoresWithEmbeddings(embeddingCandidates, prompt, {
199
+ const embedding = await embeddingEnhancer(embeddingCandidates, query, {
185
200
  dataDir,
186
201
  sources: embeddingCandidates.map((skill) => skill.path).filter(Boolean),
187
202
  timeoutMs,
@@ -191,26 +206,20 @@ export async function suggestSkills({
191
206
  return finalizeSkillScores(embedding.rules, limit);
192
207
  }
193
208
 
194
- function finalizeSkillScores(skills, limit, { minimumKeywordScore = 0.35 } = {}) {
209
+ function finalizeSkillScores(skills, limit) {
195
210
  const ranked = skills
196
- .filter((rule) => rule.domainEligible !== false)
197
211
  .map((rule) => ({
198
212
  name: rule.name,
199
213
  description: rule.description,
200
214
  path: rule.path,
201
215
  scope: rule.scope,
202
- keywordScore: rule.keywordScore,
203
216
  score: Math.min(1, Number(rule.score || 0)),
204
217
  embeddingScore: rule.embeddingScore,
205
- relevancePriority: Number(rule.relevancePriority || 0),
206
- rankScore: Math.min(1, Number(rule.score || 0)) + Number(rule.relevancePriority || 0) / 100,
218
+ rankScore: Math.min(1, Number(rule.score || 0)),
207
219
  reasons: rule.reasons || []
208
220
  }))
209
- .filter((skill) => Number(skill.keywordScore || 0) >= minimumKeywordScore
210
- || Number(skill.embeddingScore || 0) >= 0.62
211
- || Number(skill.relevancePriority || 0) >= 50)
221
+ .filter((skill) => Number(skill.embeddingScore || skill.score || 0) >= SKILL_EMBEDDING_THRESHOLD)
212
222
  .sort((a, b) => b.rankScore - a.rankScore
213
- || b.relevancePriority - a.relevancePriority
214
223
  || b.score - a.score
215
224
  || scopePriority(b.scope) - scopePriority(a.scope)
216
225
  || a.name.localeCompare(b.name));
@@ -229,14 +238,6 @@ function scopePriority(scope) {
229
238
  return scope === "project" ? 1 : 0;
230
239
  }
231
240
 
232
- function selectEmbeddingCandidates(skills) {
233
- if (skills.length <= DEFAULT_EMBEDDING_CANDIDATES) return skills;
234
- return [...skills]
235
- .filter((skill) => Number(skill.keywordScore || 0) > 0)
236
- .sort((a, b) => Number(b.keywordScore || 0) - Number(a.keywordScore || 0) || a.name.localeCompare(b.name))
237
- .slice(0, DEFAULT_EMBEDDING_CANDIDATES);
238
- }
239
-
240
241
  export async function warmSkillEmbeddings({
241
242
  cwd = process.cwd(),
242
243
  dataDir,
@@ -244,249 +245,81 @@ export async function warmSkillEmbeddings({
244
245
  skills = scanSkills({ cwd })
245
246
  } = {}) {
246
247
  if (!dataDir || !skills.length) return { count: 0, cachePath: null };
247
- return warmRuleEmbeddings({
248
- rules: skills.map((skill) => ({ content: `${skill.name} ${skill.description}` })),
249
- task: "skill discovery semantic retrieval",
248
+ const catalog = dedupeSkills(skills);
249
+ return warmIndexedEmbeddings({
250
+ kind: skillIndexKind(cwd),
251
+ items: catalog.map((skill) => ({
252
+ id: skillIndexId(skill),
253
+ text: skillEmbeddingText(skill)
254
+ })),
255
+ task: fusedProjectQuery({ prompt: "skill discovery semantic retrieval", cwd, dataDir }),
250
256
  dataDir,
251
- sources: skills.map((skill) => skill.path).filter(Boolean),
257
+ sources: catalog.map((skill) => skill.path).filter(Boolean),
252
258
  allowRemote
253
259
  });
254
260
  }
255
261
 
256
- function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
257
- const normalizedPrompt = normalizePrompt(prompt);
258
- const promptTokens = new Set(normalizedPrompt.split(/\s+/).filter(Boolean));
259
- const projectTokens = new Set(projectHints);
260
- return skills.map((skill, index) => {
261
- const enriched = skill.searchTokens ? skill : enrichSkill(skill);
262
- const name = String(enriched.name || "");
263
- const description = truncateDescription(enriched.description || "");
264
- const content = `${name} ${description}`;
265
- const matches = filterSkillMatches(
266
- enriched.searchTokens.filter((token) => promptTokens.has(token) && token.length > 2 && !GENERIC_SKILL_TOKENS.has(token)),
267
- { normalizedPrompt, enriched }
268
- );
269
- const projectMatches = enriched.searchTokens.filter((token) => projectTokens.has(token) && SPECIALIZED_SKILL_TOKENS.has(token));
270
- const normalizedName = enriched.normalizedName;
271
- const nameTokens = enriched.nameTokens;
272
- const nameHit = normalizedPrompt.includes(normalizedName);
273
- const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
274
- const scopeBonus = enriched.scope === "project" ? 0.08 : 0;
275
- const intentBonus = skillIntentBonus(normalizedPrompt, enriched, projectTokens);
276
- const relevancePriority = skillRelevancePriority(normalizedPrompt, enriched, projectTokens);
277
- const domainEligible = isSkillDomainEligible(normalizedPrompt, enriched, projectTokens);
278
- const matchScore = matches.reduce((sum, token) => sum + (SPECIALIZED_SKILL_TOKENS.has(token) ? 0.2 : 0.08), 0);
279
- const projectBonus = intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
280
- const score = Math.min(1, (matches.length ? 0.25 + matchScore : 0) + projectBonus + intentBonus + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
281
- return {
282
- id: `skill-${index + 1}`,
283
- name,
284
- description,
285
- path: enriched.path,
286
- scope: enriched.scope,
287
- content,
288
- score,
289
- keywordScore: score,
290
- relevancePriority,
291
- domainEligible,
292
- reasons: [
293
- ...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
294
- ...(projectBonus ? [`project:${projectMatches.slice(0, 4).join(",")}`] : []),
295
- ...(intentBonus ? ["intent-match"] : []),
296
- ...(nameHit || nameTokenHit ? ["name-match"] : [])
297
- ],
298
- originalOrder: index
299
- };
300
- });
301
- }
302
-
303
- function filterSkillMatches(matches, { normalizedPrompt, enriched }) {
304
- if (!/\beas\b/.test(normalizedPrompt)) return matches;
305
- const skillText = normalize(`${enriched.name} ${enriched.description}`);
306
- if (/\b(eas|expo|cicd)\b/.test(skillText)) return matches;
307
- return matches.filter((token) => token !== "android" && token !== "ios");
262
+ function skillRule({ skill, index }) {
263
+ const enriched = skill.searchTokens ? skill : enrichSkill(skill);
264
+ return {
265
+ id: skillIndexId(enriched),
266
+ name: enriched.name,
267
+ description: enriched.description,
268
+ path: enriched.path,
269
+ scope: enriched.scope,
270
+ content: skillEmbeddingText(enriched),
271
+ score: 0,
272
+ originalOrder: index
273
+ };
308
274
  }
309
275
 
310
- function isSkillDomainEligible(normalizedPrompt, enriched, projectTokens = new Set()) {
311
- const skillText = normalize(`${enriched.name} ${enriched.description}`);
312
- if (isMcpSkill(skillText) && !isMcpRelevantTask(normalizedPrompt, projectTokens)) return false;
313
- if (isOffensiveSecuritySkill(skillText) && !isSecurityTask(normalizedPrompt)) return false;
314
- if (isPlatformCommerceSkill(skillText) && !isPlatformCommerceTask(normalizedPrompt, skillText)) return false;
315
- if (!/\beas\b/.test(normalizedPrompt)) return true;
316
- if (!/\b(android|ios)\b/.test(skillText)) return true;
317
- return /\b(eas|expo|cicd)\b/.test(skillText);
276
+ function skillScoreFromEmbedding(skill, embeddingScore, reasons = []) {
277
+ const score = Math.min(1, Number(embeddingScore || 0));
278
+ return {
279
+ name: skill.name,
280
+ description: skill.description,
281
+ path: skill.path,
282
+ scope: skill.scope,
283
+ score,
284
+ embeddingScore: score,
285
+ reasons
286
+ };
318
287
  }
319
288
 
320
- function skillIntentBonus(normalizedPrompt, enriched, projectTokens = new Set()) {
321
- const skillText = normalize(`${enriched.name} ${enriched.description}`);
322
- if (isMcpRelevantTask(normalizedPrompt, projectTokens)
323
- && /\b(mcp|model context protocol|modelcontextprotocol|agent memory|tool developer|tool builder)\b/.test(skillText)) {
324
- return 0.48;
325
- }
326
- if (isCommerceTask(normalizedPrompt)
327
- && /\b(payment|payments|checkout|billing|bill|invoice|wallet|balance|stripe|paypal|commerce|monetization)\b/.test(skillText)) {
328
- return 0.46;
329
- }
330
- if (isContentAccessTask(normalizedPrompt)
331
- && /\b(api|endpoint|backend|service|services|auth|authorization|permission|permissions|access|rbac|frontend api)\b/.test(skillText)) {
332
- return 0.34;
333
- }
334
- if (isNotificationTask(normalizedPrompt)
335
- && /\b(notification|notifications|notify|message|sms|email|event|webhook)\b/.test(skillText)) {
336
- return 0.3;
337
- }
338
- if (isFrontendCheckoutTask(normalizedPrompt)
339
- && /\b(frontend|react|next|nextjs|ui|component|modal|api integration)\b/.test(skillText)) {
340
- return 0.32;
341
- }
342
- if (isExpoRuntimeTask(normalizedPrompt, projectTokens)
343
- && /\b(expo|eas|nativewind|react native|tailwind)\b/.test(skillText)) {
344
- return 0.46;
345
- }
346
- if (isNextAppRouterTask(normalizedPrompt)
347
- && /\b(next|nextjs)\b/.test(skillText)
348
- && /\b(app router|router|routing|server components)\b/.test(skillText)) {
349
- return 0.5;
350
- }
351
- if (/\beas\b/.test(normalizedPrompt)
352
- && /\b(eas|expo)\b/.test(skillText)
353
- && /\b(cicd|workflow|workflows|build|deploy|deployment|pipeline|pipelines)\b/.test(skillText)) {
354
- return 0.28;
355
- }
356
- if (/\b(webapp|frontend|ui|dashboard|button|page|component|app|router)\b/.test(normalizedPrompt)
357
- && /\b(frontend|react|next|nextjs|ui|component|tailwind|app router)\b/.test(skillText)) {
358
- return 0.36;
359
- }
360
- if (/\b(role|admin|creator|permission|permissions|authorization|access)\b/.test(normalizedPrompt)
361
- && /\b(auth|authentication|authorization|permission|permissions|access|rbac)\b/.test(skillText)) {
362
- return 0.32;
289
+ function dedupeSkills(skills) {
290
+ const byName = new Map();
291
+ for (const skill of skills || []) {
292
+ const enriched = skill.searchTokens ? skill : enrichSkill(skill);
293
+ const key = normalize(enriched.name);
294
+ if (!key) continue;
295
+ const existing = byName.get(key);
296
+ if (!existing || skillSourcePriority(enriched) > skillSourcePriority(existing)) {
297
+ byName.set(key, enriched);
298
+ }
363
299
  }
364
- return 0;
300
+ return [...byName.values()];
365
301
  }
366
302
 
367
- function skillRelevancePriority(normalizedPrompt, enriched, projectTokens = new Set()) {
368
- const skillText = normalize(`${enriched.name} ${enriched.description}`);
369
- const skillName = normalize(enriched.name);
303
+ function skillSourcePriority(skill) {
370
304
  let priority = 0;
371
- if (isMcpRelevantTask(normalizedPrompt, projectTokens)) {
372
- if (skillName === "mcp builder") priority += 760;
373
- if (skillName === "mcp management") priority += 740;
374
- if (skillName === "mcp tool developer") priority += 720;
375
- if (skillName === "agent memory mcp") priority += 700;
376
- if (skillName === "agent tool builder" || skillName === "context agent") priority += 260;
377
- if (/\b(mcp|model context protocol|modelcontextprotocol)\b/.test(skillText)) priority += 160;
378
- }
379
- if (isCommerceTask(normalizedPrompt)) {
380
- if (/\b(payment integration|stripe integration|paypal integration)\b/.test(skillText)) priority += 520;
381
- if (/\bbilling automation\b/.test(skillText)) priority += 430;
382
- if (/\b(payment|payments|checkout|billing|wallet|balance|stripe|paypal|commerce|monetization)\b/.test(skillText)) priority += 160;
383
- if (!/\bstripe\b/.test(normalizedPrompt) && /\bstripe\b/.test(skillText)) priority -= 520;
384
- if (!/\bpaypal\b/.test(normalizedPrompt) && /\bpaypal\b/.test(skillText)) priority -= 520;
385
- if (!/\bsquare\b/.test(normalizedPrompt) && /\bsquare\b/.test(skillText)) priority -= 440;
386
- if (/\b(mcp|metasploit|penetration|exploit|bug bounty)\b/.test(skillText)) priority -= 500;
387
- }
388
- if (isContentAccessTask(normalizedPrompt)) {
389
- if (/\b(api endpoint builder|backend development|backend architect|frontend api integration patterns)\b/.test(skillText)) priority += 260;
390
- if (/\b(auth implementation patterns|authorization|permission|permissions|access|rbac)\b/.test(skillText)) priority += 120;
391
- }
392
- if (isNotificationTask(normalizedPrompt)) {
393
- if (/\bsendblue notify\b/.test(skillText)) priority += 140;
394
- if (/\b(notification|notifications|notify|message|sms|email|event|webhook)\b/.test(skillText)) priority += 90;
395
- }
396
- if (isFrontendCheckoutTask(normalizedPrompt)) {
397
- if (/\bfrontend api integration patterns\b/.test(skillText)) priority += 220;
398
- if (/\breact nextjs development|nextjs best practices|nextjs app router patterns|frontend developer\b/.test(skillText)) priority += 90;
399
- }
400
- if (isExpoRuntimeTask(normalizedPrompt, projectTokens)) {
401
- if (/\bexpo deployment\b/.test(skillText)) priority += 900;
402
- if (/\bbuilding native ui\b/.test(skillText)) priority += 760;
403
- if (/\bexpo tailwind setup\b/.test(skillText)) priority += 620;
404
- if (/\bexpo\b/.test(skillText) && /\b(qr|expo go|run|running|start|connect|eas|deployment|build)\b/.test(skillText)) priority += 220;
405
- if (/\bnativewind|tailwind\b/.test(skillText) && projectTokens.has("nativewind")) priority += 120;
406
- if (/\b(next|nextjs|frontend designer|dark themed|glassmorphism|framer motion)\b/.test(skillText)) priority -= 160;
407
- }
408
- if (isNextAppRouterTask(normalizedPrompt)) {
409
- if (/\bnextjs app router patterns\b/.test(skillText)) priority += 600;
410
- if (/\bnextjs best practices\b/.test(skillText)) priority += 560;
411
- if (/\breact nextjs development\b/.test(skillText)) priority += 420;
412
- if (/\b(next|nextjs)\b/.test(skillText) && /\b(app router|router|routing|server components)\b/.test(skillText)) priority += 100;
413
- if (/\b(next|nextjs)\b/.test(skillText) && /\breact\b/.test(skillText)) priority += 70;
414
- if (/\b(glassmorphism|dark themed|dark theme|framer motion)\b/.test(skillText)) priority -= 40;
415
- }
416
- if (/\b(role|admin|creator|permission|permissions|authorization|access)\b/.test(normalizedPrompt)
417
- && /\b(auth|authentication|authorization|permission|permissions|access|rbac)\b/.test(skillText)) {
418
- priority += 55;
419
- }
305
+ if (skill.scope === "project") priority += 100;
306
+ const skillPath = String(skill.path || "");
307
+ if (skillPath.includes(`${path.sep}.codex${path.sep}skills${path.sep}`)) priority += 30;
308
+ if (skillPath.includes(`${path.sep}.config${path.sep}skillshare${path.sep}skills${path.sep}`)) priority += 20;
309
+ if (skillPath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)) priority += 10;
420
310
  return priority;
421
311
  }
422
312
 
423
- function isNextAppRouterTask(normalizedPrompt) {
424
- return /\bwebapp\b.*\bsrc\b.*\bapp\b/.test(normalizedPrompt)
425
- || /\b(next|nextjs)\b.*\b(app router|router|routing)\b/.test(normalizedPrompt)
426
- || /\bapp router\b/.test(normalizedPrompt);
427
- }
428
-
429
- function isExpoRuntimeTask(normalizedPrompt, projectTokens = new Set()) {
430
- const expoProject = projectTokens.has("expo") || projectTokens.has("nativewind") || projectTokens.has("eas");
431
- if (!expoProject) return false;
432
- return /\b(qr|connect|run|start|expo go|device|metro|tunnel|lan)\b/.test(normalizedPrompt);
433
- }
434
-
435
- function isCommerceTask(normalizedPrompt) {
436
- return /\b(purchase|purchased|buy|buyer|seller|payment|pay|checkout|wallet|balance|top up|topup|funded|billing|invoice)\b/.test(normalizedPrompt);
313
+ function skillIndexKind(cwd) {
314
+ return `skill:${path.resolve(cwd)}`;
437
315
  }
438
316
 
439
- function isContentAccessTask(normalizedPrompt) {
440
- return /\b(content access service|content access|access permissions|grant access|permissions|library|resources|tutorials|collections)\b/.test(normalizedPrompt);
317
+ function skillIndexId(skill) {
318
+ return normalize(skill.name);
441
319
  }
442
320
 
443
- function isNotificationTask(normalizedPrompt) {
444
- return /\b(notification|notifications|notify|buyer|seller)\b/.test(normalizedPrompt);
445
- }
446
-
447
- function isFrontendCheckoutTask(normalizedPrompt) {
448
- return /\b(modal|display|show|checkout|library|frontend|webapp|page|button)\b/.test(normalizedPrompt);
449
- }
450
-
451
- function isMcpTask(normalizedPrompt) {
452
- return /\b(mcp|model context protocol|tool server|tools server|server tool|bridge|proxy)\b/.test(normalizedPrompt);
453
- }
454
-
455
- function isMcpRelevantTask(normalizedPrompt, projectTokens = new Set()) {
456
- return isMcpTask(normalizedPrompt)
457
- || (isMcpProject(projectTokens) && isContextRetrievalTask(normalizedPrompt));
458
- }
459
-
460
- function isMcpProject(projectTokens = new Set()) {
461
- return projectTokens.has("mcp") || projectTokens.has("modelcontextprotocol");
462
- }
463
-
464
- function isContextRetrievalTask(normalizedPrompt) {
465
- return /\b(suggest|suggested|suggestion|skills|files|context|retrieval|retrieve|scorer|scoring|match|matching|prompt|hook|inject|injection)\b/.test(normalizedPrompt);
466
- }
467
-
468
- function isSecurityTask(normalizedPrompt) {
469
- return /\b(security|pentest|penetration|exploit|vulnerability|metasploit|bug bounty|owasp|xss|csrf|attack|audit)\b/.test(normalizedPrompt);
470
- }
471
-
472
- function isMcpSkill(skillText) {
473
- return /\bmcp\b|\bmodel context protocol\b/.test(skillText);
474
- }
475
-
476
- function isOffensiveSecuritySkill(skillText) {
477
- return /\b(metasploit|penetration testing|bug bounty|exploit|exploitation|privilege escalation|ethical hacking|web fuzzing|security assessment)\b/.test(skillText);
478
- }
479
-
480
- function isPlatformCommerceSkill(skillText) {
481
- return /\b(wordpress|woocommerce|shopify|odoo)\b/.test(skillText);
482
- }
483
-
484
- function isPlatformCommerceTask(normalizedPrompt, skillText) {
485
- if (/\bwordpress\b/.test(skillText)) return /\bwordpress\b/.test(normalizedPrompt);
486
- if (/\bwoocommerce\b/.test(skillText)) return /\bwoocommerce\b/.test(normalizedPrompt);
487
- if (/\bshopify\b/.test(skillText)) return /\bshopify\b/.test(normalizedPrompt);
488
- if (/\bodoo\b/.test(skillText)) return /\bodoo\b/.test(normalizedPrompt);
489
- return true;
321
+ function skillEmbeddingText(skill) {
322
+ return [skill.name, skill.description].filter(Boolean).join("\n");
490
323
  }
491
324
 
492
325
  export function projectSkillHints({ cwd = process.cwd() } = {}) {
@@ -511,48 +344,6 @@ export function projectSkillHints({ cwd = process.cwd() } = {}) {
511
344
  return [...hints];
512
345
  }
513
346
 
514
- function workspacePackagePaths(cwd) {
515
- const rootPackagePath = path.join(cwd, "package.json");
516
- const rootPackage = readJson(rootPackagePath);
517
- const paths = new Set([rootPackagePath]);
518
- for (const workspace of workspacePatterns(rootPackage?.workspaces)) {
519
- for (const packagePath of expandWorkspacePattern({ cwd, pattern: workspace })) {
520
- paths.add(packagePath);
521
- }
522
- }
523
- return [...paths];
524
- }
525
-
526
- function workspacePatterns(workspaces) {
527
- if (Array.isArray(workspaces)) return workspaces.filter((item) => typeof item === "string");
528
- if (Array.isArray(workspaces?.packages)) return workspaces.packages.filter((item) => typeof item === "string");
529
- return [];
530
- }
531
-
532
- function expandWorkspacePattern({ cwd, pattern }) {
533
- const normalized = String(pattern || "").replace(/\\/g, "/").replace(/\/+$/g, "");
534
- if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) return [];
535
- if (!normalized.includes("*")) {
536
- const packagePath = path.join(cwd, normalized, "package.json");
537
- return fs.existsSync(packagePath) ? [packagePath] : [];
538
- }
539
- const parts = normalized.split("/");
540
- const starIndex = parts.indexOf("*");
541
- if (starIndex < 0 || parts.includes("**")) return [];
542
- const baseDir = path.join(cwd, ...parts.slice(0, starIndex));
543
- const suffix = parts.slice(starIndex + 1);
544
- let entries = [];
545
- try {
546
- entries = fs.readdirSync(baseDir, { withFileTypes: true });
547
- } catch {
548
- return [];
549
- }
550
- return entries
551
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
552
- .map((entry) => path.join(baseDir, entry.name, ...suffix, "package.json"))
553
- .filter((packagePath) => fs.existsSync(packagePath));
554
- }
555
-
556
347
  function readJson(filePath) {
557
348
  try {
558
349
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
@@ -583,10 +374,3 @@ function enrichSkill(skill) {
583
374
  function normalize(value) {
584
375
  return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
585
376
  }
586
-
587
- function normalizePrompt(value) {
588
- return normalize(String(value || "")
589
- .replace(/https?:\/\/\S+/gi, " ")
590
- .replace(/giao\s+di[eệ]n/gi, "frontend ui")
591
- .replace(/phan\s+quyen/gi, "authorization role"));
592
- }