@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.
- package/CHANGELOG.md +26 -0
- package/README.md +14 -6
- package/bin/ctx.js +72 -30
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/embedding-scorer.js +64 -25
- package/plugins/ctx/lib/graph-strategy.js +1 -0
- package/plugins/ctx/lib/output-config.js +37 -2
- package/plugins/ctx/lib/project-profiler.js +207 -0
- package/plugins/ctx/lib/prompt-hook.js +14 -16
- package/plugins/ctx/lib/scheduler.js +2 -1
- package/plugins/ctx/lib/score-context.js +12 -2
- package/plugins/ctx/lib/setup-wizard.js +3 -1
- package/plugins/ctx/lib/skill-discoverer.js +97 -313
|
@@ -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 {
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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 ||
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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:
|
|
257
|
+
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
252
258
|
allowRemote
|
|
253
259
|
});
|
|
254
260
|
}
|
|
255
261
|
|
|
256
|
-
function
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
300
|
+
return [...byName.values()];
|
|
365
301
|
}
|
|
366
302
|
|
|
367
|
-
function
|
|
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 (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
424
|
-
return
|
|
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
|
|
440
|
-
return
|
|
317
|
+
function skillIndexId(skill) {
|
|
318
|
+
return normalize(skill.name);
|
|
441
319
|
}
|
|
442
320
|
|
|
443
|
-
function
|
|
444
|
-
return
|
|
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
|
-
}
|