@xynogen/pix-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/package.json +48 -0
- package/skills/audit.md +48 -0
- package/skills/bootstrap.md +50 -0
- package/skills/brainstorm.md +49 -0
- package/skills/clone.md +22 -0
- package/skills/commit.md +80 -0
- package/skills/debug.md +55 -0
- package/skills/explain.md +47 -0
- package/skills/finish.md +78 -0
- package/skills/handoff.md +121 -0
- package/skills/plan.md +62 -0
- package/skills/readme.md +79 -0
- package/skills/review.md +50 -0
- package/skills/runner.md +402 -0
- package/skills/search.md +47 -0
- package/skills/standup.md +253 -0
- package/skills/suggest.md +45 -0
- package/skills/task.md +46 -0
- package/skills/test.md +47 -0
- package/skills/tldr.md +32 -0
- package/skills/ui.md +36 -0
- package/skills/verify.md +45 -0
- package/src/index.ts +182 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pix-skills — skill loader extension
|
|
3
|
+
*
|
|
4
|
+
* Registers a `read_skill` tool that lets the agent load any bundled skill's
|
|
5
|
+
* full SKILL.md (or flat .md) by name. This is the safe "agent prompts itself"
|
|
6
|
+
* pattern: the agent calls the tool explicitly; no autonomous injection.
|
|
7
|
+
*
|
|
8
|
+
* Also bundles the skills folder so pi auto-loads skill descriptions into the
|
|
9
|
+
* system prompt at startup (names + descriptions only — full content on demand).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
17
|
+
import { Type } from "typebox";
|
|
18
|
+
|
|
19
|
+
// ─── Skill resolution ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Absolute path to this package's skills/ directory. */
|
|
22
|
+
function skillsRoot(): string {
|
|
23
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
24
|
+
return resolve(here, "..", "skills");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SkillEntry {
|
|
28
|
+
name: string;
|
|
29
|
+
/** Absolute path to the SKILL.md or flat .md file. */
|
|
30
|
+
path: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Discover all skills from the package's skills/ directory.
|
|
35
|
+
* Supports two layouts:
|
|
36
|
+
* - flat: skills/commit.md
|
|
37
|
+
* - subdir: skills/commit/SKILL.md
|
|
38
|
+
*/
|
|
39
|
+
function discoverSkills(): SkillEntry[] {
|
|
40
|
+
const root = skillsRoot();
|
|
41
|
+
if (!existsSync(root)) return [];
|
|
42
|
+
|
|
43
|
+
const entries: SkillEntry[] = [];
|
|
44
|
+
|
|
45
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
// Subdirectory layout: skills/<name>/SKILL.md
|
|
48
|
+
const skillMd = join(root, entry.name, "SKILL.md");
|
|
49
|
+
if (existsSync(skillMd)) {
|
|
50
|
+
entries.push({ name: entry.name, path: skillMd });
|
|
51
|
+
}
|
|
52
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
53
|
+
// Flat layout: skills/<name>.md
|
|
54
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
55
|
+
entries.push({ name, path: join(root, entry.name) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Extract the `description` from YAML frontmatter, or null. */
|
|
63
|
+
function extractDescription(content: string): string | null {
|
|
64
|
+
const m = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
65
|
+
if (!m) return null;
|
|
66
|
+
const dm = m[1]!.match(/^description\s*:\s*["']?(.+?)["']?\s*$/m);
|
|
67
|
+
return dm ? dm[1]!.trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Tool registration ────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const ParamsSchema = Type.Object({
|
|
73
|
+
name: Type.Optional(
|
|
74
|
+
Type.String({
|
|
75
|
+
description:
|
|
76
|
+
'Skill name, e.g. "commit", "debug". Omit to list all skills.',
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
full: Type.Optional(
|
|
80
|
+
Type.Boolean({
|
|
81
|
+
default: false,
|
|
82
|
+
description:
|
|
83
|
+
"When true, return the full SKILL.md content. When false (default), return the description only.",
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export default function registerSkillLoader(pi: ExtensionAPI): void {
|
|
89
|
+
pi.registerTool({
|
|
90
|
+
name: "read_skill",
|
|
91
|
+
label: "Read Skill",
|
|
92
|
+
description:
|
|
93
|
+
"Browse and load bundled skills. No args → list all skills with descriptions. name only → description for that skill. name + full=true → full instructions.",
|
|
94
|
+
promptSnippet: "Browse and load bundled skill instructions",
|
|
95
|
+
promptGuidelines: [
|
|
96
|
+
"Call read_skill() with no arguments to list all available skills and their descriptions.",
|
|
97
|
+
"Call read_skill(name=<skill>) to read the description of a specific skill before deciding to load it.",
|
|
98
|
+
"Call read_skill(name=<skill>, full=true) to load the full procedure for a skill before executing it.",
|
|
99
|
+
"Prefer read_skill over the read tool for skills — it resolves the correct path regardless of install location.",
|
|
100
|
+
],
|
|
101
|
+
executionMode: "sequential",
|
|
102
|
+
parameters: ParamsSchema,
|
|
103
|
+
|
|
104
|
+
async execute(_toolCallId, params, _signal) {
|
|
105
|
+
const ok = (text: string) => ({
|
|
106
|
+
content: [{ type: "text" as const, text }],
|
|
107
|
+
details: undefined,
|
|
108
|
+
});
|
|
109
|
+
const fail = (text: string) => ({
|
|
110
|
+
content: [{ type: "text" as const, text }],
|
|
111
|
+
details: undefined,
|
|
112
|
+
isError: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const { name, full } = params as { name?: string; full?: boolean };
|
|
116
|
+
|
|
117
|
+
// No name → list all skills
|
|
118
|
+
if (!name) {
|
|
119
|
+
const skills = discoverSkills();
|
|
120
|
+
if (!skills.length) return ok("No skills found.");
|
|
121
|
+
|
|
122
|
+
const lines = skills.map((s) => {
|
|
123
|
+
try {
|
|
124
|
+
const content = readFileSync(s.path, "utf-8");
|
|
125
|
+
const desc = extractDescription(content);
|
|
126
|
+
return desc ? `${s.name}: ${desc}` : s.name;
|
|
127
|
+
} catch {
|
|
128
|
+
return s.name;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return ok(
|
|
133
|
+
`Available skills (${skills.length}):\n\n${lines.join("\n")}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Resolve skill
|
|
138
|
+
const skills = discoverSkills();
|
|
139
|
+
const entry = skills.find(
|
|
140
|
+
(s) => s.name === name || s.name === name.replace(/\.md$/, ""),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (!entry) {
|
|
144
|
+
const names = skills.map((s) => s.name).join(", ");
|
|
145
|
+
return fail(
|
|
146
|
+
`Skill "${name}" not found. Available: ${names || "(none)"}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const content = readFileSync(entry.path, "utf-8");
|
|
152
|
+
|
|
153
|
+
// full=false (default) → description only
|
|
154
|
+
if (!full) {
|
|
155
|
+
const desc = extractDescription(content);
|
|
156
|
+
return ok(
|
|
157
|
+
desc ? `${entry.name}: ${desc}` : `${entry.name}: (no description)`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// full=true → entire file
|
|
162
|
+
return ok(content);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return fail(
|
|
165
|
+
`Failed to read skill "${name}": ${
|
|
166
|
+
err instanceof Error ? err.message : String(err)
|
|
167
|
+
}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
renderCall(args, theme) {
|
|
173
|
+
const { name, full } = args as { name?: string; full?: boolean };
|
|
174
|
+
const label = name ? `${name}${full ? " (full)" : ""}` : "list";
|
|
175
|
+
return new Text(
|
|
176
|
+
`${theme.fg("toolTitle", theme.bold("read_skill"))} ${theme.fg("muted", label)}`,
|
|
177
|
+
0,
|
|
178
|
+
0,
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|