pi-skill-router 0.2.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/LICENSE +21 -0
- package/README.md +60 -0
- package/index.ts +31 -0
- package/package.json +37 -0
- package/src/menu.ts +286 -0
- package/src/paths.ts +42 -0
- package/src/registry.ts +167 -0
- package/src/router.ts +65 -0
- package/src/sync.ts +869 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { leafSkillFile, routerRoot } from "./paths";
|
|
3
|
+
import type { RegistryCollection } from "./registry";
|
|
4
|
+
|
|
5
|
+
export interface RouteMatch {
|
|
6
|
+
collectionId: string;
|
|
7
|
+
skillName: string;
|
|
8
|
+
skillPath: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function termMatches(prompt: string, term: string): boolean {
|
|
12
|
+
const escaped = term.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
return new RegExp(`(^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`).test(prompt);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function routePrompt(
|
|
17
|
+
prompt: string,
|
|
18
|
+
collections: RegistryCollection[],
|
|
19
|
+
availableSkills: ReadonlyMap<string, Skill>,
|
|
20
|
+
root = routerRoot(),
|
|
21
|
+
): RouteMatch | undefined {
|
|
22
|
+
const normalizedPrompt = prompt.toLowerCase();
|
|
23
|
+
let bestMatch: RouteMatch | undefined;
|
|
24
|
+
let bestScore = 0;
|
|
25
|
+
|
|
26
|
+
for (const collection of collections) {
|
|
27
|
+
if (!collection.enabled || collection.mode !== "suggest") continue;
|
|
28
|
+
if (!availableSkills.has(collection.gateway)) continue;
|
|
29
|
+
|
|
30
|
+
for (const route of collection.routes) {
|
|
31
|
+
const matchingTerms = route.terms.filter((term) => termMatches(normalizedPrompt, term));
|
|
32
|
+
if (matchingTerms.length === 0) continue;
|
|
33
|
+
|
|
34
|
+
const maxTermLength = Math.max(...matchingTerms.map((term) => term.length));
|
|
35
|
+
const totalLength = matchingTerms.reduce((sum, term) => sum + term.length, 0);
|
|
36
|
+
const score = maxTermLength * 10 + totalLength + matchingTerms.length * 5;
|
|
37
|
+
|
|
38
|
+
if (score > bestScore) {
|
|
39
|
+
bestScore = score;
|
|
40
|
+
bestMatch = {
|
|
41
|
+
collectionId: collection.id,
|
|
42
|
+
skillName: route.skill,
|
|
43
|
+
skillPath: leafSkillFile(root, collection.id, route.skill),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return bestMatch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isExplicitSkillInvocation(prompt: string): boolean {
|
|
53
|
+
return /(?:^|[^a-z0-9_])\/skill:[a-z0-9]+(?:-[a-z0-9]+)*(?=$|[^a-z0-9-])/i.test(prompt)
|
|
54
|
+
|| /<skill\s+name\s*=\s*["'][^"']+["']/i.test(prompt);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function routingGuidance(match: RouteMatch): string {
|
|
58
|
+
return [
|
|
59
|
+
"## Skill collection suggestion",
|
|
60
|
+
"",
|
|
61
|
+
`The user's request strongly matches the \`${match.collectionId}\` skill collection (\`${match.skillName}\`).`,
|
|
62
|
+
`Read \`${match.skillPath}\` before applying its workflow.`,
|
|
63
|
+
"This is a suggestion, not a replacement for your judgment. Do not load unrelated collection skills.",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|