@agimon-ai/po-skill 0.0.0-alpha.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/dist/apiContracts.cjs +17 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +53 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +13 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/catalog.cjs +186 -0
  10. package/dist/catalog.cjs.map +1 -0
  11. package/dist/catalog.d.mts +66 -0
  12. package/dist/catalog.d.mts.map +1 -0
  13. package/dist/catalog.mjs +181 -0
  14. package/dist/catalog.mjs.map +1 -0
  15. package/dist/extensions/mcp-ui.mjs +31 -0
  16. package/dist/extensions/mcp-ui.mjs.map +1 -0
  17. package/dist/extensions/mcp.cjs +130 -0
  18. package/dist/extensions/mcp.cjs.map +1 -0
  19. package/dist/extensions/mcp.d.mts +5 -0
  20. package/dist/extensions/mcp.d.mts.map +1 -0
  21. package/dist/extensions/mcp.mjs +125 -0
  22. package/dist/extensions/mcp.mjs.map +1 -0
  23. package/dist/extensions/server.cjs +528 -0
  24. package/dist/extensions/server.cjs.map +1 -0
  25. package/dist/extensions/server.d.mts +5 -0
  26. package/dist/extensions/server.d.mts.map +1 -0
  27. package/dist/extensions/server.mjs +519 -0
  28. package/dist/extensions/server.mjs.map +1 -0
  29. package/dist/index.cjs +5 -0
  30. package/dist/index.d.mts +6 -0
  31. package/dist/index.d.mts.map +1 -0
  32. package/dist/index.mjs +2 -0
  33. package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
  34. package/dist/skills-Ccvg1Hcz.mjs +8 -0
  35. package/dist/skills-Ccvg1Hcz.mjs.map +1 -0
  36. package/dist/skills-ComtgYb1.cjs +25 -0
  37. package/dist/skills-ComtgYb1.cjs.map +1 -0
  38. package/llms.txt +15 -0
  39. package/package.json +135 -0
  40. package/src/prompts/po-author-skill/SKILL.md +37 -0
  41. package/src/prompts/po-use-skill/SKILL.md +18 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vuong Ngo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @agimon-ai/po-skill
