takomi 2.1.13 → 2.1.15

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.
Files changed (39) hide show
  1. package/.pi/agents/architect.md +73 -73
  2. package/.pi/agents/coder.md +70 -70
  3. package/.pi/agents/designer.md +72 -72
  4. package/.pi/agents/orchestrator.md +122 -122
  5. package/.pi/agents/reviewer.md +71 -71
  6. package/.pi/extensions/oauth-router/provider.ts +3 -1
  7. package/.pi/extensions/takomi-context-manager/config.ts +48 -48
  8. package/.pi/extensions/takomi-context-manager/context-router.ts +57 -57
  9. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +28 -28
  10. package/.pi/extensions/takomi-context-manager/diagnostics.ts +55 -55
  11. package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +56 -56
  12. package/.pi/extensions/takomi-context-manager/index.ts +56 -56
  13. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +228 -206
  14. package/.pi/extensions/takomi-context-manager/policy-registry.ts +97 -97
  15. package/.pi/extensions/takomi-context-manager/policy-tools.ts +35 -35
  16. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +39 -39
  17. package/.pi/extensions/takomi-context-manager/prompt-rewriter.ts +100 -100
  18. package/.pi/extensions/takomi-context-manager/skill-registry.ts +87 -87
  19. package/.pi/extensions/takomi-context-manager/skill-tools.ts +80 -80
  20. package/.pi/extensions/takomi-context-manager/state.ts +68 -68
  21. package/.pi/extensions/takomi-context-manager/types.ts +77 -77
  22. package/.pi/extensions/takomi-runtime/command-text.ts +10 -2
  23. package/.pi/extensions/takomi-runtime/commands.ts +78 -5
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +187 -145
  25. package/.pi/extensions/takomi-subagents/native-render.ts +41 -41
  26. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +35 -32
  27. package/.pi/extensions/takomi-subagents/run-types.ts +25 -25
  28. package/.pi/prompts/build-prompt.md +259 -259
  29. package/.pi/prompts/design-prompt.md +95 -95
  30. package/.pi/prompts/genesis-prompt.md +140 -140
  31. package/.pi/prompts/prime-prompt.md +110 -110
  32. package/.pi/themes/takomi-aurora.json +88 -88
  33. package/README.md +2 -4
  34. package/assets/.agent/skills/21st-dev-components/SKILL.md +244 -244
  35. package/assets/.agent/skills/anti-gravity/SKILL.md +112 -0
  36. package/assets/.agent/skills/gemini/SKILL.md +14 -223
  37. package/assets/.agent/skills/git-commit-generation/SKILL.md +195 -0
  38. package/package.json +1 -1
  39. package/src/pi-takomi-core/validation.ts +135 -135
