1import { formatProviderSkillDisplayName } from "@t3tools/shared/inlineSkills";
2import type { ServerProviderSkill } from "@t3tools/contracts";
3import {
4 dedupeProviderSkillsByName,
5 isProviderSkillUserInvocable,
6} from "@t3tools/client-runtime/providerSkills";
7import {
8 insertRankedSearchResult,
9 normalizeSearchQuery,
10 scoreQueryMatch,
11} from "@t3tools/shared/searchRanking";
12
13export function scoreProviderSkill(skill: ServerProviderSkill, query: string): number | null {
14 const normalizedName = skill.name.toLowerCase();
15 const normalizedLabel = formatProviderSkillDisplayName(skill).toLowerCase();
16 const normalizedShortDescription = skill.shortDescription?.toLowerCase() ?? "";
17 const normalizedDescription = skill.description?.toLowerCase() ?? "";
18 const normalizedScope = skill.scope?.toLowerCase() ?? "";
19
20 const scores = [
21 scoreQueryMatch({
22 value: normalizedName,
23 query,
24 exactBase: 0,
25 prefixBase: 2,
26 boundaryBase: 4,
27 includesBase: 6,
28 fuzzyBase: 100,
29 boundaryMarkers: ["-", "_", "/"],
30 }),
31 scoreQueryMatch({
32 value: normalizedLabel,
33 query,
34 exactBase: 1,
35 prefixBase: 3,
36 boundaryBase: 5,
37 includesBase: 7,
38 fuzzyBase: 110,
39 }),
40 scoreQueryMatch({
41 value: normalizedShortDescription,
42 query,
43 exactBase: 20,
44 prefixBase: 22,
45 boundaryBase: 24,
46 includesBase: 26,
47 }),
48 scoreQueryMatch({
49 value: normalizedDescription,
50 query,
51 exactBase: 30,
52 prefixBase: 32,
53 boundaryBase: 34,
54 includesBase: 36,
55 }),
56 scoreQueryMatch({
57 value: normalizedScope,
58 query,
59 exactBase: 40,
60 prefixBase: 42,
61 includesBase: 44,
62 }),
63 ].filter((score): score is number => score !== null);
64
65 if (scores.length === 0) {
66 return null;
67 }
68
69 return Math.min(...scores);
70}
71
72export function searchProviderSkills(
73 skills: ReadonlyArray<ServerProviderSkill>,
74 query: string,
75 limit = Number.POSITIVE_INFINITY,
76): ServerProviderSkill[] {
77 const enabledSkills = dedupeProviderSkillsByName(skills.filter(isProviderSkillUserInvocable));
78 const normalizedQuery = normalizeSearchQuery(query, { trimLeadingPattern: /^\p{Sc}+/u });
79
80 if (!normalizedQuery) {
81 return enabledSkills;
82 }
83
84 const ranked: Array<{
85 item: ServerProviderSkill;
86 score: number;
87 tieBreaker: string;
88 }> = [];
89
90 for (const skill of enabledSkills) {
91 const score = scoreProviderSkill(skill, normalizedQuery);
92 if (score === null) {
93 continue;
94 }
95
96 insertRankedSearchResult(
97 ranked,
98 {
99 item: skill,
100 score,
101 tieBreaker: `${formatProviderSkillDisplayName(skill).toLowerCase()}\u0000${skill.name}`,
102 },
103 limit,
104 );
105 }
106
107 return ranked.map((entry) => entry.item);
108}