2
+
3
+ The session skill catalog for [Po](https://www.npmjs.com/package/@agimon-ai/po).
4
+
5
+ Pi loads skills from the directories it is given at startup. Po adds two things on top: skills
6
+ that are discovered lazily rather than loaded eagerly, and a registry where other extensions
7
+ publish their own skill directories.
8
+
9
+ ## Deferred discovery
10
+
11
+ Scanning every skill directory is the slowest thing this package does, so it does not block the
12
+ session. Discovery starts on `session_start` as a numbered generation and is awaited only when
13
+ something needs the result. A reload starts a new generation; a snapshot that arrives from a
14
+ superseded one is discarded rather than shown, which is what keeps a fast reload from printing stale
15
+ diagnostics.
16
+
17
+ ## Contributing skill directories
18
+
19
+ The package provides the session-owned `doom/skill-sources` Cordis service from
20
+ `@agimon-ai/po-core/skills`. Extensions register their directories inside
21
+ `ctx.inject([DOOM_SKILL_SOURCES_SERVICE], ...)`; the returned handle is disposed automatically when
22
+ either the contributor or provider unloads. Contributions are keyed by source, so a replacement
23
+ generation takes the previous package slot without doubling it.
24
+
25
+ ## Help guidance
26
+
27
+ While the Help minor mode is active, this package contributes `po-author-skill` for creating
28
+ and distributing skills and `po-use-skill` for browsing, invoking, and diagnosing them. Both
29
+ prompts are published under `src/prompts` and routed through this package's [`llms.txt`](./llms.txt).
30
+
31
+ ## Installation
32
+
33
+ Po depends on this package and activates it as fixed host core, so a Po install already has
34
+ it. It is not selectable from `.po/modes.yaml`.
35
+
36
+ ## License
37
+
38
+ MIT
39
+
40
+ Plugin entries live in `src/extensions`, host events and commands in routed surfaces, and catalog and discovery logic in named `src/services` folders. `src/exports` contains public helpers and types. The server awaits its async declaration factory to discover named skill commands before registration and startup.
@@ -0,0 +1,17 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region src/schemas/apiContracts.ts
6
+ /** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
7
+ const apiContracts = (0, require("@agimon-ai/po-core/apiContracts").defineApiContract)({
8
+ version: 1,
9
+ http: [],
10
+ sockets: [],
11
+ dynamic: []
12
+ });
13
+ //#endregion
14
+ exports.apiContracts = apiContracts;
15
+ exports.default = apiContracts;
16
+
17
+ //# sourceMappingURL=apiContracts.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiContracts.cjs","names":["defineApiContract"],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */\nexport const apiContracts = defineApiContract({ version: 1, http: [], sockets: [], dynamic: [] });\nexport default apiContracts;\n"],"mappings":";;;;;;AAGA,MAAa,gBAAA,4CAAeA,CAAAA,CAAAA,kBAAAA,CAAkB;CAAE,SAAS;CAAG,MAAM,CAAC;CAAG,SAAS,CAAC;CAAG,SAAS,CAAC;AAAE,CAAC"}
@@ -0,0 +1,53 @@
1
+ //#region src/schemas/apiContracts.d.ts
2
+ /** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
3
+ export declare const apiContracts: {
4
+ version: 1;
5
+ protocols?: Record<string, string> | undefined;
6
+ schemas?: Record<string, boolean | object> | undefined;
7
+ http: {
8
+ id: string;
9
+ scope: "global" | "session" | "workspace";
10
+ basePath?: string | undefined;
11
+ path: string;
12
+ method: "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
13
+ description: string;
14
+ availability?: string | undefined;
15
+ authentication: "device" | "none" | "owner";
16
+ parameters?: {
17
+ name: string;
18
+ in: "header" | "path" | "query";
19
+ required: boolean;
20
+ schema: boolean | object;
21
+ }[] | undefined;
22
+ body?: {
23
+ contentType: string;
24
+ contentTypes?: string[] | undefined;
25
+ required: boolean;
26
+ schema: boolean | object;
27
+ } | undefined;
28
+ responses: Record<string, {
29
+ description: string;
30
+ contentType?: string | undefined;
31
+ schema?: boolean | object | undefined;
32
+ events?: Record<string, boolean | object> | undefined;
33
+ }>;
34
+ }[];
35
+ sockets: {
36
+ id: string;
37
+ scope: "global" | "session" | "workspace";
38
+ description: string;
39
+ availability?: string | undefined;
40
+ service: string;
41
+ member: string;
42
+ direction: "client-to-server" | "server-to-client";
43
+ kind: "channel" | "method" | "state";
44
+ input: boolean | object;
45
+ output?: boolean | object | undefined;
46
+ errors?: boolean | object | undefined;
47
+ }[];
48
+ dynamic: string[];
49
+ gaps?: string[] | undefined;
50
+ };
51
+ //#endregion
52
+ export { apiContracts as default };
53
+ //# sourceMappingURL=apiContracts.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiContracts.d.mts","names":[],"sources":["../src/schemas/apiContracts.ts"],"mappings":";;qBAGa"}
@@ -0,0 +1,13 @@
1
+ import { defineApiContract } from "@agimon-ai/po-core/apiContracts";
2
+ //#region src/schemas/apiContracts.ts
3
+ /** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
4
+ const apiContracts = defineApiContract({
5
+ version: 1,
6
+ http: [],
7
+ sockets: [],
8
+ dynamic: []
9
+ });
10
+ //#endregion
11
+ export { apiContracts, apiContracts as default };
12
+
13
+ //# sourceMappingURL=apiContracts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiContracts.mjs","names":[],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */\nexport const apiContracts = defineApiContract({ version: 1, http: [], sockets: [], dynamic: [] });\nexport default apiContracts;\n"],"mappings":";;;AAGA,MAAa,eAAe,kBAAkB;CAAE,SAAS;CAAG,MAAM,CAAC;CAAG,SAAS,CAAC;CAAG,SAAS,CAAC;AAAE,CAAC"}
@@ -0,0 +1,186 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
3
+ let node_fs = require("node:fs");
4
+ node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
5
+ let node_path = require("node:path");
6
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
7
+ let _agimon_ai_po_config_domains = require("@agimon-ai/po-config/domains");
8
+ let _agimon_ai_po_domain_plugins = require("@agimon-ai/po-domain/plugins");
9
+ let _earendil_works_pi_coding_agent = require("@earendil-works/pi-coding-agent");
10
+ //#region src/services/skillCatalog/index.ts
11
+ const SKILLS_DIRECTORY = "skills";
12
+ const SHARED_SKILLS_OWNER = ".claude/skills";
13
+ /**
14
+ * The tokenizer, loaded on first use rather than at import.
15
+ *
16
+ * `gpt-tokenizer` carries its whole vocabulary and costs about a tenth of a
17
+ * second to load. This module is reachable from the skills extension, which Pi
18
+ * loads at session start, so a top-level import would charge every session for
19
+ * a surface most of them never open.
20
+ */
21
+ let tokenizer;
22
+ async function counter() {
23
+ tokenizer ??= import("gpt-tokenizer");
24
+ return (await tokenizer).countTokens;
25
+ }
26
+ /**
27
+ * Matches a directory against the active set.
28
+ *
29
+ * A domain that lists a plugin subset puts individual skill directories in the
30
+ * set rather than the plugin's `skills` root, so containment has to be checked
31
+ * both ways: the set may hold an ancestor of the directory, or a descendant of
32
+ * it.
33
+ */
34
+ function createActiveMatcher(directories) {
35
+ const active = directories.map((directory) => node_path.default.resolve(directory));
36
+ return (directory) => {
37
+ const resolved = node_path.default.resolve(directory);
38
+ return active.some((entry) => entry === resolved || entry.startsWith(`${resolved}${node_path.default.sep}`) || resolved.startsWith(`${entry}${node_path.default.sep}`));
39
+ };
40
+ }
41
+ function collect(directory, group, owner, isActive, loaded, diagnostics, discovery = "recursive") {
42
+ const result = (0, _earendil_works_pi_coding_agent.loadSkillsFromDir)({
43
+ dir: directory,
44
+ source: owner
45
+ });
46
+ for (const diagnostic of result.diagnostics) diagnostics.push(`${diagnostic.path ?? directory}: ${diagnostic.message}`);
47
+ const root = node_path.default.resolve(directory);
48
+ const kept = result.skills.filter((skill) => {
49
+ const skillDirectory = node_path.default.dirname(skill.filePath);
50
+ if (discovery === "direct-children" && node_path.default.dirname(skillDirectory) !== root) return false;
51
+ return isActive(skillDirectory);
52
+ });
53
+ loaded.push(...kept);
54
+ return kept.map((skill) => ({
55
+ name: skill.name,
56
+ description: skill.description,
57
+ filePath: skill.filePath,
58
+ baseDir: skill.baseDir,
59
+ group,
60
+ owner,
61
+ modelInvocable: !skill.disableModelInvocation
62
+ })).sort((left, right) => left.name.localeCompare(right.name));
63
+ }
64
+ function sortOwners(owners) {
65
+ return owners.sort((left, right) => left.owner.localeCompare(right.owner));
66
+ }
67
+ function collectHelpSkills(skills, loaded, diagnostics) {
68
+ const occupiedNames = new Set(loaded.map((skill) => skill.name));
69
+ const owners = /* @__PURE__ */ new Map();
70
+ const candidates = [...skills].sort((left, right) => left.sourceInfo.source.localeCompare(right.sourceInfo.source) || left.name.localeCompare(right.name) || left.filePath.localeCompare(right.filePath));
71
+ for (const skill of candidates) {
72
+ if (occupiedNames.has(skill.name)) {
73
+ diagnostics.push(`Help ${skill.sourceInfo.source} [HELP_SKILL_COLLISION]: '${skill.name}' was ignored because a normal skill wins.`);
74
+ continue;
75
+ }
76
+ occupiedNames.add(skill.name);
77
+ loaded.push(skill);
78
+ const owner = skill.sourceInfo.source;
79
+ const entries = owners.get(owner) ?? [];
80
+ entries.push({
81
+ name: skill.name,
82
+ description: skill.description,
83
+ filePath: skill.filePath,
84
+ baseDir: skill.baseDir,
85
+ group: "help",
86
+ owner,
87
+ modelInvocable: !skill.disableModelInvocation
88
+ });
89
+ owners.set(owner, entries);
90
+ }
91
+ return sortOwners([...owners.entries()].map(([owner, entries]) => ({
92
+ owner,
93
+ skills: entries.sort((left, right) => left.name.localeCompare(right.name))
94
+ })));
95
+ }
96
+ /**
97
+ * Every skill this session actually loaded, grouped by where it comes from,
98
+ * priced in tokens.
99
+ *
100
+ * Discovery runs over `loadSkillsFromDir` from Pi rather than the harness's own
101
+ * walker so the listing matches what Pi would load, including its ignore-file
102
+ * handling; the same skills are then handed to Pi's `formatSkillsForPrompt` so
103
+ * the always-on figure is the real prompt block rather than a reconstruction of
104
+ * it.
105
+ */
106
+ async function buildSkillCatalog(options) {
107
+ const { repoRoot } = options;
108
+ const isActive = createActiveMatcher(options.activeSkillDirectories);
109
+ const diagnostics = [...options.helpDiagnostics ?? []];
110
+ const loaded = [];
111
+ const extensionOwners = [];
112
+ for (const entry of options.extensionSources) {
113
+ const skills = entry.directories.flatMap((directory) => collect(directory, "extensions", entry.source, () => true, loaded, diagnostics));
114
+ if (skills.length > 0) extensionOwners.push({
115
+ owner: entry.source,
116
+ skills
117
+ });
118
+ }
119
+ const pluginOwners = [];
120
+ const domainPlugins = await (0, _agimon_ai_po_domain_plugins.materializePluginEntries)((0, _agimon_ai_po_config_domains.resolvePluginEntries)(repoRoot, (0, _agimon_ai_po_config_domains.listDomainNames)(repoRoot), []));
121
+ for (const entry of domainPlugins) {
122
+ const directory = node_path.default.join(entry.directory, SKILLS_DIRECTORY);
123
+ if (!node_fs.default.existsSync(directory)) continue;
124
+ const owner = node_path.default.basename(entry.directory);
125
+ const skills = collect(directory, "plugins", owner, isActive, loaded, diagnostics, entry.skillDiscovery);
126
+ if (skills.length > 0) pluginOwners.push({
127
+ owner,
128
+ skills
129
+ });
130
+ }
131
+ const sharedSkills = collect(node_path.default.join(repoRoot, ".claude", SKILLS_DIRECTORY), "default", SHARED_SKILLS_OWNER, isActive, loaded, diagnostics);
132
+ const defaultOwners = sharedSkills.length > 0 ? [{
133
+ owner: SHARED_SKILLS_OWNER,
134
+ skills: sharedSkills
135
+ }] : [];
136
+ const helpOwners = collectHelpSkills(options.helpSkills ?? [], loaded, diagnostics);
137
+ const groups = [
138
+ {
139
+ key: "extensions",
140
+ label: "extensions",
141
+ owners: sortOwners(extensionOwners)
142
+ },
143
+ {
144
+ key: "help",
145
+ label: "Help",
146
+ owners: helpOwners
147
+ },
148
+ {
149
+ key: "plugins",
150
+ label: "plugins",
151
+ owners: sortOwners(pluginOwners)
152
+ },
153
+ {
154
+ key: "default",
155
+ label: "default",
156
+ owners: defaultOwners
157
+ }
158
+ ];
159
+ const countTokens = await counter();
160
+ const prompt = (0, _earendil_works_pi_coding_agent.formatSkillsForPrompt)(loaded);
161
+ const framingTokens = countTokens((0, _earendil_works_pi_coding_agent.formatSkillsForPrompt)([]));
162
+ const marginal = /* @__PURE__ */ new Map();
163
+ for (const skill of loaded) marginal.set(skill.name, Math.max(0, countTokens((0, _earendil_works_pi_coding_agent.formatSkillsForPrompt)([skill])) - framingTokens));
164
+ for (const group of groups) for (const owner of group.owners) for (const skill of owner.skills) {
165
+ const tokens = marginal.get(skill.name);
166
+ if (tokens !== void 0) skill.promptTokens = tokens;
167
+ }
168
+ let bodyTokens = 0;
169
+ for (const skill of loaded) try {
170
+ bodyTokens += countTokens(node_fs.default.readFileSync(skill.filePath, "utf8"));
171
+ } catch (error) {
172
+ diagnostics.push(`${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`);
173
+ }
174
+ return {
175
+ groups,
176
+ skillCount: loaded.length,
177
+ promptTokens: prompt ? countTokens(prompt) : 0,
178
+ bodyTokens,
179
+ diagnostics
180
+ };
181
+ }
182
+ //#endregion
183
+ exports.buildSkillCatalog = buildSkillCatalog;
184
+ exports.counter = counter;
185
+
186
+ //# sourceMappingURL=catalog.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.cjs","names":["path","loadSkillsFromDir","materializePluginEntries","resolvePluginEntries","listDomainNames","fs","formatSkillsForPrompt"],"sources":["../src/services/skillCatalog/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { listDomainNames, type PluginSkillDiscovery, resolvePluginEntries } from '@agimon-ai/po-config/domains';\nimport type { SkillSourceContribution } from '@agimon-ai/po-core/skills';\nimport { materializePluginEntries } from '@agimon-ai/po-domain/plugins';\nimport { formatSkillsForPrompt, loadSkillsFromDir, type Skill } from '@earendil-works/pi-coding-agent';\n\nconst SKILLS_DIRECTORY = 'skills';\nconst SHARED_SKILLS_OWNER = '.claude/skills';\n\nexport type SkillGroupKey = 'extensions' | 'help' | 'plugins' | 'default';\n\nexport interface SkillEntry {\n name: string;\n description: string;\n filePath: string;\n baseDir: string;\n group: SkillGroupKey;\n /** Package name for an extension, plugin directory name, or `.claude/skills`. */\n owner: string;\n modelInvocable: boolean;\n /**\n * What removing this skill would save from the `<available_skills>` block.\n *\n * Marginal rather than proportional: the block has fixed framing that no\n * single skill pays for, so the useful figure is what actually goes away.\n * Bodies are excluded because they load on demand and are not a resting cost.\n */\n promptTokens?: number;\n}\n\nexport interface SkillOwnerNode {\n owner: string;\n skills: SkillEntry[];\n}\n\nexport interface SkillGroup {\n key: SkillGroupKey;\n label: string;\n owners: SkillOwnerNode[];\n}\n\nexport interface SkillCatalog {\n groups: SkillGroup[];\n skillCount: number;\n /** Tokens the `<available_skills>` prompt block costs on every request. */\n promptTokens: number;\n /** Tokens every loaded SKILL.md costs in full, if all of them were read. */\n bodyTokens: number;\n /** Frontmatter and read failures, surfaced instead of thrown. */\n diagnostics: string[];\n}\n\nexport interface SkillCatalogOptions {\n repoRoot: string;\n /** Exact files the current selection resolved to, from `HarnessState.skillDirectories`. */\n activeSkillDirectories: readonly string[];\n extensionSources: readonly SkillSourceContribution[];\n /** Activation-gated wrappers already accepted by the merged active-skill view. */\n helpSkills?: readonly Skill[];\n /** Bounded Help activation and collision diagnostics. */\n helpDiagnostics?: readonly string[];\n}\n\n/**\n * The tokenizer, loaded on first use rather than at import.\n *\n * `gpt-tokenizer` carries its whole vocabulary and costs about a tenth of a\n * second to load. This module is reachable from the skills extension, which Pi\n * loads at session start, so a top-level import would charge every session for\n * a surface most of them never open.\n */\nlet tokenizer: Promise<typeof import('gpt-tokenizer')> | undefined;\n\nexport async function counter(): Promise<(text: string) => number> {\n tokenizer ??= import('gpt-tokenizer');\n return (await tokenizer).countTokens;\n}\n\n/**\n * Matches a directory against the active set.\n *\n * A domain that lists a plugin subset puts individual skill directories in the\n * set rather than the plugin's `skills` root, so containment has to be checked\n * both ways: the set may hold an ancestor of the directory, or a descendant of\n * it.\n */\nfunction createActiveMatcher(directories: readonly string[]): (directory: string) => boolean {\n const active = directories.map((directory) => path.resolve(directory));\n return (directory) => {\n const resolved = path.resolve(directory);\n return active.some(\n (entry) =>\n entry === resolved || entry.startsWith(`${resolved}${path.sep}`) || resolved.startsWith(`${entry}${path.sep}`),\n );\n };\n}\n\nfunction collect(\n directory: string,\n group: SkillGroupKey,\n owner: string,\n isActive: (directory: string) => boolean,\n loaded: Skill[],\n diagnostics: string[],\n discovery: PluginSkillDiscovery = 'recursive',\n): SkillEntry[] {\n const result = loadSkillsFromDir({ dir: directory, source: owner });\n for (const diagnostic of result.diagnostics) {\n diagnostics.push(`${diagnostic.path ?? directory}: ${diagnostic.message}`);\n }\n // Checked per skill, not per root: a subset domain selects some of a plugin's\n // skills and leaves the rest on disk, unloaded and therefore not listed.\n const root = path.resolve(directory);\n const kept = result.skills.filter((skill) => {\n const skillDirectory = path.dirname(skill.filePath);\n if (discovery === 'direct-children' && path.dirname(skillDirectory) !== root) return false;\n return isActive(skillDirectory);\n });\n // Kept in their loaded form as well, because the prompt-block cost is measured\n // by handing them back to Pi's own formatter.\n loaded.push(...kept);\n return kept\n .map((skill) => ({\n name: skill.name,\n description: skill.description,\n filePath: skill.filePath,\n baseDir: skill.baseDir,\n group,\n owner,\n modelInvocable: !skill.disableModelInvocation,\n }))\n .sort((left, right) => left.name.localeCompare(right.name));\n}\n\nfunction sortOwners(owners: SkillOwnerNode[]): SkillOwnerNode[] {\n return owners.sort((left, right) => left.owner.localeCompare(right.owner));\n}\n\nfunction collectHelpSkills(skills: readonly Skill[], loaded: Skill[], diagnostics: string[]): SkillOwnerNode[] {\n const occupiedNames = new Set(loaded.map((skill) => skill.name));\n const owners = new Map<string, SkillEntry[]>();\n const candidates = [...skills].sort(\n (left, right) =>\n left.sourceInfo.source.localeCompare(right.sourceInfo.source) ||\n left.name.localeCompare(right.name) ||\n left.filePath.localeCompare(right.filePath),\n );\n for (const skill of candidates) {\n if (occupiedNames.has(skill.name)) {\n diagnostics.push(\n `Help ${skill.sourceInfo.source} [HELP_SKILL_COLLISION]: '${skill.name}' was ignored because a normal skill wins.`,\n );\n continue;\n }\n occupiedNames.add(skill.name);\n loaded.push(skill);\n const owner = skill.sourceInfo.source;\n const entries = owners.get(owner) ?? [];\n entries.push({\n name: skill.name,\n description: skill.description,\n filePath: skill.filePath,\n baseDir: skill.baseDir,\n group: 'help',\n owner,\n modelInvocable: !skill.disableModelInvocation,\n });\n owners.set(owner, entries);\n }\n return sortOwners(\n [...owners.entries()].map(([owner, entries]) => ({\n owner,\n skills: entries.sort((left, right) => left.name.localeCompare(right.name)),\n })),\n );\n}\n\n/**\n * Every skill this session actually loaded, grouped by where it comes from,\n * priced in tokens.\n *\n * Discovery runs over `loadSkillsFromDir` from Pi rather than the harness's own\n * walker so the listing matches what Pi would load, including its ignore-file\n * handling; the same skills are then handed to Pi's `formatSkillsForPrompt` so\n * the always-on figure is the real prompt block rather than a reconstruction of\n * it.\n */\nexport async function buildSkillCatalog(options: SkillCatalogOptions): Promise<SkillCatalog> {\n const { repoRoot } = options;\n const isActive = createActiveMatcher(options.activeSkillDirectories);\n const diagnostics: string[] = [...(options.helpDiagnostics ?? [])];\n const loaded: Skill[] = [];\n\n const extensionOwners: SkillOwnerNode[] = [];\n for (const entry of options.extensionSources) {\n const skills = entry.directories.flatMap((directory) =>\n collect(directory, 'extensions', entry.source, () => true, loaded, diagnostics),\n );\n if (skills.length > 0) extensionOwners.push({ owner: entry.source, skills });\n }\n\n const pluginOwners: SkillOwnerNode[] = [];\n const domainPlugins = await materializePluginEntries(resolvePluginEntries(repoRoot, listDomainNames(repoRoot), []));\n for (const entry of domainPlugins) {\n const directory = path.join(entry.directory, SKILLS_DIRECTORY);\n if (!fs.existsSync(directory)) continue;\n const owner = path.basename(entry.directory);\n const skills = collect(directory, 'plugins', owner, isActive, loaded, diagnostics, entry.skillDiscovery);\n if (skills.length > 0) pluginOwners.push({ owner, skills });\n }\n\n const sharedDirectory = path.join(repoRoot, '.claude', SKILLS_DIRECTORY);\n const sharedSkills = collect(sharedDirectory, 'default', SHARED_SKILLS_OWNER, isActive, loaded, diagnostics);\n const defaultOwners = sharedSkills.length > 0 ? [{ owner: SHARED_SKILLS_OWNER, skills: sharedSkills }] : [];\n const helpOwners = collectHelpSkills(options.helpSkills ?? [], loaded, diagnostics);\n\n const groups: SkillGroup[] = [\n { key: 'extensions', label: 'extensions', owners: sortOwners(extensionOwners) },\n { key: 'help', label: 'Help', owners: helpOwners },\n { key: 'plugins', label: 'plugins', owners: sortOwners(pluginOwners) },\n { key: 'default', label: 'default', owners: defaultOwners },\n ];\n\n const countTokens = await counter();\n const prompt = formatSkillsForPrompt(loaded);\n const framingTokens = countTokens(formatSkillsForPrompt([]));\n const marginal = new Map<string, number>();\n for (const skill of loaded) {\n marginal.set(skill.name, Math.max(0, countTokens(formatSkillsForPrompt([skill])) - framingTokens));\n }\n for (const group of groups) {\n for (const owner of group.owners) {\n for (const skill of owner.skills) {\n const tokens = marginal.get(skill.name);\n if (tokens !== undefined) skill.promptTokens = tokens;\n }\n }\n }\n let bodyTokens = 0;\n for (const skill of loaded) {\n try {\n bodyTokens += countTokens(fs.readFileSync(skill.filePath, 'utf8'));\n } catch (error) {\n diagnostics.push(`${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n return {\n groups,\n skillCount: loaded.length,\n promptTokens: prompt ? countTokens(prompt) : 0,\n bodyTokens,\n diagnostics,\n };\n}\n\n/** The skills a name resolves to, used to decide what `enter` can invoke. */\nexport function findSkill(catalog: SkillCatalog, name: string): SkillEntry | undefined {\n for (const group of catalog.groups) {\n for (const owner of group.owners) {\n const match = owner.skills.find((skill) => skill.name === name);\n if (match) return match;\n }\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { Skill } from "@earendil-works/pi-coding-agent";
2
+ import { SkillSourceContribution } from "@agimon-ai/po-core/skills";
3
+ //#region src/services/skillCatalog/index.d.ts
4
+ type SkillGroupKey = 'extensions' | 'help' | 'plugins' | 'default';
5
+ interface SkillEntry {
6
+ name: string;
7
+ description: string;
8
+ filePath: string;
9
+ baseDir: string;
10
+ group: SkillGroupKey;
11
+ /** Package name for an extension, plugin directory name, or `.claude/skills`. */
12
+ owner: string;
13
+ modelInvocable: boolean;
14
+ /**
15
+ * What removing this skill would save from the `<available_skills>` block.
16
+ *
17
+ * Marginal rather than proportional: the block has fixed framing that no
18
+ * single skill pays for, so the useful figure is what actually goes away.
19
+ * Bodies are excluded because they load on demand and are not a resting cost.
20
+ */
21
+ promptTokens?: number;
22
+ }
23
+ interface SkillOwnerNode {
24
+ owner: string;
25
+ skills: SkillEntry[];
26
+ }
27
+ interface SkillGroup {
28
+ key: SkillGroupKey;
29
+ label: string;
30
+ owners: SkillOwnerNode[];
31
+ }
32
+ interface SkillCatalog {
33
+ groups: SkillGroup[];
34
+ skillCount: number;
35
+ /** Tokens the `<available_skills>` prompt block costs on every request. */
36
+ promptTokens: number;
37
+ /** Tokens every loaded SKILL.md costs in full, if all of them were read. */
38
+ bodyTokens: number;
39
+ /** Frontmatter and read failures, surfaced instead of thrown. */
40
+ diagnostics: string[];
41
+ }
42
+ interface SkillCatalogOptions {
43
+ repoRoot: string;
44
+ /** Exact files the current selection resolved to, from `HarnessState.skillDirectories`. */
45
+ activeSkillDirectories: readonly string[];
46
+ extensionSources: readonly SkillSourceContribution[];
47
+ /** Activation-gated wrappers already accepted by the merged active-skill view. */
48
+ helpSkills?: readonly Skill[];
49
+ /** Bounded Help activation and collision diagnostics. */
50
+ helpDiagnostics?: readonly string[];
51
+ }
52
+ export declare function counter(): Promise<(text: string) => number>;
53
+ /**
54
+ * Every skill this session actually loaded, grouped by where it comes from,
55
+ * priced in tokens.
56
+ *
57
+ * Discovery runs over `loadSkillsFromDir` from Pi rather than the harness's own
58
+ * walker so the listing matches what Pi would load, including its ignore-file
59
+ * handling; the same skills are then handed to Pi's `formatSkillsForPrompt` so
60
+ * the always-on figure is the real prompt block rather than a reconstruction of
61
+ * it.
62
+ */
63
+ export declare function buildSkillCatalog(options: SkillCatalogOptions): Promise<SkillCatalog>;
64
+ //#endregion
65
+ export type { SkillCatalog, SkillCatalogOptions, SkillEntry, SkillGroup, SkillOwnerNode };
66
+ //# sourceMappingURL=catalog.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.mts","names":[],"sources":["../src/services/skillCatalog/index.ts"],"mappings":";;;KAWY;UAEK;EACf;EACA;EACA;EACA;EACA,OAAO;;EAEP;EACA;;;;;;;;EAQA;;UAGe;EACf;EACA,QAAQ;;UAGO;EACf,KAAK;EACL;EACA,QAAQ;;UAGO;EACf,QAAQ;EACR;;EAEA;;EAEA;;EAEA;;UAGe;EACf;;EAEA;EACA,2BAA2B;;EAE3B,sBAAsB;;EAEtB;;wBAaoB,WAAW,SAAS;;;;;;;;;;;wBAkHpB,kBAAkB,SAAS,sBAAsB,QAAQ"}
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { listDomainNames, resolvePluginEntries } from "@agimon-ai/po-config/domains";
4
+ import { materializePluginEntries } from "@agimon-ai/po-domain/plugins";
5
+ import { formatSkillsForPrompt, loadSkillsFromDir } from "@earendil-works/pi-coding-agent";
6
+ //#region src/services/skillCatalog/index.ts
7
+ const SKILLS_DIRECTORY = "skills";
8
+ const SHARED_SKILLS_OWNER = ".claude/skills";
9
+ /**
10
+ * The tokenizer, loaded on first use rather than at import.
11
+ *
12
+ * `gpt-tokenizer` carries its whole vocabulary and costs about a tenth of a
13
+ * second to load. This module is reachable from the skills extension, which Pi
14
+ * loads at session start, so a top-level import would charge every session for
15
+ * a surface most of them never open.
16
+ */
17
+ let tokenizer;
18
+ async function counter() {
19
+ tokenizer ??= import("gpt-tokenizer");
20
+ return (await tokenizer).countTokens;
21
+ }
22
+ /**
23
+ * Matches a directory against the active set.
24
+ *
25
+ * A domain that lists a plugin subset puts individual skill directories in the
26
+ * set rather than the plugin's `skills` root, so containment has to be checked
27
+ * both ways: the set may hold an ancestor of the directory, or a descendant of
28
+ * it.
29
+ */
30
+ function createActiveMatcher(directories) {
31
+ const active = directories.map((directory) => path.resolve(directory));
32
+ return (directory) => {
33
+ const resolved = path.resolve(directory);
34
+ return active.some((entry) => entry === resolved || entry.startsWith(`${resolved}${path.sep}`) || resolved.startsWith(`${entry}${path.sep}`));
35
+ };
36
+ }
37
+ function collect(directory, group, owner, isActive, loaded, diagnostics, discovery = "recursive") {
38
+ const result = loadSkillsFromDir({
39
+ dir: directory,
40
+ source: owner
41
+ });
42
+ for (const diagnostic of result.diagnostics) diagnostics.push(`${diagnostic.path ?? directory}: ${diagnostic.message}`);
43
+ const root = path.resolve(directory);
44
+ const kept = result.skills.filter((skill) => {
45
+ const skillDirectory = path.dirname(skill.filePath);
46
+ if (discovery === "direct-children" && path.dirname(skillDirectory) !== root) return false;
47
+ return isActive(skillDirectory);
48
+ });
49
+ loaded.push(...kept);
50
+ return kept.map((skill) => ({
51
+ name: skill.name,
52
+ description: skill.description,
53
+ filePath: skill.filePath,
54
+ baseDir: skill.baseDir,
55
+ group,
56
+ owner,
57
+ modelInvocable: !skill.disableModelInvocation
58
+ })).sort((left, right) => left.name.localeCompare(right.name));
59
+ }
60
+ function sortOwners(owners) {
61
+ return owners.sort((left, right) => left.owner.localeCompare(right.owner));
62
+ }
63
+ function collectHelpSkills(skills, loaded, diagnostics) {
64
+ const occupiedNames = new Set(loaded.map((skill) => skill.name));
65
+ const owners = /* @__PURE__ */ new Map();
66
+ const candidates = [...skills].sort((left, right) => left.sourceInfo.source.localeCompare(right.sourceInfo.source) || left.name.localeCompare(right.name) || left.filePath.localeCompare(right.filePath));
67
+ for (const skill of candidates) {
68
+ if (occupiedNames.has(skill.name)) {
69
+ diagnostics.push(`Help ${skill.sourceInfo.source} [HELP_SKILL_COLLISION]: '${skill.name}' was ignored because a normal skill wins.`);
70
+ continue;
71
+ }
72
+ occupiedNames.add(skill.name);
73
+ loaded.push(skill);
74
+ const owner = skill.sourceInfo.source;
75
+ const entries = owners.get(owner) ?? [];
76
+ entries.push({
77
+ name: skill.name,
78
+ description: skill.description,
79
+ filePath: skill.filePath,
80
+ baseDir: skill.baseDir,
81
+ group: "help",
82
+ owner,
83
+ modelInvocable: !skill.disableModelInvocation
84
+ });
85
+ owners.set(owner, entries);
86
+ }
87
+ return sortOwners([...owners.entries()].map(([owner, entries]) => ({
88
+ owner,
89
+ skills: entries.sort((left, right) => left.name.localeCompare(right.name))
90
+ })));
91
+ }
92
+ /**
93
+ * Every skill this session actually loaded, grouped by where it comes from,
94
+ * priced in tokens.
95
+ *
96
+ * Discovery runs over `loadSkillsFromDir` from Pi rather than the harness's own
97
+ * walker so the listing matches what Pi would load, including its ignore-file
98
+ * handling; the same skills are then handed to Pi's `formatSkillsForPrompt` so
99
+ * the always-on figure is the real prompt block rather than a reconstruction of
100
+ * it.
101
+ */
102
+ async function buildSkillCatalog(options) {
103
+ const { repoRoot } = options;
104
+ const isActive = createActiveMatcher(options.activeSkillDirectories);
105
+ const diagnostics = [...options.helpDiagnostics ?? []];
106
+ const loaded = [];
107
+ const extensionOwners = [];
108
+ for (const entry of options.extensionSources) {
109
+ const skills = entry.directories.flatMap((directory) => collect(directory, "extensions", entry.source, () => true, loaded, diagnostics));
110
+ if (skills.length > 0) extensionOwners.push({
111
+ owner: entry.source,
112
+ skills
113
+ });
114
+ }
115
+ const pluginOwners = [];
116
+ const domainPlugins = await materializePluginEntries(resolvePluginEntries(repoRoot, listDomainNames(repoRoot), []));
117
+ for (const entry of domainPlugins) {
118
+ const directory = path.join(entry.directory, SKILLS_DIRECTORY);
119
+ if (!fs.existsSync(directory)) continue;
120
+ const owner = path.basename(entry.directory);
121
+ const skills = collect(directory, "plugins", owner, isActive, loaded, diagnostics, entry.skillDiscovery);
122
+ if (skills.length > 0) pluginOwners.push({
123
+ owner,
124
+ skills
125
+ });
126
+ }
127
+ const sharedSkills = collect(path.join(repoRoot, ".claude", SKILLS_DIRECTORY), "default", SHARED_SKILLS_OWNER, isActive, loaded, diagnostics);
128
+ const defaultOwners = sharedSkills.length > 0 ? [{
129
+ owner: SHARED_SKILLS_OWNER,
130
+ skills: sharedSkills
131
+ }] : [];
132
+ const helpOwners = collectHelpSkills(options.helpSkills ?? [], loaded, diagnostics);
133
+ const groups = [
134
+ {
135
+ key: "extensions",
136
+ label: "extensions",
137
+ owners: sortOwners(extensionOwners)
138
+ },
139
+ {
140
+ key: "help",
141
+ label: "Help",
142
+ owners: helpOwners
143
+ },
144
+ {
145
+ key: "plugins",
146
+ label: "plugins",
147
+ owners: sortOwners(pluginOwners)
148
+ },
149
+ {
150
+ key: "default",
151
+ label: "default",
152
+ owners: defaultOwners
153
+ }
154
+ ];
155
+ const countTokens = await counter();
156
+ const prompt = formatSkillsForPrompt(loaded);
157
+ const framingTokens = countTokens(formatSkillsForPrompt([]));
158
+ const marginal = /* @__PURE__ */ new Map();
159
+ for (const skill of loaded) marginal.set(skill.name, Math.max(0, countTokens(formatSkillsForPrompt([skill])) - framingTokens));
160
+ for (const group of groups) for (const owner of group.owners) for (const skill of owner.skills) {
161
+ const tokens = marginal.get(skill.name);
162
+ if (tokens !== void 0) skill.promptTokens = tokens;
163
+ }
164
+ let bodyTokens = 0;
165
+ for (const skill of loaded) try {
166
+ bodyTokens += countTokens(fs.readFileSync(skill.filePath, "utf8"));
167
+ } catch (error) {
168
+ diagnostics.push(`${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`);
169
+ }
170
+ return {
171
+ groups,
172
+ skillCount: loaded.length,
173
+ promptTokens: prompt ? countTokens(prompt) : 0,
174
+ bodyTokens,
175
+ diagnostics
176
+ };
177
+ }
178
+ //#endregion
179
+ export { buildSkillCatalog, counter };
180
+
181
+ //# sourceMappingURL=catalog.mjs.map