takomi 2.1.44 → 2.5.1
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/.pi/README.md +9 -8
- package/.pi/agents/architect.md +2 -2
- package/.pi/agents/designer.md +2 -2
- package/.pi/agents/worker.md +32 -0
- package/.pi/extensions/oauth-router/commands.ts +66 -35
- package/.pi/extensions/oauth-router/index.ts +51 -7
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
- package/.pi/extensions/takomi-context-manager/index.ts +3 -2
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +50 -117
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
- package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
- package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
- package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
- package/.pi/extensions/takomi-context-manager/types.ts +6 -0
- package/.pi/extensions/takomi-runtime/command-text.ts +5 -4
- package/.pi/extensions/takomi-runtime/commands.ts +58 -25
- package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
- package/.pi/extensions/takomi-runtime/index.ts +385 -124
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +262 -326
- package/.pi/extensions/takomi-runtime/profile.ts +9 -8
- package/.pi/extensions/takomi-runtime/routing-policy.ts +97 -66
- package/.pi/extensions/takomi-runtime/shared.ts +7 -30
- package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
- package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
- package/.pi/extensions/takomi-subagents/agents.ts +9 -0
- package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
- package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
- package/.pi/extensions/takomi-subagents/index.ts +50 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +62 -47
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
- package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
- package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +198 -29
- package/.pi/settings.json +39 -36
- package/.pi/takomi/model-routing.md +288 -3
- package/.pi/takomi-profile.json +54 -50
- package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
- package/package.json +4 -3
- package/src/pi-takomi-core/orchestration.ts +39 -21
- package/src/pi-takomi-core/routing.ts +8 -8
- package/src/pi-takomi-core/types.ts +35 -5
- package/src/pi-takomi-core/workflows.ts +86 -45
- package/src/skills-catalog.js +2 -202
- package/src/skills-installer.js +4 -1
|
@@ -1,42 +1,93 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/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
|
-
import { persistReportSnapshot, restoreReportFromSession } from "./session-state";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
state.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/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
|
+
import { persistReportSnapshot, restoreReportFromSession } from "./session-state";
|
|
7
|
+
import { renderCompactCard, renderExpandedMarkdown, renderToolCall, resultText } from "./tool-renderers";
|
|
8
|
+
import { normalizeName } from "./skill-registry";
|
|
9
|
+
|
|
10
|
+
function requestedPolicies(state: ContextManagerState, names: string[] | undefined): string[] {
|
|
11
|
+
return names?.length ? names : [...state.policies.values()].map((policy) => policy.name).sort((a, b) => a.localeCompare(b, "en"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function missingPolicies(state: ContextManagerState, names: string[]): string[] {
|
|
15
|
+
return names.filter((name) => !state.policies.has(normalizeName(name)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState): void {
|
|
19
|
+
pi.registerTool({
|
|
20
|
+
name: "policy_manifest",
|
|
21
|
+
label: "Policy Manifest",
|
|
22
|
+
description: "Return descriptions for available context policy packs without loading full policy content.",
|
|
23
|
+
promptSnippet: "Show available context policy pack descriptions",
|
|
24
|
+
parameters: Type.Object({ policies: Type.Optional(Type.Array(Type.String({ description: "Policy name to inspect" }))) }),
|
|
25
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
26
|
+
restoreReportFromSession(state, ctx);
|
|
27
|
+
state.report.timestamp = new Date().toISOString();
|
|
28
|
+
state.report.cwd = ctx.cwd;
|
|
29
|
+
state.report.toolCalls.policyManifest += 1;
|
|
30
|
+
persistReportSnapshot(pi, state, "policy_manifest");
|
|
31
|
+
const requested = requestedPolicies(state, params.policies);
|
|
32
|
+
const missing = missingPolicies(state, requested);
|
|
33
|
+
return { content: [{ type: "text", text: renderPolicyManifest(state.policies, params.policies ?? []) }], details: { requested, found: requested.length - missing.length, missing } };
|
|
34
|
+
},
|
|
35
|
+
renderCall(args, theme) {
|
|
36
|
+
return renderToolCall("policy_manifest", args.policies?.length ? `${args.policies.length} requested` : "all policies", theme);
|
|
37
|
+
},
|
|
38
|
+
renderResult(result, { expanded }, theme) {
|
|
39
|
+
const details = result.details as { requested?: string[]; found?: number; missing?: string[] } | undefined;
|
|
40
|
+
const requested = details?.requested ?? [];
|
|
41
|
+
const found = details?.found ?? 0;
|
|
42
|
+
const missing = details?.missing ?? [];
|
|
43
|
+
const status = missing.length ? "warning" : requested.length ? "success" : "pending";
|
|
44
|
+
const summary = missing.length ? `${found} available · ${missing.length} unavailable` : requested.length ? `${found} policies available` : "no policies discovered";
|
|
45
|
+
const metadata = missing.length ? `Missing: ${missing.join(", ")}` : `${requested.length} requested`;
|
|
46
|
+
if (!expanded) return renderCompactCard({ status, title: "Policy manifest", summary, metadata }, theme);
|
|
47
|
+
return renderExpandedMarkdown({ status, title: "Policy manifest", summary, metadata: [metadata], markdown: resultText(result) }, theme);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
pi.registerTool({
|
|
52
|
+
name: "policy_load",
|
|
53
|
+
label: "Policy Load",
|
|
54
|
+
description: "Load one or more context policy packs required before sensitive tools such as takomi_subagent.",
|
|
55
|
+
promptSnippet: "Load policy packs required before sensitive tool calls",
|
|
56
|
+
parameters: Type.Object({ policies: Type.Array(Type.String({ description: "Policy pack name to load" })) }),
|
|
57
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
58
|
+
restoreReportFromSession(state, ctx);
|
|
59
|
+
state.report.timestamp = new Date().toISOString();
|
|
60
|
+
state.report.cwd = ctx.cwd;
|
|
61
|
+
state.report.toolCalls.policyLoad += 1;
|
|
62
|
+
const text = renderPolicies(state.policies, state.loadedPolicies, params.policies);
|
|
63
|
+
syncReportLedger(state);
|
|
64
|
+
persistReportSnapshot(pi, state, "policy_load");
|
|
65
|
+
const missing = missingPolicies(state, params.policies);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text }],
|
|
68
|
+
details: {
|
|
69
|
+
requested: params.policies,
|
|
70
|
+
loadedPolicies: [...state.loadedPolicies].sort(),
|
|
71
|
+
loadedCount: params.policies.length - missing.length,
|
|
72
|
+
missing,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
renderCall(args, theme) {
|
|
77
|
+
return renderToolCall("policy_load", `${args.policies.length} requested`, theme);
|
|
78
|
+
},
|
|
79
|
+
renderResult(result, { expanded }, theme) {
|
|
80
|
+
const details = result.details as { requested?: string[]; loadedCount?: number; missing?: string[] } | undefined;
|
|
81
|
+
const requested = details?.requested ?? [];
|
|
82
|
+
const loadedCount = details?.loadedCount ?? 0;
|
|
83
|
+
const missing = details?.missing ?? [];
|
|
84
|
+
const status = missing.length || requested.length === 0 ? "warning" : "success";
|
|
85
|
+
const summary = requested.length === 0
|
|
86
|
+
? "no policies requested"
|
|
87
|
+
: missing.length ? `${loadedCount} loaded · ${missing.length} unavailable` : `${loadedCount} policies loaded`;
|
|
88
|
+
const metadata = missing.length ? `Missing: ${missing.join(", ")}` : `${requested.length} requested`;
|
|
89
|
+
if (!expanded) return renderCompactCard({ status, title: "Policy load", summary, metadata }, theme);
|
|
90
|
+
return renderExpandedMarkdown({ status, title: "Policy load", summary, metadata: [metadata], markdown: resultText(result) }, theme);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type SkillCategory = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
color: string;
|
|
5
|
+
description: string;
|
|
6
|
+
skills: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const CORE_SKILLS: string[];
|
|
10
|
+
export const SKILL_CATEGORIES: SkillCategory[];
|
|
11
|
+
export function getSkillCategory(skillName: string): string | undefined;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export const CORE_SKILLS = [
|
|
2
|
+
'takomi',
|
|
3
|
+
'sync-docs',
|
|
4
|
+
'code-review',
|
|
5
|
+
'security-audit',
|
|
6
|
+
'optimize-agent-context',
|
|
7
|
+
'agent-recovery',
|
|
8
|
+
'avoid-feature-creep',
|
|
9
|
+
'ai-sdk',
|
|
10
|
+
'git-commit-generation',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const SKILL_CATEGORIES = [
|
|
14
|
+
{
|
|
15
|
+
id: 'core',
|
|
16
|
+
title: 'Core / Recommended',
|
|
17
|
+
color: 'cyan',
|
|
18
|
+
description: 'Essential skills for efficient Takomi usage.',
|
|
19
|
+
skills: CORE_SKILLS,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'developer',
|
|
23
|
+
title: 'Developer / Frameworks',
|
|
24
|
+
color: 'blue',
|
|
25
|
+
description: 'Framework, repo, and developer workflow helpers.',
|
|
26
|
+
skills: [
|
|
27
|
+
'ai-sdk',
|
|
28
|
+
'nextjs-standards',
|
|
29
|
+
'context7',
|
|
30
|
+
'monorepo-management',
|
|
31
|
+
'upgrading-expo',
|
|
32
|
+
'github-ops',
|
|
33
|
+
'git-worktree',
|
|
34
|
+
'git-commit-generation',
|
|
35
|
+
'pr-comment-fix',
|
|
36
|
+
'shared-resend-portfolio',
|
|
37
|
+
'jules',
|
|
38
|
+
'gemini',
|
|
39
|
+
'anti-gravity',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'security',
|
|
44
|
+
title: 'Security / Review',
|
|
45
|
+
color: 'red',
|
|
46
|
+
description: 'Security, audit, and review workflows.',
|
|
47
|
+
skills: [
|
|
48
|
+
'security-audit',
|
|
49
|
+
'audit-website',
|
|
50
|
+
'code-review',
|
|
51
|
+
'jstar-reviewer',
|
|
52
|
+
'convex-security-audit',
|
|
53
|
+
'convex-security-check',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'convex',
|
|
58
|
+
title: 'Convex',
|
|
59
|
+
color: 'green',
|
|
60
|
+
description: 'Convex framework skills and best practices.',
|
|
61
|
+
skills: [
|
|
62
|
+
'convex',
|
|
63
|
+
'convex-agents',
|
|
64
|
+
'convex-best-practices',
|
|
65
|
+
'convex-component-authoring',
|
|
66
|
+
'convex-cron-jobs',
|
|
67
|
+
'convex-file-storage',
|
|
68
|
+
'convex-functions',
|
|
69
|
+
'convex-http-actions',
|
|
70
|
+
'convex-migrations',
|
|
71
|
+
'convex-realtime',
|
|
72
|
+
'convex-schema-validator',
|
|
73
|
+
'convex-security-audit',
|
|
74
|
+
'convex-security-check',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'frontend',
|
|
79
|
+
title: 'Frontend / UI',
|
|
80
|
+
color: 'magenta',
|
|
81
|
+
description: 'Frontend implementation, UI/UX, components, and testing.',
|
|
82
|
+
skills: [
|
|
83
|
+
'frontend-design',
|
|
84
|
+
'web-design-guidelines',
|
|
85
|
+
'building-native-ui',
|
|
86
|
+
'ui-ux-pro-max',
|
|
87
|
+
'component-analysis',
|
|
88
|
+
'21st-dev-components',
|
|
89
|
+
'stitch',
|
|
90
|
+
'webapp-testing',
|
|
91
|
+
'figma',
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'docs-office',
|
|
96
|
+
title: 'Docs / Office / Extraction',
|
|
97
|
+
color: 'yellow',
|
|
98
|
+
description: 'Document formats, extraction, and README support.',
|
|
99
|
+
skills: [
|
|
100
|
+
'pdf',
|
|
101
|
+
'docx',
|
|
102
|
+
'pptx',
|
|
103
|
+
'xlsx',
|
|
104
|
+
'high-fidelity-extraction',
|
|
105
|
+
'crafting-effective-readmes',
|
|
106
|
+
'exam-creator-skill',
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'marketing',
|
|
111
|
+
title: 'Marketing / SEO / Copy',
|
|
112
|
+
color: 'green',
|
|
113
|
+
description: 'Marketing, SEO, naming, pricing, and social strategy.',
|
|
114
|
+
skills: [
|
|
115
|
+
'copywriting',
|
|
116
|
+
'marketing-ideas',
|
|
117
|
+
'pricing-strategy',
|
|
118
|
+
'programmatic-seo',
|
|
119
|
+
'seo-ready',
|
|
120
|
+
'social-content',
|
|
121
|
+
'twitter-automation',
|
|
122
|
+
'google-trends',
|
|
123
|
+
'domain-name-brainstormer',
|
|
124
|
+
'global-brand-namer',
|
|
125
|
+
'youtube-pipeline',
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'ai-media',
|
|
130
|
+
title: 'AI Media / Content Creation',
|
|
131
|
+
color: 'magenta',
|
|
132
|
+
description: 'Optional AI media/content skills. Not installed by default.',
|
|
133
|
+
skills: [
|
|
134
|
+
'ai-avatar-video',
|
|
135
|
+
'ai-marketing-videos',
|
|
136
|
+
'ai-podcast-creation',
|
|
137
|
+
'ai-product-photography',
|
|
138
|
+
'ai-social-media-content',
|
|
139
|
+
'ai-voice-cloning',
|
|
140
|
+
'takomi-flow',
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'creative-video',
|
|
145
|
+
title: 'Creative / Video / Art',
|
|
146
|
+
color: 'blue',
|
|
147
|
+
description: 'Creative visuals, video, animation, and art workflows.',
|
|
148
|
+
skills: [
|
|
149
|
+
'algorithmic-art',
|
|
150
|
+
'blender-mcp-scene-director',
|
|
151
|
+
'takomi-flow',
|
|
152
|
+
'remotion',
|
|
153
|
+
'remotion-real-ui-video',
|
|
154
|
+
'youtube-pipeline',
|
|
155
|
+
'ai-avatar-video',
|
|
156
|
+
'ai-marketing-videos',
|
|
157
|
+
'photo-book-builder',
|
|
158
|
+
'hyperframes',
|
|
159
|
+
'general-video',
|
|
160
|
+
'motion-graphics',
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'hyperframes',
|
|
165
|
+
title: 'HyperFrames',
|
|
166
|
+
color: 'magenta',
|
|
167
|
+
description: 'HTML-native video creation and animation framework.',
|
|
168
|
+
skills: [
|
|
169
|
+
'hyperframes',
|
|
170
|
+
'hyperframes-core',
|
|
171
|
+
'hyperframes-keyframes',
|
|
172
|
+
'hyperframes-animation',
|
|
173
|
+
'hyperframes-creative',
|
|
174
|
+
'hyperframes-media',
|
|
175
|
+
'hyperframes-registry',
|
|
176
|
+
'hyperframes-cli',
|
|
177
|
+
'remotion-to-hyperframes',
|
|
178
|
+
'media-use',
|
|
179
|
+
'embedded-captions',
|
|
180
|
+
'faceless-explainer',
|
|
181
|
+
'general-video',
|
|
182
|
+
'motion-graphics',
|
|
183
|
+
'music-to-video',
|
|
184
|
+
'pr-to-video',
|
|
185
|
+
'product-launch-video',
|
|
186
|
+
'slideshow',
|
|
187
|
+
'talking-head-recut',
|
|
188
|
+
'website-to-video',
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'skill-building',
|
|
193
|
+
title: 'Skill Building / Prompting / Orchestration',
|
|
194
|
+
color: 'cyan',
|
|
195
|
+
description: 'Skill authoring, prompt engineering, and optional orchestration helpers.',
|
|
196
|
+
skills: [
|
|
197
|
+
'skill-creator',
|
|
198
|
+
'prompt-engineering',
|
|
199
|
+
'subagent-driven-development',
|
|
200
|
+
'spawn-task',
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a bundled skill to its canonical installer category. Some skills are
|
|
207
|
+
* intentionally listed in multiple browsing categories; the first catalog
|
|
208
|
+
* entry is the stable primary taxonomy used by install manifests and reports.
|
|
209
|
+
*/
|
|
210
|
+
export function getSkillCategory(skillName) {
|
|
211
|
+
return SKILL_CATEGORIES.find((category) => category.skills.includes(skillName))?.id;
|
|
212
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { getSkillCategory } from "./skill-categories";
|
|
4
5
|
import type { SkillRecord } from "./types";
|
|
5
6
|
|
|
6
7
|
export function normalizeText(value: string): string {
|
|
@@ -35,7 +36,10 @@ export function collectSkillsFromOptions(options: unknown): SkillRecord[] {
|
|
|
35
36
|
return [{
|
|
36
37
|
name,
|
|
37
38
|
description: getString(item, ["description", "summary"]),
|
|
38
|
-
location: getString(item, ["location", "path", "file", "skillPath"]),
|
|
39
|
+
location: getString(item, ["filePath", "location", "path", "file", "skillPath"]),
|
|
40
|
+
category: getString(item, ["category", "group", "taxonomy"]),
|
|
41
|
+
sourceCategory: getString(item, ["sourceCategory", "installerCategory"]),
|
|
42
|
+
packageName: getString(item, ["package", "packageName", "sourceSlug", "source"]),
|
|
39
43
|
source: "systemPromptOptions",
|
|
40
44
|
}];
|
|
41
45
|
});
|
|
@@ -57,7 +61,15 @@ export function collectSkillsFromXml(systemPrompt: string): SkillRecord[] {
|
|
|
57
61
|
for (const match of root[1].matchAll(/<skill>([\s\S]*?)<\/skill>/gi)) {
|
|
58
62
|
const name = extractTag(match[1], "name");
|
|
59
63
|
if (!name) continue;
|
|
60
|
-
skills.push({
|
|
64
|
+
skills.push({
|
|
65
|
+
name,
|
|
66
|
+
description: extractTag(match[1], "description"),
|
|
67
|
+
location: extractTag(match[1], "location"),
|
|
68
|
+
category: extractTag(match[1], "category") ?? extractTag(match[1], "group"),
|
|
69
|
+
sourceCategory: extractTag(match[1], "source_category") ?? extractTag(match[1], "installer_category"),
|
|
70
|
+
packageName: extractTag(match[1], "package") ?? extractTag(match[1], "source"),
|
|
71
|
+
source: "xml",
|
|
72
|
+
});
|
|
61
73
|
}
|
|
62
74
|
return skills;
|
|
63
75
|
}
|
|
@@ -71,14 +83,95 @@ export function mergeSkills(records: SkillRecord[]): Map<string, SkillRecord> {
|
|
|
71
83
|
name: existing.name,
|
|
72
84
|
description: existing.description ?? skill.description,
|
|
73
85
|
location: existing.location ?? skill.location,
|
|
86
|
+
category: existing.category ?? skill.category,
|
|
87
|
+
sourceCategory: existing.sourceCategory ?? skill.sourceCategory,
|
|
88
|
+
packageName: existing.packageName ?? skill.packageName,
|
|
74
89
|
source: existing.source === "systemPromptOptions" ? existing.source : skill.source,
|
|
75
90
|
} : skill);
|
|
76
91
|
}
|
|
77
92
|
return merged;
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
export function compareSkillText(a: string, b: string): number {
|
|
96
|
+
return normalizeName(a).localeCompare(normalizeName(b), "en") || a.localeCompare(b, "en");
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
export function sortedSkills(skills: Map<string, SkillRecord>): SkillRecord[] {
|
|
81
|
-
return [...skills.values()].sort((a, b) => a.name
|
|
100
|
+
return [...skills.values()].sort((a, b) => compareSkillText(a.name, b.name));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type SkillCategoryGroup = { category: string; skills: SkillRecord[] };
|
|
104
|
+
|
|
105
|
+
/** Safe, renderer-only skill fields. Keep discovery/source details out of tool results. */
|
|
106
|
+
export type SkillRenderProjection = Pick<SkillRecord, "name" | "description">;
|
|
107
|
+
export type SkillIndexRenderGroup = { category: string; skills: SkillRenderProjection[] };
|
|
108
|
+
|
|
109
|
+
function categorySlug(value: string | undefined): string | undefined {
|
|
110
|
+
const normalized = value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
111
|
+
return normalized || undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pathTaxonomyCategory(location: string | undefined): string | undefined {
|
|
115
|
+
if (!location) return undefined;
|
|
116
|
+
const segments = location.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
117
|
+
const skillsIndex = segments.map((segment) => segment.toLowerCase()).lastIndexOf("skills");
|
|
118
|
+
if (skillsIndex < 0) return undefined;
|
|
119
|
+
// A categorized filesystem layout is .../skills/<category>/<skill>/SKILL.md.
|
|
120
|
+
// A normal flat skill root (.../skills/<skill>/SKILL.md) intentionally falls through.
|
|
121
|
+
const afterSkills = segments.slice(skillsIndex + 1);
|
|
122
|
+
return afterSkills.length >= 3 ? categorySlug(afterSkills[0]) : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function packageSlug(location: string | undefined): string | undefined {
|
|
126
|
+
if (!location) return undefined;
|
|
127
|
+
const segments = location.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
128
|
+
const nodeModulesIndex = segments.map((segment) => segment.toLowerCase()).lastIndexOf("node_modules");
|
|
129
|
+
if (nodeModulesIndex < 0) return undefined;
|
|
130
|
+
const first = segments[nodeModulesIndex + 1];
|
|
131
|
+
if (!first) return undefined;
|
|
132
|
+
if (first.startsWith("@")) {
|
|
133
|
+
const second = segments[nodeModulesIndex + 2];
|
|
134
|
+
return second ? categorySlug(`${first.slice(1)}-${second}`) : categorySlug(first.slice(1));
|
|
135
|
+
}
|
|
136
|
+
return categorySlug(first);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Category precedence is deterministic and does not infer from a skill name:
|
|
141
|
+
* explicit skill metadata → installer/source taxonomy → path/package metadata
|
|
142
|
+
* → uncategorized.
|
|
143
|
+
*/
|
|
144
|
+
export function skillCategory(skill: SkillRecord): string {
|
|
145
|
+
return categorySlug(skill.category)
|
|
146
|
+
?? categorySlug(skill.sourceCategory)
|
|
147
|
+
?? pathTaxonomyCategory(skill.location)
|
|
148
|
+
?? categorySlug(skill.packageName)
|
|
149
|
+
?? packageSlug(skill.location)
|
|
150
|
+
?? "uncategorized";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function groupedSkills(skills: Iterable<SkillRecord>): SkillCategoryGroup[] {
|
|
154
|
+
const grouped = new Map<string, SkillRecord[]>();
|
|
155
|
+
for (const skill of skills) {
|
|
156
|
+
const category = skillCategory(skill);
|
|
157
|
+
const entries = grouped.get(category) ?? [];
|
|
158
|
+
entries.push(skill);
|
|
159
|
+
grouped.set(category, entries);
|
|
160
|
+
}
|
|
161
|
+
return [...grouped.entries()]
|
|
162
|
+
.sort(([a], [b]) => compareSkillText(a, b))
|
|
163
|
+
.map(([category, entries]) => ({ category, skills: entries.sort((a, b) => compareSkillText(a.name, b.name)) }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Project the registry into the only fields the skill-index renderer needs.
|
|
168
|
+
* Locations and discovery metadata must remain internal to the registry.
|
|
169
|
+
*/
|
|
170
|
+
export function skillIndexRenderGroups(skills: Iterable<SkillRecord>): SkillIndexRenderGroup[] {
|
|
171
|
+
return groupedSkills(skills).map(({ category, skills: entries }) => ({
|
|
172
|
+
category,
|
|
173
|
+
skills: entries.map(({ name, description }) => ({ name, description })),
|
|
174
|
+
}));
|
|
82
175
|
}
|
|
83
176
|
|
|
84
177
|
export function findSkill(skills: Map<string, SkillRecord>, name: string): SkillRecord | undefined {
|
|
@@ -117,7 +210,14 @@ async function readSkillFile(filePath: string): Promise<SkillRecord | undefined>
|
|
|
117
210
|
const name = frontmatter.name?.trim() || path.basename(path.dirname(filePath));
|
|
118
211
|
const description = frontmatter.description?.trim();
|
|
119
212
|
if (!name || !description) return undefined;
|
|
120
|
-
return {
|
|
213
|
+
return {
|
|
214
|
+
name,
|
|
215
|
+
description,
|
|
216
|
+
location: filePath,
|
|
217
|
+
category: frontmatter.category?.trim() || frontmatter.group?.trim(),
|
|
218
|
+
packageName: frontmatter.package?.trim() || frontmatter.source?.trim(),
|
|
219
|
+
source: "filesystem",
|
|
220
|
+
};
|
|
121
221
|
} catch {
|
|
122
222
|
return undefined;
|
|
123
223
|
}
|
|
@@ -175,6 +275,77 @@ async function collectPackageSkillRoots(nodeModulesRoot: string): Promise<string
|
|
|
175
275
|
return roots;
|
|
176
276
|
}
|
|
177
277
|
|
|
278
|
+
export type InstallerTaxonomyEntry = { category: string; skillFilePath: string };
|
|
279
|
+
|
|
280
|
+
export type InstallerTaxonomyOptions = {
|
|
281
|
+
home?: string;
|
|
282
|
+
takomiHome?: string;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
function canonicalPath(filePath: string): string {
|
|
286
|
+
const resolved = path.normalize(path.resolve(filePath));
|
|
287
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Read Takomi's installer ownership registry without mutating or reconciling it.
|
|
292
|
+
* Legacy owned entries without category metadata use the tracked catalog's
|
|
293
|
+
* stable primary category, so upgrades do not require another installer run.
|
|
294
|
+
*/
|
|
295
|
+
export async function readInstallerTaxonomy(options: InstallerTaxonomyOptions = {}): Promise<Map<string, InstallerTaxonomyEntry>> {
|
|
296
|
+
const home = options.home ?? os.homedir();
|
|
297
|
+
const manifestPath = path.join(options.takomiHome ?? process.env.TAKOMI_HOME_DIR ?? path.join(home, ".takomi"), "skills-manifest.json");
|
|
298
|
+
try {
|
|
299
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as {
|
|
300
|
+
targetRoot?: unknown;
|
|
301
|
+
owned?: Record<string, unknown>;
|
|
302
|
+
};
|
|
303
|
+
const targetRoot = typeof manifest.targetRoot === "string" && manifest.targetRoot.trim()
|
|
304
|
+
? manifest.targetRoot
|
|
305
|
+
: path.join(home, ".agents", "skills");
|
|
306
|
+
const taxonomy = new Map<string, InstallerTaxonomyEntry>();
|
|
307
|
+
for (const [name, rawEntry] of Object.entries(manifest.owned ?? {})) {
|
|
308
|
+
if (!rawEntry || (typeof rawEntry !== "object" && typeof rawEntry !== "string")) continue;
|
|
309
|
+
const entry = typeof rawEntry === "object" ? rawEntry as Record<string, unknown> : {};
|
|
310
|
+
const manifestCategory = typeof entry.category === "string" && entry.category.trim()
|
|
311
|
+
? entry.category.trim()
|
|
312
|
+
: undefined;
|
|
313
|
+
const category = manifestCategory ?? getSkillCategory(name);
|
|
314
|
+
if (!category) continue;
|
|
315
|
+
const targetPath = typeof entry.targetPath === "string" && entry.targetPath.trim()
|
|
316
|
+
? entry.targetPath
|
|
317
|
+
: path.join(targetRoot, name);
|
|
318
|
+
taxonomy.set(normalizeName(name), {
|
|
319
|
+
category,
|
|
320
|
+
skillFilePath: canonicalPath(path.join(targetPath, "SKILL.md")),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return taxonomy;
|
|
324
|
+
} catch {
|
|
325
|
+
return new Map();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Enrich records already supplied by Pi using exact name + canonical SKILL.md
|
|
331
|
+
* ownership. This remains independent from filesystem discovery and read-only.
|
|
332
|
+
*/
|
|
333
|
+
export function applyInstallerTaxonomy(skills: SkillRecord[], taxonomy: Map<string, InstallerTaxonomyEntry>): SkillRecord[] {
|
|
334
|
+
return skills.map((skill) => {
|
|
335
|
+
const entry = taxonomy.get(normalizeName(skill.name));
|
|
336
|
+
if (!entry || !skill.location || canonicalPath(skill.location) !== entry.skillFilePath) return skill;
|
|
337
|
+
return { ...skill, sourceCategory: skill.sourceCategory ?? entry.category };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function enrichSkillsWithInstallerTaxonomy(
|
|
342
|
+
skills: SkillRecord[],
|
|
343
|
+
options: InstallerTaxonomyOptions = {},
|
|
344
|
+
): Promise<SkillRecord[]> {
|
|
345
|
+
if (skills.length === 0) return skills;
|
|
346
|
+
return applyInstallerTaxonomy(skills, await readInstallerTaxonomy(options));
|
|
347
|
+
}
|
|
348
|
+
|
|
178
349
|
function ancestorSkillRoots(cwd: string): string[] {
|
|
179
350
|
const roots: string[] = [];
|
|
180
351
|
let current = path.resolve(cwd || process.cwd());
|
|
@@ -190,8 +361,8 @@ function ancestorSkillRoots(cwd: string): string[] {
|
|
|
190
361
|
return roots;
|
|
191
362
|
}
|
|
192
363
|
|
|
193
|
-
export async function discoverSkillsFromFilesystem(cwd = process.cwd()): Promise<SkillRecord[]> {
|
|
194
|
-
const home = os.homedir();
|
|
364
|
+
export async function discoverSkillsFromFilesystem(cwd = process.cwd(), options: InstallerTaxonomyOptions = {}): Promise<SkillRecord[]> {
|
|
365
|
+
const home = options.home ?? os.homedir();
|
|
195
366
|
const roots = [
|
|
196
367
|
{ root: path.join(home, ".pi", "agent", "skills"), directMarkdownFiles: true },
|
|
197
368
|
{ root: path.join(home, ".agents", "skills"), directMarkdownFiles: false },
|
|
@@ -202,6 +373,6 @@ export async function discoverSkillsFromFilesystem(cwd = process.cwd()): Promise
|
|
|
202
373
|
for (const { root, directMarkdownFiles } of roots) {
|
|
203
374
|
for (const file of await collectSkillFiles(root, directMarkdownFiles)) skillFiles.add(file);
|
|
204
375
|
}
|
|
205
|
-
const skills = await Promise.all([...skillFiles].map(readSkillFile));
|
|
206
|
-
return skills
|
|
376
|
+
const skills = (await Promise.all([...skillFiles].map(readSkillFile))).filter((skill): skill is SkillRecord => Boolean(skill));
|
|
377
|
+
return enrichSkillsWithInstallerTaxonomy(skills, options);
|
|
207
378
|
}
|