@minhpnq1807/contextos 0.5.50 → 0.5.52
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 +15 -0
- package/README.md +114 -9
- package/bin/ctx.js +64 -8
- package/eval/skill-routing/cases.yaml +366 -0
- package/eval/skill-routing/fixtures/docker-node/Dockerfile +4 -0
- package/eval/skill-routing/fixtures/docker-node/docker-compose.yml +5 -0
- package/eval/skill-routing/fixtures/docker-node/package.json +6 -0
- package/eval/skill-routing/fixtures/expo-eas/.github/workflows/eas.yml +1 -0
- package/eval/skill-routing/fixtures/expo-eas/app.json +5 -0
- package/eval/skill-routing/fixtures/expo-eas/eas.json +6 -0
- package/eval/skill-routing/fixtures/expo-eas/package.json +11 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/app.json +6 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/eas.json +5 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/package.json +8 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/vercel.json +3 -0
- package/eval/skill-routing/fixtures/express-mongo-jwt/package.json +8 -0
- package/eval/skill-routing/fixtures/firebase-hosting/firebase.json +11 -0
- package/eval/skill-routing/fixtures/firebase-hosting/package.json +6 -0
- package/eval/skill-routing/fixtures/flutter-firebase/pubspec.yaml +5 -0
- package/eval/skill-routing/fixtures/frontend-only-next/package.json +8 -0
- package/eval/skill-routing/fixtures/integration-test/jest.config.js +3 -0
- package/eval/skill-routing/fixtures/integration-test/package.json +10 -0
- package/eval/skill-routing/fixtures/jest-project/jest.config.js +3 -0
- package/eval/skill-routing/fixtures/jest-project/package.json +7 -0
- package/eval/skill-routing/fixtures/nest-prisma/package.json +10 -0
- package/eval/skill-routing/fixtures/nest-prisma/prisma/schema.prisma +4 -0
- package/eval/skill-routing/fixtures/next-vercel/.github/workflows/deploy.yml +1 -0
- package/eval/skill-routing/fixtures/next-vercel/package.json +8 -0
- package/eval/skill-routing/fixtures/next-vercel/vercel.json +3 -0
- package/eval/skill-routing/fixtures/oauth-google/.env.example +3 -0
- package/eval/skill-routing/fixtures/oauth-google/package.json +9 -0
- package/eval/skill-routing/fixtures/password-reset/package.json +8 -0
- package/eval/skill-routing/fixtures/playwright-project/package.json +6 -0
- package/eval/skill-routing/fixtures/playwright-project/playwright.config.ts +5 -0
- package/eval/skill-routing/fixtures/railway-render/package.json +6 -0
- package/eval/skill-routing/fixtures/railway-render/railway.json +6 -0
- package/eval/skill-routing/fixtures/railway-render/render.yaml +5 -0
- package/eval/skill-routing/fixtures/rbac-api/package.json +8 -0
- package/eval/skill-routing/fixtures/redis-cache/package.json +7 -0
- package/eval/skill-routing/fixtures/static-docs/README.md +3 -0
- package/eval/skill-routing/run-eval.js +278 -0
- package/package.json +3 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/analyzer.js +17 -2
- package/plugins/ctx/lib/auto-warm.js +1 -0
- package/plugins/ctx/lib/ctx-mcp-client.js +21 -0
- package/plugins/ctx/lib/embedding-scorer.js +34 -0
- package/plugins/ctx/lib/hook-io.js +11 -1
- package/plugins/ctx/lib/package-install.js +1 -1
- package/plugins/ctx/lib/project-profiler.js +5 -1
- package/plugins/ctx/lib/prompt-hook.js +17 -2
- package/plugins/ctx/lib/score-context.js +13 -2
- package/plugins/ctx/lib/setup-wizard.js +8 -3
- package/plugins/ctx/lib/skill-discoverer.js +480 -27
- package/plugins/ctx/lib/skillshare-sync.js +112 -0
- package/plugins/ctx/lib/workflow-discoverer.js +3 -1
- package/plugins/ctx/mcp/contextos-server.js +29 -1
- package/plugins/ctx/mcp/server.js +50 -4
|
@@ -16,6 +16,7 @@ const SCAN_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
|
16
16
|
const MAX_DESCRIPTION_CHARS = 500;
|
|
17
17
|
const SKILL_EMBEDDING_THRESHOLD = 0.45;
|
|
18
18
|
const DEFAULT_SKILL_TIMEOUT_MS = 2000;
|
|
19
|
+
const DEFAULT_ROUTER_THRESHOLD = 0.35;
|
|
19
20
|
|
|
20
21
|
const scanCache = new Map();
|
|
21
22
|
|
|
@@ -50,6 +51,40 @@ export function parseSkillFrontmatter(content = "", { fallbackName = "", skillPa
|
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
export function parseSkillMetadata(content = "") {
|
|
55
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
56
|
+
const root = {};
|
|
57
|
+
const stack = [{ indent: -1, value: root }];
|
|
58
|
+
|
|
59
|
+
for (const rawLine of lines) {
|
|
60
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) continue;
|
|
61
|
+
const indent = rawLine.match(/^\s*/)?.[0].length || 0;
|
|
62
|
+
const line = rawLine.trim();
|
|
63
|
+
while (stack.length > 1 && indent <= stack.at(-1).indent) stack.pop();
|
|
64
|
+
const parent = stack.at(-1).value;
|
|
65
|
+
|
|
66
|
+
if (line.startsWith("- ")) {
|
|
67
|
+
if (!Array.isArray(parent)) continue;
|
|
68
|
+
parent.push(parseScalar(line.slice(2)));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
73
|
+
if (!match) continue;
|
|
74
|
+
const [, key, rawValue] = match;
|
|
75
|
+
if (rawValue) {
|
|
76
|
+
parent[key] = parseScalar(rawValue);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const next = nextMeaningfulLine(lines, rawLine);
|
|
81
|
+
parent[key] = next?.trim().startsWith("- ") ? [] : {};
|
|
82
|
+
stack.push({ indent, value: parent[key] });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return normalizeSkillMetadata(root);
|
|
86
|
+
}
|
|
87
|
+
|
|
53
88
|
function truncateDescription(value) {
|
|
54
89
|
return String(value || "").replace(/\s+/g, " ").trim().slice(0, MAX_DESCRIPTION_CHARS);
|
|
55
90
|
}
|
|
@@ -101,8 +136,10 @@ export function scanSkills({ cwd = process.cwd(), roots = skillSearchRoots({ cwd
|
|
|
101
136
|
skillPath
|
|
102
137
|
});
|
|
103
138
|
if (!skill.name || !skill.description) continue;
|
|
139
|
+
const metadata = readSkillMetadata({ skillPath, skill });
|
|
104
140
|
skills.push(enrichSkill({
|
|
105
141
|
...skill,
|
|
142
|
+
metadata,
|
|
106
143
|
root,
|
|
107
144
|
scope: isInsidePath(skillPath, cwd) ? "project" : "global",
|
|
108
145
|
relativePath: path.relative(cwd, skillPath)
|
|
@@ -167,22 +204,19 @@ export async function suggestSkills({
|
|
|
167
204
|
limit = DEFAULT_LIMIT,
|
|
168
205
|
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || DEFAULT_SKILL_TIMEOUT_MS),
|
|
169
206
|
indexedSearcher = searchIndexedEmbeddings,
|
|
170
|
-
embeddingEnhancer = enhanceRuleScoresWithEmbeddings
|
|
207
|
+
embeddingEnhancer = enhanceRuleScoresWithEmbeddings,
|
|
208
|
+
embeddingsEnabled = true
|
|
171
209
|
} = {}) {
|
|
172
210
|
if (!String(prompt || "").trim() || !skills.length) return [];
|
|
173
211
|
const catalog = dedupeSkills(skills);
|
|
174
|
-
const query =
|
|
212
|
+
const query = skillQuery({ prompt, cwd, dataDir });
|
|
175
213
|
const byId = new Map(catalog.map((skill) => [skillIndexId(skill), skill]));
|
|
176
214
|
const explicitSkills = explicitSkillSuggestions({ prompt, byId });
|
|
215
|
+
const projectEvidence = detectProjectEvidence({ cwd });
|
|
216
|
+
if (!embeddingsEnabled) return finalizeSkillScores(explicitSkills, limit, { cwd, prompt, projectEvidence });
|
|
177
217
|
|
|
178
218
|
if (dataDir) {
|
|
179
|
-
const indexed = await
|
|
180
|
-
kind: skillIndexKind(cwd),
|
|
181
|
-
task: query,
|
|
182
|
-
dataDir,
|
|
183
|
-
timeoutMs,
|
|
184
|
-
allowRemote: false
|
|
185
|
-
});
|
|
219
|
+
const indexed = await searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher });
|
|
186
220
|
if (indexed.status === "enabled" && indexed.items.length) {
|
|
187
221
|
return finalizeSkillScores([
|
|
188
222
|
...explicitSkills,
|
|
@@ -193,14 +227,14 @@ export async function suggestSkills({
|
|
|
193
227
|
return skillScoreFromEmbedding(skill, item.embeddingScore, [`embedding:${Number(item.embeddingScore || 0).toFixed(2)}`]);
|
|
194
228
|
})
|
|
195
229
|
.filter(Boolean)
|
|
196
|
-
], limit);
|
|
230
|
+
], limit, { cwd, prompt, projectEvidence });
|
|
197
231
|
}
|
|
198
232
|
}
|
|
199
233
|
|
|
200
|
-
if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return finalizeSkillScores(explicitSkills, limit);
|
|
234
|
+
if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return finalizeSkillScores(explicitSkills, limit, { cwd, prompt, projectEvidence });
|
|
201
235
|
|
|
202
236
|
const embeddingCandidates = catalog.map((skill, index) => skillRule({ skill, index }));
|
|
203
|
-
if (!embeddingCandidates.length) return finalizeSkillScores(explicitSkills, limit);
|
|
237
|
+
if (!embeddingCandidates.length) return finalizeSkillScores(explicitSkills, limit, { cwd, prompt, projectEvidence });
|
|
204
238
|
|
|
205
239
|
const embedding = await embeddingEnhancer(embeddingCandidates, query, {
|
|
206
240
|
dataDir,
|
|
@@ -209,7 +243,13 @@ export async function suggestSkills({
|
|
|
209
243
|
allowRemote: false
|
|
210
244
|
});
|
|
211
245
|
|
|
212
|
-
return finalizeSkillScores([...explicitSkills, ...embedding.rules], limit);
|
|
246
|
+
return finalizeSkillScores([...explicitSkills, ...embedding.rules], limit, { cwd, prompt, projectEvidence });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function skillQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
|
|
250
|
+
const focusedPrompt = String(prompt || "").trim();
|
|
251
|
+
const fused = fusedProjectQuery({ prompt, cwd, dataDir });
|
|
252
|
+
return [focusedPrompt, focusedPrompt, fused].filter(Boolean).join("\n");
|
|
213
253
|
}
|
|
214
254
|
|
|
215
255
|
function explicitSkillSuggestions({ prompt = "", byId = new Map() } = {}) {
|
|
@@ -235,19 +275,28 @@ function extractExplicitSkillNames(prompt = "") {
|
|
|
235
275
|
return names;
|
|
236
276
|
}
|
|
237
277
|
|
|
238
|
-
function
|
|
278
|
+
export async function diagnoseSkills({
|
|
279
|
+
prompt = "",
|
|
280
|
+
skills = [],
|
|
281
|
+
dataDir,
|
|
282
|
+
cwd = process.cwd(),
|
|
283
|
+
limit = 10,
|
|
284
|
+
...options
|
|
285
|
+
} = {}) {
|
|
286
|
+
const suggestions = await suggestSkills({ prompt, skills, dataDir, cwd, limit, ...options });
|
|
287
|
+
const projectEvidence = detectProjectEvidence({ cwd });
|
|
288
|
+
return {
|
|
289
|
+
prompt,
|
|
290
|
+
cwd,
|
|
291
|
+
projectEvidence,
|
|
292
|
+
skills: suggestions
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function finalizeSkillScores(skills, limit, { cwd = process.cwd(), prompt = "", projectEvidence = detectProjectEvidence({ cwd }) } = {}) {
|
|
239
297
|
const ranked = skills
|
|
240
|
-
.map((rule) => ({
|
|
241
|
-
|
|
242
|
-
description: rule.description,
|
|
243
|
-
path: rule.path,
|
|
244
|
-
scope: rule.scope,
|
|
245
|
-
score: Math.min(1, Number(rule.score || 0)),
|
|
246
|
-
embeddingScore: rule.embeddingScore,
|
|
247
|
-
rankScore: Math.min(1, Number(rule.score || 0)),
|
|
248
|
-
reasons: rule.reasons || []
|
|
249
|
-
}))
|
|
250
|
-
.filter((skill) => Number(skill.embeddingScore || skill.score || 0) >= SKILL_EMBEDDING_THRESHOLD)
|
|
298
|
+
.map((rule) => hybridSkillScore(rule, { prompt, projectEvidence }))
|
|
299
|
+
.filter((skill) => skill.explicit || Number(skill.rankScore || 0) >= DEFAULT_ROUTER_THRESHOLD)
|
|
251
300
|
.sort((a, b) => b.rankScore - a.rankScore
|
|
252
301
|
|| b.score - a.score
|
|
253
302
|
|| scopePriority(b.scope) - scopePriority(a.scope)
|
|
@@ -275,7 +324,7 @@ export async function warmSkillEmbeddings({
|
|
|
275
324
|
} = {}) {
|
|
276
325
|
if (!dataDir || !skills.length) return { count: 0, cachePath: null };
|
|
277
326
|
const catalog = dedupeSkills(skills);
|
|
278
|
-
|
|
327
|
+
const workspaceResult = await warmIndexedEmbeddings({
|
|
279
328
|
kind: skillIndexKind(cwd),
|
|
280
329
|
items: catalog.map((skill) => ({
|
|
281
330
|
id: skillIndexId(skill),
|
|
@@ -286,6 +335,19 @@ export async function warmSkillEmbeddings({
|
|
|
286
335
|
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
287
336
|
allowRemote
|
|
288
337
|
});
|
|
338
|
+
if (workspaceResult.status === "missing-model" || workspaceResult.status === "warm-failed") return workspaceResult;
|
|
339
|
+
await warmIndexedEmbeddings({
|
|
340
|
+
kind: sharedSkillIndexKind(),
|
|
341
|
+
items: catalog.map((skill) => ({
|
|
342
|
+
id: skillIndexId(skill),
|
|
343
|
+
text: skillEmbeddingText(skill)
|
|
344
|
+
})),
|
|
345
|
+
task: fusedProjectQuery({ prompt: "skill discovery semantic retrieval", cwd, dataDir }),
|
|
346
|
+
dataDir,
|
|
347
|
+
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
348
|
+
allowRemote
|
|
349
|
+
});
|
|
350
|
+
return workspaceResult;
|
|
289
351
|
}
|
|
290
352
|
|
|
291
353
|
function skillRule({ skill, index }) {
|
|
@@ -309,6 +371,7 @@ function skillScoreFromEmbedding(skill, embeddingScore, reasons = []) {
|
|
|
309
371
|
description: skill.description,
|
|
310
372
|
path: skill.path,
|
|
311
373
|
scope: skill.scope,
|
|
374
|
+
metadata: skill.metadata,
|
|
312
375
|
score,
|
|
313
376
|
embeddingScore: score,
|
|
314
377
|
reasons
|
|
@@ -343,12 +406,43 @@ function skillIndexKind(cwd) {
|
|
|
343
406
|
return `skill:${path.resolve(cwd)}`;
|
|
344
407
|
}
|
|
345
408
|
|
|
409
|
+
function sharedSkillIndexKind() {
|
|
410
|
+
return "skill:global";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher }) {
|
|
414
|
+
const workspace = await indexedSearcher({
|
|
415
|
+
kind: skillIndexKind(cwd),
|
|
416
|
+
task: query,
|
|
417
|
+
dataDir,
|
|
418
|
+
timeoutMs,
|
|
419
|
+
allowRemote: false
|
|
420
|
+
});
|
|
421
|
+
if (workspace.status === "enabled" && workspace.items.length) return workspace;
|
|
422
|
+
const shared = await indexedSearcher({
|
|
423
|
+
kind: sharedSkillIndexKind(),
|
|
424
|
+
task: query,
|
|
425
|
+
dataDir,
|
|
426
|
+
timeoutMs,
|
|
427
|
+
allowRemote: false
|
|
428
|
+
});
|
|
429
|
+
if (shared.status === "enabled" && shared.items.length) return shared;
|
|
430
|
+
return workspace.status === "enabled" ? workspace : shared;
|
|
431
|
+
}
|
|
432
|
+
|
|
346
433
|
function skillIndexId(skill) {
|
|
347
434
|
return normalize(skill.name);
|
|
348
435
|
}
|
|
349
436
|
|
|
350
437
|
function skillEmbeddingText(skill) {
|
|
351
|
-
|
|
438
|
+
const metadata = skill.metadata || {};
|
|
439
|
+
return [
|
|
440
|
+
skill.name,
|
|
441
|
+
skill.description,
|
|
442
|
+
...(metadata.positivePrompts || []),
|
|
443
|
+
...(metadata.dependencies || []),
|
|
444
|
+
...(metadata.files || [])
|
|
445
|
+
].filter(Boolean).join("\n");
|
|
352
446
|
}
|
|
353
447
|
|
|
354
448
|
export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
@@ -381,6 +475,365 @@ function readJson(filePath) {
|
|
|
381
475
|
}
|
|
382
476
|
}
|
|
383
477
|
|
|
478
|
+
function readSkillMetadata({ skillPath, skill }) {
|
|
479
|
+
const skillDir = path.dirname(skillPath);
|
|
480
|
+
for (const fileName of ["skill.yaml", "skill.yml"]) {
|
|
481
|
+
const metadataPath = path.join(skillDir, fileName);
|
|
482
|
+
if (!fs.existsSync(metadataPath)) continue;
|
|
483
|
+
try {
|
|
484
|
+
return {
|
|
485
|
+
...inferSkillMetadata(skill),
|
|
486
|
+
...parseSkillMetadata(fs.readFileSync(metadataPath, "utf8")),
|
|
487
|
+
sourcePath: metadataPath
|
|
488
|
+
};
|
|
489
|
+
} catch {
|
|
490
|
+
return inferSkillMetadata(skill);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return inferSkillMetadata(skill);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizeSkillMetadata(metadata = {}) {
|
|
497
|
+
const positive = metadata.positive_triggers || metadata.triggers || {};
|
|
498
|
+
const negative = metadata.negative_triggers || {};
|
|
499
|
+
return {
|
|
500
|
+
id: metadata.id,
|
|
501
|
+
name: metadata.name,
|
|
502
|
+
positivePrompts: asArray(positive.prompts || positive.keywords || metadata.keywords),
|
|
503
|
+
files: asArray(positive.files || metadata.files),
|
|
504
|
+
dependencies: asArray(positive.dependencies || metadata.dependencies),
|
|
505
|
+
negativePrompts: asArray(negative.prompts || negative.keywords),
|
|
506
|
+
negativeFiles: asArray(negative.files),
|
|
507
|
+
negativeDependencies: asArray(negative.dependencies),
|
|
508
|
+
relatedSkills: asArray(metadata.related_skills)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function inferSkillMetadata(skill = {}) {
|
|
513
|
+
const text = normalize(`${skill.name || ""} ${skill.description || ""}`);
|
|
514
|
+
const metadata = {
|
|
515
|
+
positivePrompts: [],
|
|
516
|
+
files: [],
|
|
517
|
+
dependencies: [],
|
|
518
|
+
negativePrompts: [],
|
|
519
|
+
negativeFiles: [],
|
|
520
|
+
negativeDependencies: [],
|
|
521
|
+
relatedSkills: []
|
|
522
|
+
};
|
|
523
|
+
if (/\b(eas|expo|react native|mobile deployment|app store)\b/.test(text)) {
|
|
524
|
+
metadata.positivePrompts.push("eas", "expo build", "deployed", "deploy", "submit", "android", "ios", "mobile release", "qr", "connect");
|
|
525
|
+
metadata.files.push("eas.json", "app.json", "app.config.js", "app.config.ts", ".github/workflows/*");
|
|
526
|
+
metadata.dependencies.push("expo", "eas-cli", "expo-router", "react-native");
|
|
527
|
+
metadata.negativeDependencies.push("next", "vite");
|
|
528
|
+
metadata.negativeFiles.push("vercel.json");
|
|
529
|
+
}
|
|
530
|
+
if (/\bgithub actions|ci cd|cicd\b/.test(text)) {
|
|
531
|
+
metadata.positivePrompts.push("deploy", "deployed", "ci", "workflow", "github actions", "build failed");
|
|
532
|
+
metadata.files.push(".github/workflows/*");
|
|
533
|
+
}
|
|
534
|
+
if (/\benv|secret|credential|api key\b/.test(text)) {
|
|
535
|
+
metadata.positivePrompts.push("secret", "env", "environment", "api key", "deploy", "deployed");
|
|
536
|
+
metadata.files.push(".env", ".env.example", ".github/workflows/*");
|
|
537
|
+
}
|
|
538
|
+
if (/\bbuild log|debugging|debug|error|failed|failure\b/.test(text)) {
|
|
539
|
+
metadata.positivePrompts.push("error", "failed", "failure", "fix", "debug", "build", "deployed");
|
|
540
|
+
}
|
|
541
|
+
if (/\bvercel\b/.test(text)) {
|
|
542
|
+
metadata.positivePrompts.push("vercel", "deploy", "deployed");
|
|
543
|
+
metadata.files.push("vercel.json", "next.config.js", "next.config.ts");
|
|
544
|
+
metadata.dependencies.push("next", "vercel");
|
|
545
|
+
metadata.negativeDependencies.push("expo", "react-native");
|
|
546
|
+
metadata.negativeFiles.push("eas.json");
|
|
547
|
+
}
|
|
548
|
+
if (/\bnext|app router\b/.test(text)) {
|
|
549
|
+
metadata.dependencies.push("next", "react");
|
|
550
|
+
metadata.positivePrompts.push("frontend", "ui", "role", "dashboard", "app router");
|
|
551
|
+
}
|
|
552
|
+
return metadata;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function hybridSkillScore(skill, { prompt, projectEvidence }) {
|
|
556
|
+
const semanticScore = Math.min(1, Number(skill.embeddingScore || skill.score || 0));
|
|
557
|
+
const metadata = skill.metadata || inferSkillMetadata(skill);
|
|
558
|
+
const hasRouterSignals = metadataHasSignals(metadata);
|
|
559
|
+
const promptMatch = matchTextTriggers(prompt, metadata.positivePrompts);
|
|
560
|
+
const dependencyEvidence = matchList(projectEvidence.dependencies, metadata.dependencies);
|
|
561
|
+
const fileEvidence = matchFiles(projectEvidence.files, metadata.files);
|
|
562
|
+
const negativeDependencies = matchList(projectEvidence.dependencies, metadata.negativeDependencies);
|
|
563
|
+
const negativeFiles = matchFiles(projectEvidence.files, metadata.negativeFiles);
|
|
564
|
+
const negativePrompts = matchTextTriggers(prompt, metadata.negativePrompts);
|
|
565
|
+
const negativePenalty = Math.max(negativeDependencies.score, negativeFiles.score, negativePrompts.score);
|
|
566
|
+
const projectEvidenceScore = dependencyEvidence.score;
|
|
567
|
+
const fileConfigScore = fileEvidence.score;
|
|
568
|
+
const graphScore = 0;
|
|
569
|
+
const hybridScore = Math.max(0, Math.min(1,
|
|
570
|
+
semanticScore * 0.35
|
|
571
|
+
+ promptMatch.score * 0.20
|
|
572
|
+
+ projectEvidenceScore * 0.25
|
|
573
|
+
+ fileConfigScore * 0.10
|
|
574
|
+
+ graphScore * 0.05
|
|
575
|
+
- negativePenalty * 0.20
|
|
576
|
+
));
|
|
577
|
+
const explicit = (skill.reasons || []).includes("explicit-skill");
|
|
578
|
+
const finalScore = hasRouterSignals ? hybridScore : semanticScore;
|
|
579
|
+
const calibratedConfidence = calibrateSkillConfidence(finalScore, {
|
|
580
|
+
prompt,
|
|
581
|
+
promptMatch,
|
|
582
|
+
dependencyEvidence,
|
|
583
|
+
fileEvidence,
|
|
584
|
+
negativePenalty,
|
|
585
|
+
explicit
|
|
586
|
+
});
|
|
587
|
+
const evidence = [...new Set([
|
|
588
|
+
...(skill.reasons || []),
|
|
589
|
+
...promptMatch.matches.map((item) => `prompt:${item}`),
|
|
590
|
+
...dependencyEvidence.matches.map((item) => `dependency:${item}`),
|
|
591
|
+
...fileEvidence.matches.map((item) => `file:${item}`)
|
|
592
|
+
])];
|
|
593
|
+
const negativeEvidence = [
|
|
594
|
+
...negativeDependencies.matches.map((item) => `dependency:${item}`),
|
|
595
|
+
...negativeFiles.matches.map((item) => `file:${item}`),
|
|
596
|
+
...negativePrompts.matches.map((item) => `prompt:${item}`)
|
|
597
|
+
];
|
|
598
|
+
const rankScore = explicit ? semanticScore : finalScore;
|
|
599
|
+
return {
|
|
600
|
+
name: skill.name,
|
|
601
|
+
description: skill.description,
|
|
602
|
+
path: skill.path,
|
|
603
|
+
scope: skill.scope,
|
|
604
|
+
score: finalScore,
|
|
605
|
+
confidence: calibratedConfidence,
|
|
606
|
+
confidenceBand: confidenceBand(calibratedConfidence),
|
|
607
|
+
embeddingScore: semanticScore,
|
|
608
|
+
semanticScore,
|
|
609
|
+
promptTriggerScore: promptMatch.score,
|
|
610
|
+
projectEvidenceScore,
|
|
611
|
+
fileConfigScore,
|
|
612
|
+
graphScore,
|
|
613
|
+
negativePenalty,
|
|
614
|
+
rankScore,
|
|
615
|
+
explicit,
|
|
616
|
+
evidence,
|
|
617
|
+
negativeEvidence,
|
|
618
|
+
reasons: skill.reasons || []
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function calibrateSkillConfidence(score, {
|
|
623
|
+
prompt = "",
|
|
624
|
+
promptMatch,
|
|
625
|
+
dependencyEvidence,
|
|
626
|
+
fileEvidence,
|
|
627
|
+
negativePenalty = 0,
|
|
628
|
+
explicit = false
|
|
629
|
+
} = {}) {
|
|
630
|
+
let confidence = Math.max(0, Math.min(1, Number(score || 0)));
|
|
631
|
+
const hasDependencyEvidence = Boolean(dependencyEvidence?.matches?.length);
|
|
632
|
+
const hasFileEvidence = Boolean(fileEvidence?.matches?.length);
|
|
633
|
+
const hasPromptEvidence = Boolean(promptMatch?.matches?.length);
|
|
634
|
+
const hasProjectEvidence = hasDependencyEvidence || hasFileEvidence;
|
|
635
|
+
|
|
636
|
+
if (!hasProjectEvidence && !explicit) {
|
|
637
|
+
confidence = Math.min(confidence, 0.62);
|
|
638
|
+
}
|
|
639
|
+
if (isAmbiguousPrompt(prompt) && !(hasDependencyEvidence && hasFileEvidence) && !explicit) {
|
|
640
|
+
confidence = Math.min(confidence, 0.64);
|
|
641
|
+
}
|
|
642
|
+
if (hasPromptEvidence && hasProjectEvidence && confidence >= 0.5) {
|
|
643
|
+
confidence = Math.max(confidence, 0.68);
|
|
644
|
+
}
|
|
645
|
+
if (hasDependencyEvidence && hasFileEvidence) {
|
|
646
|
+
confidence = Math.max(confidence, 0.88);
|
|
647
|
+
}
|
|
648
|
+
if (negativePenalty > 0) {
|
|
649
|
+
confidence = Math.min(confidence, 0.74);
|
|
650
|
+
}
|
|
651
|
+
return Math.max(0, Math.min(1, confidence));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function confidenceBand(confidence) {
|
|
655
|
+
const value = Number(confidence || 0);
|
|
656
|
+
if (value >= 0.85) return "high";
|
|
657
|
+
if (value >= 0.65) return "medium";
|
|
658
|
+
return "low";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function isAmbiguousPrompt(prompt = "") {
|
|
662
|
+
const tokens = normalize(prompt).split(" ").filter(Boolean);
|
|
663
|
+
if (tokens.length <= 2) return true;
|
|
664
|
+
const generic = new Set(["fix", "add", "update", "debug", "deploy", "deployed", "auth", "cache", "test", "error"]);
|
|
665
|
+
return tokens.length <= 4 && tokens.every((token) => generic.has(token));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function detectProjectEvidence({ cwd = process.cwd() } = {}) {
|
|
669
|
+
const dependencies = new Set();
|
|
670
|
+
const scripts = new Set();
|
|
671
|
+
const files = new Set();
|
|
672
|
+
for (const packagePath of workspacePackagePaths(cwd)) {
|
|
673
|
+
const packageDir = path.dirname(packagePath);
|
|
674
|
+
const packageJson = readJson(packagePath);
|
|
675
|
+
files.add(normalizeFile(path.relative(cwd, packagePath) || "package.json"));
|
|
676
|
+
for (const name of Object.keys({
|
|
677
|
+
...(packageJson?.dependencies || {}),
|
|
678
|
+
...(packageJson?.devDependencies || {}),
|
|
679
|
+
...(packageJson?.peerDependencies || {})
|
|
680
|
+
})) dependencies.add(normalizeDependency(name));
|
|
681
|
+
for (const name of Object.keys(packageJson?.scripts || {})) scripts.add(normalize(name));
|
|
682
|
+
for (const fileName of knownProjectConfigFiles()) {
|
|
683
|
+
if (fs.existsSync(path.join(packageDir, fileName))) files.add(normalizeFile(path.relative(cwd, path.join(packageDir, fileName))));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const fileName of knownProjectConfigFiles()) {
|
|
687
|
+
if (fs.existsSync(path.join(cwd, fileName))) files.add(normalizeFile(fileName));
|
|
688
|
+
}
|
|
689
|
+
const pubspecPath = path.join(cwd, "pubspec.yaml");
|
|
690
|
+
if (fs.existsSync(pubspecPath)) {
|
|
691
|
+
files.add("pubspec.yaml");
|
|
692
|
+
for (const dependency of readPubspecDependencies(pubspecPath)) dependencies.add(normalizeDependency(dependency));
|
|
693
|
+
}
|
|
694
|
+
collectExistingFiles(cwd, [".github/workflows"], files);
|
|
695
|
+
return {
|
|
696
|
+
dependencies: [...dependencies],
|
|
697
|
+
scripts: [...scripts],
|
|
698
|
+
files: [...files]
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function knownProjectConfigFiles() {
|
|
703
|
+
return [
|
|
704
|
+
"eas.json",
|
|
705
|
+
"app.json",
|
|
706
|
+
"app.config.js",
|
|
707
|
+
"app.config.ts",
|
|
708
|
+
"vercel.json",
|
|
709
|
+
"next.config.js",
|
|
710
|
+
"next.config.ts",
|
|
711
|
+
"firebase.json",
|
|
712
|
+
"railway.json",
|
|
713
|
+
"render.yaml",
|
|
714
|
+
"Dockerfile",
|
|
715
|
+
"docker-compose.yml",
|
|
716
|
+
"jest.config.js",
|
|
717
|
+
"jest.config.ts",
|
|
718
|
+
"playwright.config.js",
|
|
719
|
+
"playwright.config.ts"
|
|
720
|
+
];
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function readPubspecDependencies(filePath) {
|
|
724
|
+
const dependencies = [];
|
|
725
|
+
let inDependencies = false;
|
|
726
|
+
try {
|
|
727
|
+
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
728
|
+
if (/^dependencies:\s*$/.test(line)) {
|
|
729
|
+
inDependencies = true;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (inDependencies && /^\S/.test(line) && !/^dependencies:\s*$/.test(line)) break;
|
|
733
|
+
const match = line.match(/^\s{2}([A-Za-z0-9_-]+):/);
|
|
734
|
+
if (inDependencies && match) dependencies.push(match[1]);
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
return dependencies;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function collectExistingFiles(cwd, relativeDirs, files) {
|
|
743
|
+
for (const relativeDir of relativeDirs) {
|
|
744
|
+
const directory = path.join(cwd, relativeDir);
|
|
745
|
+
let entries = [];
|
|
746
|
+
try {
|
|
747
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
748
|
+
} catch {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
files.add(normalizeFile(relativeDir));
|
|
752
|
+
for (const entry of entries) {
|
|
753
|
+
if (entry.isFile()) files.add(normalizeFile(path.join(relativeDir, entry.name)));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function matchTextTriggers(text, triggers = []) {
|
|
759
|
+
const normalizedText = normalize(text);
|
|
760
|
+
const matches = [];
|
|
761
|
+
for (const trigger of triggers || []) {
|
|
762
|
+
const normalizedTrigger = normalize(trigger);
|
|
763
|
+
if (!normalizedTrigger) continue;
|
|
764
|
+
if (normalizedText.includes(normalizedTrigger)) matches.push(trigger);
|
|
765
|
+
}
|
|
766
|
+
return { score: matches.length ? 1 : 0, matches };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function matchList(values = [], triggers = []) {
|
|
770
|
+
const valueSet = new Set(values.map(normalizeDependency));
|
|
771
|
+
const matches = [];
|
|
772
|
+
for (const trigger of triggers || []) {
|
|
773
|
+
const normalizedTrigger = normalizeDependency(trigger);
|
|
774
|
+
if (valueSet.has(normalizedTrigger)) matches.push(trigger);
|
|
775
|
+
}
|
|
776
|
+
return { score: matches.length ? 1 : 0, matches };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function matchFiles(files = [], triggers = []) {
|
|
780
|
+
const normalizedFiles = files.map(normalizeFile);
|
|
781
|
+
const matches = [];
|
|
782
|
+
for (const trigger of triggers || []) {
|
|
783
|
+
const normalizedTrigger = normalizeFile(trigger);
|
|
784
|
+
if (!normalizedTrigger) continue;
|
|
785
|
+
if (normalizedTrigger.endsWith("/*")) {
|
|
786
|
+
const prefix = normalizedTrigger.slice(0, -1);
|
|
787
|
+
if (normalizedFiles.some((file) => file.startsWith(prefix))) matches.push(trigger);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (normalizedFiles.some((file) => file === normalizedTrigger || file.endsWith(`/${normalizedTrigger}`))) matches.push(trigger);
|
|
791
|
+
}
|
|
792
|
+
return { score: matches.length ? 1 : 0, matches };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function metadataHasSignals(metadata = {}) {
|
|
796
|
+
return [
|
|
797
|
+
metadata.positivePrompts,
|
|
798
|
+
metadata.files,
|
|
799
|
+
metadata.dependencies,
|
|
800
|
+
metadata.negativePrompts,
|
|
801
|
+
metadata.negativeFiles,
|
|
802
|
+
metadata.negativeDependencies
|
|
803
|
+
].some((items) => Array.isArray(items) && items.length > 0);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function asArray(value) {
|
|
807
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
808
|
+
if (value === undefined || value === null || value === "") return [];
|
|
809
|
+
return [String(value)];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function parseScalar(value) {
|
|
813
|
+
const trimmed = String(value || "").trim();
|
|
814
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
815
|
+
return trimmed.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
816
|
+
}
|
|
817
|
+
return trimmed.replace(/^["']|["']$/g, "");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function nextMeaningfulLine(lines, currentRawLine) {
|
|
821
|
+
const start = lines.indexOf(currentRawLine);
|
|
822
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
823
|
+
const line = lines[index];
|
|
824
|
+
if (line.trim() && !line.trimStart().startsWith("#")) return line;
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function normalizeFile(value) {
|
|
830
|
+
return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function normalizeDependency(value) {
|
|
834
|
+
return String(value || "").toLowerCase().trim();
|
|
835
|
+
}
|
|
836
|
+
|
|
384
837
|
function addHintText(hints, value) {
|
|
385
838
|
for (const token of normalize(value).split(/\s+/).filter(Boolean)) hints.add(token);
|
|
386
839
|
}
|