@@ -1,35 +1,35 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "typebox";
3
- import type { ContextManagerState } from "./state";
4
- import { syncReportLedger } from "./state";
5
- import { renderPolicies, renderPolicyManifest } from "./policy-registry";
6
-
7
- export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState): void {
8
- pi.registerTool({
9
- name: "policy_manifest",
10
- label: "Policy Manifest",
11
- description: "Return descriptions for available context policy packs without loading full policy content.",
12
- promptSnippet: "Show available context policy pack descriptions",
13
- parameters: Type.Object({ policies: Type.Optional(Type.Array(Type.String({ description: "Policy name to inspect" }))) }),
14
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15
- state.report.cwd = ctx.cwd;
16
- state.report.toolCalls.policyManifest += 1;
17
- return { content: [{ type: "text", text: renderPolicyManifest(state.policies, params.policies ?? []) }], details: { requested: params.policies ?? [...state.policies.keys()] } };
18
- },
19
- });
20
-
21
- pi.registerTool({
22
- name: "policy_load",
23
- label: "Policy Load",
24
- description: "Load one or more context policy packs required before sensitive tools such as takomi_subagent.",
25
- promptSnippet: "Load policy packs required before sensitive tool calls",
26
- parameters: Type.Object({ policies: Type.Array(Type.String({ description: "Policy pack name to load" })) }),
27
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
28
- state.report.cwd = ctx.cwd;
29
- state.report.toolCalls.policyLoad += 1;
30
- const text = renderPolicies(state.policies, state.loadedPolicies, params.policies);
31
- syncReportLedger(state);
32
- return { content: [{ type: "text", text }], details: { requested: params.policies, loadedPolicies: [...state.loadedPolicies].sort() } };
33
- },
34
- });
35
- }
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { ContextManagerState } from "./state";
4
+ import { syncReportLedger } from "./state";
5
+ import { renderPolicies, renderPolicyManifest } from "./policy-registry";
6
+
7
+ export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState): void {
8
+ pi.registerTool({
9
+ name: "policy_manifest",
10
+ label: "Policy Manifest",
11
+ description: "Return descriptions for available context policy packs without loading full policy content.",
12
+ promptSnippet: "Show available context policy pack descriptions",
13
+ parameters: Type.Object({ policies: Type.Optional(Type.Array(Type.String({ description: "Policy name to inspect" }))) }),
14
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15
+ state.report.cwd = ctx.cwd;
16
+ state.report.toolCalls.policyManifest += 1;
17
+ return { content: [{ type: "text", text: renderPolicyManifest(state.policies, params.policies ?? []) }], details: { requested: params.policies ?? [...state.policies.keys()] } };
18
+ },
19
+ });
20
+
21
+ pi.registerTool({
22
+ name: "policy_load",
23
+ label: "Policy Load",
24
+ description: "Load one or more context policy packs required before sensitive tools such as takomi_subagent.",
25
+ promptSnippet: "Load policy packs required before sensitive tool calls",
26
+ parameters: Type.Object({ policies: Type.Array(Type.String({ description: "Policy pack name to load" })) }),
27
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
28
+ state.report.cwd = ctx.cwd;
29
+ state.report.toolCalls.policyLoad += 1;
30
+ const text = renderPolicies(state.policies, state.loadedPolicies, params.policies);
31
+ syncReportLedger(state);
32
+ return { content: [{ type: "text", text }], details: { requested: params.policies, loadedPolicies: [...state.loadedPolicies].sort() } };
33
+ },
34
+ });
35
+ }
@@ -1,39 +1,39 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { ContextManagerConfig } from "./types";
3
- import type { ContextManagerState } from "./state";
4
- import { recordBlocked, syncReportLedger } from "./state";
5
- import { renderPolicies } from "./policy-registry";
6
-
7
- function renderPolicyGateBlock(toolName: string, missing: string[], policyText: string): string {
8
- return [
9
- `Blocked ${toolName}: required policy context had not been loaded yet.`,
10
- "",
11
- "The required policy context is provided below and has now been marked as loaded for this session.",
12
- "Retry the original tool call now, following the policy.",
13
- "",
14
- "Required policies:",
15
- ...missing.map((policy) => `- ${policy}`),
16
- "",
17
- "Loaded policy context:",
18
- policyText,
19
- ].join("\n");
20
- }
21
-
22
- export function installPrerequisiteGates(pi: ExtensionAPI, state: ContextManagerState, getConfig: () => ContextManagerConfig): void {
23
- pi.on("tool_call", async (event, ctx) => {
24
- state.report.cwd = ctx.cwd;
25
- const prereqs = getConfig().toolPrerequisites[event.toolName] ?? [];
26
-
27
- for (const prereq of prereqs) {
28
- if (prereq.type !== "policies") continue;
29
- const missing = prereq.policies.filter((policy) => !state.loadedPolicies.has(policy));
30
- if (missing.length === 0) continue;
31
-
32
- const policyText = renderPolicies(state.policies, state.loadedPolicies, missing);
33
- syncReportLedger(state);
34
- const reason = renderPolicyGateBlock(event.toolName, missing, policyText);
35
- recordBlocked(state, event.toolName, reason);
36
- return { block: true, reason };
37
- }
38
- });
39
- }
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { ContextManagerConfig } from "./types";
3
+ import type { ContextManagerState } from "./state";
4
+ import { recordBlocked, syncReportLedger } from "./state";
5
+ import { renderPolicies } from "./policy-registry";
6
+
7
+ function renderPolicyGateBlock(toolName: string, missing: string[], policyText: string): string {
8
+ return [
9
+ `Blocked ${toolName}: required policy context had not been loaded yet.`,
10
+ "",
11
+ "The required policy context is provided below and has now been marked as loaded for this session.",
12
+ "Retry the original tool call now, following the policy.",
13
+ "",
14
+ "Required policies:",
15
+ ...missing.map((policy) => `- ${policy}`),
16
+ "",
17
+ "Loaded policy context:",
18
+ policyText,
19
+ ].join("\n");
20
+ }
21
+
22
+ export function installPrerequisiteGates(pi: ExtensionAPI, state: ContextManagerState, getConfig: () => ContextManagerConfig): void {
23
+ pi.on("tool_call", async (event, ctx) => {
24
+ state.report.cwd = ctx.cwd;
25
+ const prereqs = getConfig().toolPrerequisites[event.toolName] ?? [];
26
+
27
+ for (const prereq of prereqs) {
28
+ if (prereq.type !== "policies") continue;
29
+ const missing = prereq.policies.filter((policy) => !state.loadedPolicies.has(policy));
30
+ if (missing.length === 0) continue;
31
+
32
+ const policyText = renderPolicies(state.policies, state.loadedPolicies, missing);
33
+ syncReportLedger(state);
34
+ const reason = renderPolicyGateBlock(event.toolName, missing, policyText);
35
+ recordBlocked(state, event.toolName, reason);
36
+ return { block: true, reason };
37
+ }
38
+ });
39
+ }
@@ -1,100 +1,100 @@
1
- import type { CandidateContext, ContextManagerConfig, SkillRecord } from "./types";
2
- import { renderCandidateHint } from "./context-router";
3
- import { sortedSkills } from "./skill-registry";
4
-
5
- function renderSkillNames(skills: SkillRecord[]): string {
6
- return ["Available skills (names only):", ...skills.map((skill) => `- ${skill.name}`)].join("\n");
7
- }
8
-
9
- function renderSkillDisplay(skills: SkillRecord[], candidates: CandidateContext[], config: ContextManagerConfig): string {
10
- const mode = config.skillDisplay.mode;
11
- const countLine = skills.length === 0 ? "Skills: none discovered." : `Skills: ${skills.length} available.`;
12
-
13
- if (mode === "hidden") return `${countLine} Use skill_index if this task may need a skill.`;
14
- if (mode === "all-names") return skills.length === 0 ? countLine : renderSkillNames(skills);
15
- if (mode === "auto" && skills.length <= config.skillDisplay.maxVisibleSkillNames) {
16
- return skills.length === 0 ? countLine : renderSkillNames(skills);
17
- }
18
-
19
- const candidateHint = renderCandidateHint(candidates);
20
- return [countLine, candidateHint || "Use skill_index if a specialized skill may help."].join("\n");
21
- }
22
-
23
- function renderProgressiveRule(config: ContextManagerConfig): string {
24
- if (!config.skillDisplay.alwaysShowToolInstructions) return "";
25
- return [
26
- "Skill loading:",
27
- "- Skills are optional capability packs that give you special instructions/tools for specialized, repetitive tasks.",
28
- "- Do not preload skill descriptions into the prompt.",
29
- "- Use skill_index to view all skill names when needed.",
30
- "- For uncertain matches, request skill_manifest for likely skills; manifests include descriptions and locations.",
31
- "- If a skill is clearly relevant or the user names it directly, use skill_load without requesting a manifest first.",
32
- "- Load full skill instructions only for skills you will actually use.",
33
- "",
34
- "Policy loading:",
35
- "- Model/subagent/lifecycle policies are lazy-loaded policy packs.",
36
- "- The active routing policy may come from the project or the bundled Takomi harness default.",
37
- "- Use policy_manifest or policy_load when you need to inspect or quote a policy explicitly.",
38
- "- If takomi_subagent is blocked for missing policy context, the gate has already loaded the required policy for this session; retry the tool call and follow it.",
39
- ].join("\n");
40
- }
41
-
42
- function compactHeavyPolicyBlocks(prompt: string, config: ContextManagerConfig): { prompt: string; removedSections: string[] } {
43
- let next = prompt;
44
- const removedSections: string[] = [];
45
- if (config.promptCompaction.compactModelRouting) {
46
- const modelRoutingRegex = /(Project|Bundled) Takomi model routing policy is active\. Apply it when choosing parent\/subagent models and escalation levels:\s*\n\n# Takomi Model Routing Policy[\s\S]*?(?=\nAvailable model context from Pi registry:)/;
47
- if (modelRoutingRegex.test(next)) {
48
- next = next.replace(modelRoutingRegex, [
49
- "Project Takomi model routing policy is available as a lazy-loaded policy pack.",
50
- "The subagent prerequisite gate can provide this policy automatically on first blocked takomi_subagent attempt, then the agent should retry.",
51
- "",
52
- ].join("\n"));
53
- removedSections.push("full model routing policy");
54
- }
55
- }
56
- if (config.promptCompaction.compactModelRegistry) {
57
- const registryRegex = /Available model context from Pi registry:[^\n]*(?:\n|$)/;
58
- if (registryRegex.test(next)) {
59
- next = next.replace(registryRegex, "Available model registry context exists. The subagent policy gate will provide model routing context if needed.\n");
60
- removedSections.push("verbose model registry list");
61
- }
62
- }
63
- return { prompt: next, removedSections };
64
- }
65
-
66
- export function rewritePrompt(systemPrompt: string, skills: Map<string, SkillRecord>, candidates: CandidateContext[], config: ContextManagerConfig): {
67
- prompt: string;
68
- changed: boolean;
69
- removedSections: string[];
70
- warnings: string[];
71
- } {
72
- const warnings: string[] = [];
73
- const removedSections: string[] = [];
74
- let next = systemPrompt;
75
- let changed = false;
76
-
77
- if (config.promptCompaction.compactSkillDescriptions) {
78
- const sorted = sortedSkills(skills);
79
- const replacement = [
80
- renderSkillDisplay(sorted, candidates, config),
81
- renderProgressiveRule(config),
82
- ].filter(Boolean).join("\n\n");
83
- const skillBlockRegex = /<available_skills>[\s\S]*?<\/available_skills>/i;
84
- if (!skillBlockRegex.test(next)) {
85
- warnings.push("No <available_skills> block found; appended progressive skill guidance instead.");
86
- next = `${next}\n\n${replacement}`;
87
- changed = true;
88
- } else {
89
- next = next.replace(skillBlockRegex, replacement);
90
- changed = true;
91
- removedSections.push(`available_skills descriptions (${config.skillDisplay.mode} display)`);
92
- }
93
- }
94
-
95
- const compacted = compactHeavyPolicyBlocks(next, config);
96
- next = compacted.prompt;
97
- removedSections.push(...compacted.removedSections);
98
- changed = changed || compacted.removedSections.length > 0;
99
- return { prompt: next, changed, removedSections, warnings };
100
- }
1
+ import type { CandidateContext, ContextManagerConfig, SkillRecord } from "./types";
2
+ import { renderCandidateHint } from "./context-router";
3
+ import { sortedSkills } from "./skill-registry";
4
+
5
+ function renderSkillNames(skills: SkillRecord[]): string {
6
+ return ["Available skills (names only):", ...skills.map((skill) => `- ${skill.name}`)].join("\n");
7
+ }
8
+
9
+ function renderSkillDisplay(skills: SkillRecord[], candidates: CandidateContext[], config: ContextManagerConfig): string {
10
+ const mode = config.skillDisplay.mode;
11
+ const countLine = skills.length === 0 ? "Skills: none discovered." : `Skills: ${skills.length} available.`;
12
+
13
+ if (mode === "hidden") return `${countLine} Use skill_index if this task may need a skill.`;
14
+ if (mode === "all-names") return skills.length === 0 ? countLine : renderSkillNames(skills);
15
+ if (mode === "auto" && skills.length <= config.skillDisplay.maxVisibleSkillNames) {
16
+ return skills.length === 0 ? countLine : renderSkillNames(skills);
17
+ }
18
+
19
+ const candidateHint = renderCandidateHint(candidates);
20
+ return [countLine, candidateHint || "Use skill_index if a specialized skill may help."].join("\n");
21
+ }
22
+
23
+ function renderProgressiveRule(config: ContextManagerConfig): string {
24
+ if (!config.skillDisplay.alwaysShowToolInstructions) return "";
25
+ return [
26
+ "Skill loading:",
27
+ "- Skills are optional capability packs that give you special instructions/tools for specialized, repetitive tasks.",
28
+ "- Do not preload skill descriptions into the prompt.",
29
+ "- Use skill_index to view all skill names when needed.",
30
+ "- For uncertain matches, request skill_manifest for likely skills; manifests include descriptions and locations.",
31
+ "- If a skill is clearly relevant or the user names it directly, use skill_load without requesting a manifest first.",
32
+ "- Load full skill instructions only for skills you will actually use.",
33
+ "",
34
+ "Policy loading:",
35
+ "- Model/subagent/lifecycle policies are lazy-loaded policy packs.",
36
+ "- The active routing policy may come from the project or the bundled Takomi harness default.",
37
+ "- Use policy_manifest or policy_load when you need to inspect or quote a policy explicitly.",
38
+ "- If takomi_subagent is blocked for missing policy context, the gate has already loaded the required policy for this session; retry the tool call and follow it.",
39
+ ].join("\n");
40
+ }
41
+
42
+ function compactHeavyPolicyBlocks(prompt: string, config: ContextManagerConfig): { prompt: string; removedSections: string[] } {
43
+ let next = prompt;
44
+ const removedSections: string[] = [];
45
+ if (config.promptCompaction.compactModelRouting) {
46
+ const modelRoutingRegex = /(Project|Bundled) Takomi model routing policy is active\. Apply it when choosing parent\/subagent models and escalation levels:\s*\n\n# Takomi Model Routing Policy[\s\S]*?(?=\nAvailable model context from Pi registry:)/;
47
+ if (modelRoutingRegex.test(next)) {
48
+ next = next.replace(modelRoutingRegex, [
49
+ "Project Takomi model routing policy is available as a lazy-loaded policy pack.",
50
+ "The subagent prerequisite gate can provide this policy automatically on first blocked takomi_subagent attempt, then the agent should retry.",
51
+ "",
52
+ ].join("\n"));
53
+ removedSections.push("full model routing policy");
54
+ }
55
+ }
56
+ if (config.promptCompaction.compactModelRegistry) {
57
+ const registryRegex = /Available model context from Pi registry:[^\n]*(?:\n|$)/;
58
+ if (registryRegex.test(next)) {
59
+ next = next.replace(registryRegex, "Available model registry context exists. The subagent policy gate will provide model routing context if needed.\n");
60
+ removedSections.push("verbose model registry list");
61
+ }
62
+ }
63
+ return { prompt: next, removedSections };
64
+ }
65
+
66
+ export function rewritePrompt(systemPrompt: string, skills: Map<string, SkillRecord>, candidates: CandidateContext[], config: ContextManagerConfig): {
67
+ prompt: string;
68
+ changed: boolean;
69
+ removedSections: string[];
70
+ warnings: string[];
71
+ } {
72
+ const warnings: string[] = [];
73
+ const removedSections: string[] = [];
74
+ let next = systemPrompt;
75
+ let changed = false;
76
+
77
+ if (config.promptCompaction.compactSkillDescriptions) {
78
+ const sorted = sortedSkills(skills);
79
+ const replacement = [
80
+ renderSkillDisplay(sorted, candidates, config),
81
+ renderProgressiveRule(config),
82
+ ].filter(Boolean).join("\n\n");
83
+ const skillBlockRegex = /<available_skills>[\s\S]*?<\/available_skills>/i;
84
+ if (!skillBlockRegex.test(next)) {
85
+ warnings.push("No <available_skills> block found; appended progressive skill guidance instead.");
86
+ next = `${next}\n\n${replacement}`;
87
+ changed = true;
88
+ } else {
89
+ next = next.replace(skillBlockRegex, replacement);
90
+ changed = true;
91
+ removedSections.push(`available_skills descriptions (${config.skillDisplay.mode} display)`);
92
+ }
93
+ }
94
+
95
+ const compacted = compactHeavyPolicyBlocks(next, config);
96
+ next = compacted.prompt;
97
+ removedSections.push(...compacted.removedSections);
98
+ changed = changed || compacted.removedSections.length > 0;
99
+ return { prompt: next, changed, removedSections, warnings };
100
+ }
@@ -1,87 +1,87 @@
1
- import type { SkillRecord } from "./types";
2
-
3
- export function normalizeText(value: string): string {
4
- return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
5
- }
6
-
7
- export function normalizeName(name: string): string {
8
- return name.trim().toLowerCase();
9
- }
10
-
11
- function getString(input: unknown, keys: string[]): string | undefined {
12
- if (!input || typeof input !== "object") return undefined;
13
- const record = input as Record<string, unknown>;
14
- for (const key of keys) {
15
- const value = record[key];
16
- if (typeof value === "string" && value.trim()) return value.trim();
17
- }
18
- return undefined;
19
- }
20
-
21
- export function collectSkillsFromOptions(options: unknown): SkillRecord[] {
22
- if (!options || typeof options !== "object") return [];
23
- const skills = (options as { skills?: unknown }).skills;
24
- const rawList = Array.isArray(skills)
25
- ? skills
26
- : skills && typeof skills === "object"
27
- ? Object.values(skills as Record<string, unknown>)
28
- : [];
29
- return rawList.flatMap((item): SkillRecord[] => {
30
- const name = getString(item, ["name", "id", "title"]);
31
- if (!name) return [];
32
- return [{
33
- name,
34
- description: getString(item, ["description", "summary"]),
35
- location: getString(item, ["location", "path", "file", "skillPath"]),
36
- source: "systemPromptOptions",
37
- }];
38
- });
39
- }
40
-
41
- function decodeXmlEntities(value: string): string {
42
- return value.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
43
- }
44
-
45
- function extractTag(block: string, tag: string): string | undefined {
46
- const match = block.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "i"));
47
- return match?.[1]?.trim() ? decodeXmlEntities(match[1].trim()) : undefined;
48
- }
49
-
50
- export function collectSkillsFromXml(systemPrompt: string): SkillRecord[] {
51
- const root = systemPrompt.match(/<available_skills>([\s\S]*?)<\/available_skills>/i);
52
- if (!root) return [];
53
- const skills: SkillRecord[] = [];
54
- for (const match of root[1].matchAll(/<skill>([\s\S]*?)<\/skill>/gi)) {
55
- const name = extractTag(match[1], "name");
56
- if (!name) continue;
57
- skills.push({ name, description: extractTag(match[1], "description"), location: extractTag(match[1], "location"), source: "xml" });
58
- }
59
- return skills;
60
- }
61
-
62
- export function mergeSkills(records: SkillRecord[]): Map<string, SkillRecord> {
63
- const merged = new Map<string, SkillRecord>();
64
- for (const skill of records) {
65
- const key = normalizeName(skill.name);
66
- const existing = merged.get(key);
67
- merged.set(key, existing ? {
68
- name: existing.name,
69
- description: existing.description ?? skill.description,
70
- location: existing.location ?? skill.location,
71
- source: existing.source === "systemPromptOptions" ? existing.source : skill.source,
72
- } : skill);
73
- }
74
- return merged;
75
- }
76
-
77
- export function sortedSkills(skills: Map<string, SkillRecord>): SkillRecord[] {
78
- return [...skills.values()].sort((a, b) => a.name.localeCompare(b.name));
79
- }
80
-
81
- export function findSkill(skills: Map<string, SkillRecord>, name: string): SkillRecord | undefined {
82
- const key = normalizeName(name);
83
- const exact = skills.get(key);
84
- if (exact) return exact;
85
- const matches = sortedSkills(skills).filter((skill) => normalizeName(skill.name).includes(key));
86
- return matches.length === 1 ? matches[0] : undefined;
87
- }
1
+ import type { SkillRecord } from "./types";
2
+
3
+ export function normalizeText(value: string): string {
4
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ export function normalizeName(name: string): string {
8
+ return name.trim().toLowerCase();
9
+ }
10
+
11
+ function getString(input: unknown, keys: string[]): string | undefined {
12
+ if (!input || typeof input !== "object") return undefined;
13
+ const record = input as Record<string, unknown>;
14
+ for (const key of keys) {
15
+ const value = record[key];
16
+ if (typeof value === "string" && value.trim()) return value.trim();
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ export function collectSkillsFromOptions(options: unknown): SkillRecord[] {
22
+ if (!options || typeof options !== "object") return [];
23
+ const skills = (options as { skills?: unknown }).skills;
24
+ const rawList = Array.isArray(skills)
25
+ ? skills
26
+ : skills && typeof skills === "object"
27
+ ? Object.values(skills as Record<string, unknown>)
28
+ : [];
29
+ return rawList.flatMap((item): SkillRecord[] => {
30
+ const name = getString(item, ["name", "id", "title"]);
31
+ if (!name) return [];
32
+ return [{
33
+ name,
34
+ description: getString(item, ["description", "summary"]),
35
+ location: getString(item, ["location", "path", "file", "skillPath"]),
36
+ source: "systemPromptOptions",
37
+ }];
38
+ });
39
+ }
40
+
41
+ function decodeXmlEntities(value: string): string {
42
+ return value.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
43
+ }
44
+
45
+ function extractTag(block: string, tag: string): string | undefined {
46
+ const match = block.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "i"));
47
+ return match?.[1]?.trim() ? decodeXmlEntities(match[1].trim()) : undefined;
48
+ }
49
+
50
+ export function collectSkillsFromXml(systemPrompt: string): SkillRecord[] {
51
+ const root = systemPrompt.match(/<available_skills>([\s\S]*?)<\/available_skills>/i);
52
+ if (!root) return [];
53
+ const skills: SkillRecord[] = [];
54
+ for (const match of root[1].matchAll(/<skill>([\s\S]*?)<\/skill>/gi)) {
55
+ const name = extractTag(match[1], "name");
56
+ if (!name) continue;
57
+ skills.push({ name, description: extractTag(match[1], "description"), location: extractTag(match[1], "location"), source: "xml" });
58
+ }
59
+ return skills;
60
+ }
61
+
62
+ export function mergeSkills(records: SkillRecord[]): Map<string, SkillRecord> {
63
+ const merged = new Map<string, SkillRecord>();
64
+ for (const skill of records) {
65
+ const key = normalizeName(skill.name);
66
+ const existing = merged.get(key);
67
+ merged.set(key, existing ? {
68
+ name: existing.name,
69
+ description: existing.description ?? skill.description,
70
+ location: existing.location ?? skill.location,
71
+ source: existing.source === "systemPromptOptions" ? existing.source : skill.source,
72
+ } : skill);
73
+ }
74
+ return merged;
75
+ }
76
+
77
+ export function sortedSkills(skills: Map<string, SkillRecord>): SkillRecord[] {
78
+ return [...skills.values()].sort((a, b) => a.name.localeCompare(b.name));
79
+ }
80
+
81
+ export function findSkill(skills: Map<string, SkillRecord>, name: string): SkillRecord | undefined {
82
+ const key = normalizeName(name);
83
+ const exact = skills.get(key);
84
+ if (exact) return exact;
85
+ const matches = sortedSkills(skills).filter((skill) => normalizeName(skill.name).includes(key));
86
+ return matches.length === 1 ? matches[0] : undefined;
87
+ }