@adeildo/pi-skills 0.0.0 → 5.5.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 +28 -0
- package/README.md +42 -2
- package/package.json +41 -3
- package/src/complete.ts +63 -0
- package/src/index.ts +106 -0
- package/src/render.ts +45 -0
- package/src/skills.ts +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 adeildo
|
|
4
|
+
|
|
5
|
+
Parts of this package are ported from rpiv-ask-user-question
|
|
6
|
+
(https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-ask-user-question):
|
|
7
|
+
the tool's schema, the validation and line-ending cleanup, the preview layout and box,
|
|
8
|
+
and the RPC dialog walker. Those parts are also:
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 juicesharp
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">@adeildo/pi-skills</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/@adeildo/pi-skills"><img src="https://img.shields.io/npm/v/@adeildo/pi-skills" alt="npm"></a>
|
|
5
|
+
<a href="https://pi.dev"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffelipeadeildo%2Fpi-harness%2Fmain%2Fpackage.json&query=%24.devDependencies%5B%22%40earendil-works%2Fpi-coding-agent%22%5D&label=pi%20SDK&color=6E56CF" alt="pi SDK"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<strong>Call your skills from anywhere in a message, several at once.</strong><br>
|
|
10
|
+
The model reads each one in full. The chat shows a small chip where you typed it.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><code>pi install npm:@adeildo/pi-skills</code></p>
|
|
14
|
+
|
|
15
|
+
## What changes
|
|
16
|
+
|
|
17
|
+
This piece brings no skills of its own. It changes how you call the ones pi already loaded, from your skill folders or from packages.
|
|
18
|
+
|
|
19
|
+
Pi expands `/skill:name` only at the start of a message, and only one. With this piece, any `/skill:name` in the text counts:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
use /skill:simplify, then go over the docs with /skill:unslop
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Before the message, the model gets one block per skill, the same `<skill>` block pi writes for `/skill:name`. Your text reaches it as you typed it. In the chat, each reference shows as `[skill] name` on a violet background, the way pi labels a folded skill. The blocks stay folded until `Ctrl+O`, which expands tool output, shows them in full.
|
|
26
|
+
|
|
27
|
+
A reference counts when it names a skill pi loaded and follows a space, the start of a line or an opening bracket. Inside backticks it stays text.
|
|
28
|
+
|
|
29
|
+
## Typing a reference
|
|
30
|
+
|
|
31
|
+
A `/` past the start of the message opens a list of the skills alone, narrowed as you type, the way pi's palette does at the start. Picking one writes `/skill:name` and a space. The list opens on `/` in the editor of [`@adeildo/pi-look`](../look), which also colors each reference while you type. In any other editor, `Tab` after the `/` opens it.
|
|
32
|
+
|
|
33
|
+
At the start of the message, `/` still opens pi's own palette, with the commands and the skills.
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
- A message that starts with `/skill:` goes out with a space before it, so pi does not expand that skill a second time. The chat does not draw the space.
|
|
38
|
+
- While the model works, the skills queue with the message, as a steer or a follow-up.
|
|
39
|
+
- If a skill file cannot be read, the message goes without that skill and a warning says why.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeildo/pi-skills",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.5.0",
|
|
4
|
+
"description": "Call the skills you already have from anywhere in a message, several at once. The model reads each one in full, and the chat shows a small chip where you typed it.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-coding-agent",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"skills"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/felipeadeildo/pi-harness/tree/main/packages/skills#readme",
|
|
13
|
+
"bugs": "https://github.com/felipeadeildo/pi-harness/issues",
|
|
5
14
|
"license": "MIT",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "adeildo",
|
|
17
|
+
"email": "contato@felipeadeildo.com",
|
|
18
|
+
"url": "https://github.com/felipeadeildo"
|
|
19
|
+
},
|
|
6
20
|
"repository": {
|
|
7
21
|
"type": "git",
|
|
8
22
|
"url": "git+https://github.com/felipeadeildo/pi-harness.git",
|
|
9
23
|
"directory": "packages/skills"
|
|
10
24
|
},
|
|
11
|
-
"
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./src/index.ts"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@adeildo/pi-kit": "5.5.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@earendil-works/pi-ai": "*",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*"
|
|
44
|
+
},
|
|
45
|
+
"pi": {
|
|
46
|
+
"extensions": [
|
|
47
|
+
"./src/index.ts"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
12
50
|
}
|
package/src/complete.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Pi's palette opens on `/` only at the start of a message. This one lists only the skills, anywhere
|
|
2
|
+
// else in the text: the look's editor opens it on `/`, and Tab opens it in any editor.
|
|
3
|
+
import { SKILL_REF, skillQueryAt } from "@adeildo/pi-kit";
|
|
4
|
+
import {
|
|
5
|
+
type AutocompleteItem,
|
|
6
|
+
type AutocompleteProvider,
|
|
7
|
+
fuzzyFilter,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
import type { Skill } from "./skills.ts";
|
|
11
|
+
|
|
12
|
+
export function completeSkills(
|
|
13
|
+
current: AutocompleteProvider,
|
|
14
|
+
skills: () => readonly Skill[],
|
|
15
|
+
): AutocompleteProvider {
|
|
16
|
+
return {
|
|
17
|
+
triggerCharacters: current.triggerCharacters,
|
|
18
|
+
|
|
19
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
20
|
+
const typing = skillQueryAt(lines, cursorLine, cursorCol);
|
|
21
|
+
if (typing !== undefined) {
|
|
22
|
+
const items = matching(skills(), typing.query);
|
|
23
|
+
if (items.length > 0) return { items, prefix: typing.typed };
|
|
24
|
+
}
|
|
25
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
29
|
+
if (!item.value.startsWith(SKILL_REF) || !prefix.startsWith("/"))
|
|
30
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
31
|
+
|
|
32
|
+
const line = lines[cursorLine] ?? "";
|
|
33
|
+
const start = cursorCol - prefix.length;
|
|
34
|
+
const rest = line.slice(cursorCol);
|
|
35
|
+
const after = rest.startsWith(" ") ? rest : ` ${rest}`;
|
|
36
|
+
return {
|
|
37
|
+
lines: lines.with(cursorLine, line.slice(0, start) + item.value + after),
|
|
38
|
+
cursorLine,
|
|
39
|
+
cursorCol: start + item.value.length + 1,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
44
|
+
if (skillQueryAt(lines, cursorLine, cursorCol) !== undefined) return true;
|
|
45
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** As pi's palette matches: by the bare name first, then by `skill:name`. */
|
|
51
|
+
function matching(skills: readonly Skill[], query: string): AutocompleteItem[] {
|
|
52
|
+
const byName = fuzzyFilter([...skills], query, (skill) => skill.name);
|
|
53
|
+
const byRef = fuzzyFilter(
|
|
54
|
+
skills.filter((skill) => !byName.includes(skill)),
|
|
55
|
+
query,
|
|
56
|
+
(skill) => `skill:${skill.name}`,
|
|
57
|
+
);
|
|
58
|
+
return [...byName, ...byRef].map((skill) => ({
|
|
59
|
+
value: `${SKILL_REF}${skill.name}`,
|
|
60
|
+
label: `skill:${skill.name}`,
|
|
61
|
+
description: skill.description,
|
|
62
|
+
}));
|
|
63
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addSkillNames,
|
|
3
|
+
createApp,
|
|
4
|
+
defineFeature,
|
|
5
|
+
drawSkillRefs,
|
|
6
|
+
type FeatureScope,
|
|
7
|
+
referencedSkills,
|
|
8
|
+
SKILL_NAMES,
|
|
9
|
+
SKILL_REF,
|
|
10
|
+
} from "@adeildo/pi-kit";
|
|
11
|
+
import type { TextContent } from "@earendil-works/pi-ai";
|
|
12
|
+
import type {
|
|
13
|
+
ExtensionAPI,
|
|
14
|
+
ExtensionContext,
|
|
15
|
+
InputEvent,
|
|
16
|
+
InputEventResult,
|
|
17
|
+
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
import { completeSkills } from "./complete.ts";
|
|
20
|
+
import { chip, renderSkills } from "./render.ts";
|
|
21
|
+
import { loadedSkills, type Skill, skillBlock } from "./skills.ts";
|
|
22
|
+
|
|
23
|
+
export { loadedSkills, skillBlock, type Skill } from "./skills.ts";
|
|
24
|
+
|
|
25
|
+
/** The message that carries the skills a message named, right before it. */
|
|
26
|
+
export const SKILLS_MESSAGE = "pi-skills";
|
|
27
|
+
|
|
28
|
+
export const skills = defineFeature({
|
|
29
|
+
id: "skills",
|
|
30
|
+
description: "Call the skills you already have from anywhere in a message, several at once",
|
|
31
|
+
setup(scope) {
|
|
32
|
+
let session: ExtensionContext | undefined;
|
|
33
|
+
|
|
34
|
+
scope.onSessionStart((ctx) => {
|
|
35
|
+
session = ctx;
|
|
36
|
+
if (ctx.mode === "tui")
|
|
37
|
+
ctx.ui.addAutocompleteProvider((current) =>
|
|
38
|
+
completeSkills(current, () => loadedSkills(scope)),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
scope.onShutdown(() => {
|
|
42
|
+
session = undefined;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
scope.events.on(SKILL_NAMES, (data: unknown) => addSkillNames(data, namesOf(scope)));
|
|
46
|
+
|
|
47
|
+
scope.on("input", (event, ctx) => expand(scope, ctx, event));
|
|
48
|
+
|
|
49
|
+
scope.registerMessageRenderer(SKILLS_MESSAGE, (message, options) =>
|
|
50
|
+
renderSkills(message, options.expanded),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
scope.registerMarkdownTransformer((markdown, context) => {
|
|
54
|
+
const theme = session?.mode === "tui" ? session.ui.theme : undefined;
|
|
55
|
+
if (context.messageType !== "user" || theme === undefined) return markdown;
|
|
56
|
+
const typed = markdown.startsWith(` ${SKILL_REF}`) ? markdown.slice(1) : markdown;
|
|
57
|
+
return drawSkillRefs(typed, namesOf(scope), (name) => chip(theme, name));
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The skills the message names go first, in one message the model reads and the chat folds. The
|
|
64
|
+
* text stays as you typed it, but for a space before a leading `/skill:`, which keeps pi from
|
|
65
|
+
* expanding that one again. Markdown does not draw the space.
|
|
66
|
+
*/
|
|
67
|
+
function expand(
|
|
68
|
+
scope: FeatureScope,
|
|
69
|
+
ctx: ExtensionContext,
|
|
70
|
+
event: InputEvent,
|
|
71
|
+
): InputEventResult | undefined {
|
|
72
|
+
const loaded = loadedSkills(scope);
|
|
73
|
+
const named = referencedSkills(
|
|
74
|
+
event.text,
|
|
75
|
+
loaded.map((skill) => skill.name),
|
|
76
|
+
);
|
|
77
|
+
const blocks = named.flatMap((name) => blockOf(ctx, loaded, name));
|
|
78
|
+
if (blocks.length === 0) return undefined;
|
|
79
|
+
|
|
80
|
+
scope.sendMessage(
|
|
81
|
+
{ customType: SKILLS_MESSAGE, content: blocks, display: true, details: { skills: named } },
|
|
82
|
+
event.streamingBehavior && { deliverAs: event.streamingBehavior },
|
|
83
|
+
);
|
|
84
|
+
if (!event.text.startsWith(SKILL_REF)) return undefined;
|
|
85
|
+
return { action: "transform", text: ` ${event.text}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function blockOf(ctx: ExtensionContext, loaded: readonly Skill[], name: string): TextContent[] {
|
|
89
|
+
const skill = loaded.find((entry) => entry.name === name);
|
|
90
|
+
if (skill === undefined) return [];
|
|
91
|
+
try {
|
|
92
|
+
return [{ type: "text", text: skillBlock(skill) }];
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
95
|
+
ctx.ui.notify(`pi-skills: could not read ${name}: ${reason}`, "warning");
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function namesOf(pi: Pick<ExtensionAPI, "getCommands">): string[] {
|
|
101
|
+
return loadedSkills(pi).map((skill) => skill.name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default function piSkills(pi: ExtensionAPI): void {
|
|
105
|
+
createApp(pi, { name: "pi-skills" }).use(skills).build();
|
|
106
|
+
}
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getMarkdownTheme,
|
|
3
|
+
type MessageRenderer,
|
|
4
|
+
parseSkillBlock,
|
|
5
|
+
SkillInvocationMessageComponent,
|
|
6
|
+
type Theme,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { type Component, Container, Spacer } from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
type SkillsMessage = Parameters<MessageRenderer>[0];
|
|
11
|
+
|
|
12
|
+
const NBSP = "\u00a0";
|
|
13
|
+
|
|
14
|
+
/** A skill where you typed it, drawn as pi draws a folded skill: `[skill] name`. */
|
|
15
|
+
export function chip(theme: Theme, name: string): string {
|
|
16
|
+
const label = theme.fg("customMessageLabel", "\x1b[1m[skill]\x1b[22m");
|
|
17
|
+
const inside = `${NBSP}${label}${NBSP}${theme.fg("customMessageText", name)}${NBSP}`;
|
|
18
|
+
// The line keeps its own colors after the chip.
|
|
19
|
+
return (
|
|
20
|
+
theme.bg("customMessageBg", inside) +
|
|
21
|
+
theme.getBgAnsi("userMessageBg") +
|
|
22
|
+
theme.getFgAnsi("userMessageText")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Nothing until the tool output expands, then each skill in full, as pi shows one. */
|
|
27
|
+
export function renderSkills(message: SkillsMessage, expanded: boolean): Component {
|
|
28
|
+
const shown = new Container();
|
|
29
|
+
if (!expanded) return shown;
|
|
30
|
+
|
|
31
|
+
for (const text of textsOf(message)) {
|
|
32
|
+
const block = parseSkillBlock(text);
|
|
33
|
+
if (block === null) continue;
|
|
34
|
+
if (shown.children.length > 0) shown.addChild(new Spacer(1));
|
|
35
|
+
const skill = new SkillInvocationMessageComponent(block, getMarkdownTheme());
|
|
36
|
+
skill.setExpanded(true);
|
|
37
|
+
shown.addChild(skill);
|
|
38
|
+
}
|
|
39
|
+
return shown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function textsOf(message: SkillsMessage): string[] {
|
|
43
|
+
if (typeof message.content === "string") return [message.content];
|
|
44
|
+
return message.content.flatMap((part) => (part.type === "text" ? [part.text] : []));
|
|
45
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { type ExtensionAPI, stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export interface Skill {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PREFIX = "skill:";
|
|
13
|
+
|
|
14
|
+
/** The skills pi loaded for this session, which `/skill:` can name. */
|
|
15
|
+
export function loadedSkills(pi: Pick<ExtensionAPI, "getCommands">): Skill[] {
|
|
16
|
+
return pi
|
|
17
|
+
.getCommands()
|
|
18
|
+
.filter((command) => command.source === "skill" && command.name.startsWith(PREFIX))
|
|
19
|
+
.map((command) => ({
|
|
20
|
+
name: command.name.slice(PREFIX.length),
|
|
21
|
+
description: command.description,
|
|
22
|
+
filePath: command.sourceInfo.path,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The skill as the model reads it, in the block pi writes for `/skill:name`. */
|
|
27
|
+
export function skillBlock(skill: Skill): string {
|
|
28
|
+
const body = stripFrontmatter(readFileSync(skill.filePath, "utf8")).trim();
|
|
29
|
+
return `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${dirname(skill.filePath)}.\n\n${body}\n</skill>`;
|
|
30
|
+
}
|