pi-skillful 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/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +35 -0
- package/CONTRIBUTING.md +52 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/SECURITY.md +22 -0
- package/extensions/pi-skillful/index.ts +8 -0
- package/package.json +57 -0
- package/src/config.ts +129 -0
- package/src/extensions/inline-skill-invocation.ts +72 -0
- package/src/extensions/skill-visibility.ts +296 -0
- package/src/skills.ts +38 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning for releases.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Inline `/skill:name` expansion anywhere in a prompt.
|
|
12
|
+
- `/skillful` menu for global/project skill prompt visibility.
|
|
13
|
+
- `skillful.hiddenSkills` settings support.
|
|
14
|
+
- Startup skill-list annotations for visible vs hidden skills.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project follows the Contributor Covenant Code of Conduct, version 2.1.
|
|
4
|
+
|
|
5
|
+
## Our pledge
|
|
6
|
+
|
|
7
|
+
We pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
8
|
+
|
|
9
|
+
## Our standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment include:
|
|
12
|
+
|
|
13
|
+
- demonstrating empathy and kindness toward other people;
|
|
14
|
+
- being respectful of differing opinions, viewpoints, and experiences;
|
|
15
|
+
- giving and gracefully accepting constructive feedback;
|
|
16
|
+
- accepting responsibility and apologizing to those affected by mistakes;
|
|
17
|
+
- focusing on what is best for the overall community.
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
- sexualized language or imagery, and sexual attention or advances;
|
|
22
|
+
- trolling, insulting or derogatory comments, and personal or political attacks;
|
|
23
|
+
- public or private harassment;
|
|
24
|
+
- publishing others' private information without explicit permission;
|
|
25
|
+
- other conduct that could reasonably be considered inappropriate in a professional setting.
|
|
26
|
+
|
|
27
|
+
## Enforcement
|
|
28
|
+
|
|
29
|
+
Project maintainers are responsible for clarifying and enforcing standards of acceptable behavior and may remove, edit, or reject comments, commits, code, issues, and other contributions that are not aligned with this Code of Conduct.
|
|
30
|
+
|
|
31
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainer through GitHub. All reports will be reviewed and investigated promptly and fairly.
|
|
32
|
+
|
|
33
|
+
## Attribution
|
|
34
|
+
|
|
35
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing to `pi-skillful`.
|
|
4
|
+
|
|
5
|
+
## Development setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun run check
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package is source-distributed: Pi loads the TypeScript extension files directly. There is no build step for runtime use.
|
|
13
|
+
|
|
14
|
+
## Local testing
|
|
15
|
+
|
|
16
|
+
Install this checkout into a temporary Pi project:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mkdir -p <test-project>
|
|
20
|
+
cd <test-project>
|
|
21
|
+
pi install -l /path/to/pi-skillful
|
|
22
|
+
pi
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then run `/skillful` inside Pi.
|
|
26
|
+
|
|
27
|
+
For a one-off run without changing settings:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi -e /path/to/pi-skillful/extensions/pi-skillful
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Pull request checklist
|
|
34
|
+
|
|
35
|
+
Before opening a pull request:
|
|
36
|
+
|
|
37
|
+
- Run `bun run check`.
|
|
38
|
+
- Run `bun run pack:dry-run` and confirm the package contents are intentional.
|
|
39
|
+
- Update `README.md` if user-facing behavior changes.
|
|
40
|
+
- Update `CHANGELOG.md` for notable changes.
|
|
41
|
+
- Keep examples and paths generic; do not commit machine-specific paths or credentials.
|
|
42
|
+
|
|
43
|
+
## Coding guidelines
|
|
44
|
+
|
|
45
|
+
- Keep extensions small and focused.
|
|
46
|
+
- Prefer Pi's documented extension APIs and TUI primitives over private internals.
|
|
47
|
+
- Preserve explicit skill invocation even when a skill is hidden from model auto-discovery.
|
|
48
|
+
- Be careful when writing project settings. If `.pi/settings.json` would become empty after removing `skillful`, delete it instead.
|
|
49
|
+
|
|
50
|
+
## Code of conduct
|
|
51
|
+
|
|
52
|
+
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jose Mocito
|
|
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,131 @@
|
|
|
1
|
+
# pi-skillful
|
|
2
|
+
|
|
3
|
+
`pi-skillful` is a [Pi](https://github.com/badlogic/pi-mono) package that improves skill workflows.
|
|
4
|
+
|
|
5
|
+
It currently provides two extensions:
|
|
6
|
+
|
|
7
|
+
- **Inline skill invocation**: invoke one or more skills anywhere in a prompt with `/skill:name`.
|
|
8
|
+
- **Skill prompt visibility**: choose which skills are hidden from the model's automatic skill-discovery prompt, while keeping them explicitly invokable and visibly marked in Pi's startup skill list.
|
|
9
|
+
|
|
10
|
+
> [!WARNING]
|
|
11
|
+
> Pi packages can execute arbitrary code through extensions. Review package source before installing any third-party Pi package.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### Inline skill invocation
|
|
16
|
+
|
|
17
|
+
Vanilla Pi expands `/skill:name` only when it appears at the beginning of the prompt. `pi-skillful` expands known skill markers anywhere in the prompt, including multiple skills:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
Use /skill:code-security and /skill:semgrep to review this change.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The extension replaces each known marker with that skill's `SKILL.md` content before Pi's built-in skill/template expansion runs.
|
|
24
|
+
|
|
25
|
+
### Skill prompt visibility
|
|
26
|
+
|
|
27
|
+
Hide skills from the `<available_skills>` section of the system prompt without editing each skill's `disable-model-invocation` frontmatter.
|
|
28
|
+
|
|
29
|
+
Hidden skills:
|
|
30
|
+
|
|
31
|
+
- are not advertised to the model for automatic skill selection;
|
|
32
|
+
- remain loaded by Pi;
|
|
33
|
+
- remain available for explicit invocation with `/skill:name`, including inline invocation.
|
|
34
|
+
|
|
35
|
+
Configuration is stored under the `skillful` key in Pi settings:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"skillful": {
|
|
40
|
+
"hiddenSkills": ["pdf", "xlsx"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Supported scopes:
|
|
46
|
+
|
|
47
|
+
- Global: `~/.pi/agent/settings.json`
|
|
48
|
+
- Project: `.pi/settings.json`
|
|
49
|
+
|
|
50
|
+
Effective hidden skills are the union of global and project `hiddenSkills`.
|
|
51
|
+
|
|
52
|
+
Open the menu with:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
/skillful
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The menu lists all loaded skills alphabetically. Toggle a skill off to save it in the active scope's `hiddenSkills` list. Use the Global/Project tabs to choose which settings file to edit.
|
|
59
|
+
|
|
60
|
+
Pi's startup `[Skills]` list is also annotated: visible skills are marked with a green dot, and hidden skills are marked with a dim red dot.
|
|
61
|
+
|
|
62
|
+
When the project settings file contains only `skillful` settings and the project `hiddenSkills` list becomes empty, `.pi/settings.json` is deleted instead of leaving an empty settings file behind.
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
Install from GitHub:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pi install git:github.com/jvm/pi-skillful
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Install project-locally with Pi's `-l` flag:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pi install -l git:github.com/jvm/pi-skillful
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
During local development from this repository:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pi install /path/to/pi-skillful
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For a one-off test run without installing:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pi -e /path/to/pi-skillful/extensions/pi-skillful
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
1. Start Pi in a project with this package installed.
|
|
93
|
+
2. Run `/skillful`.
|
|
94
|
+
3. Select the Global or Project tab.
|
|
95
|
+
4. Toggle skills on/off.
|
|
96
|
+
5. Send a prompt normally, or explicitly invoke hidden skills with `/skill:name` anywhere in the prompt.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
Please analyze this using /skill:code-security, then summarize the risk.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
This package is source-distributed. Pi loads the TypeScript extensions directly via its extension loader.
|
|
107
|
+
|
|
108
|
+
Requirements:
|
|
109
|
+
|
|
110
|
+
- Node.js >= 20.6.0
|
|
111
|
+
- Bun for local development commands
|
|
112
|
+
|
|
113
|
+
Common commands:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bun install
|
|
117
|
+
bun run check
|
|
118
|
+
bun run pack:dry-run
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Contributing
|
|
122
|
+
|
|
123
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow and pull request guidelines.
|
|
124
|
+
|
|
125
|
+
## Security
|
|
126
|
+
|
|
127
|
+
Please report security issues privately. See [SECURITY.md](SECURITY.md).
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT. See [LICENSE](LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are provided for the latest released version of `pi-skillful`.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open a public issue for suspected security vulnerabilities.
|
|
10
|
+
|
|
11
|
+
Report privately by contacting the repository maintainer through GitHub. Include:
|
|
12
|
+
|
|
13
|
+
- a description of the issue;
|
|
14
|
+
- steps to reproduce;
|
|
15
|
+
- affected versions or commits, if known;
|
|
16
|
+
- any suggested mitigation.
|
|
17
|
+
|
|
18
|
+
The maintainer will acknowledge reports as soon as practical and coordinate disclosure once a fix or mitigation is available.
|
|
19
|
+
|
|
20
|
+
## Security model
|
|
21
|
+
|
|
22
|
+
`pi-skillful` is a Pi package. Pi extensions execute with the same permissions as the local user running Pi. Users should review installed Pi packages and only install packages from sources they trust.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import inlineSkillInvocation from "../../src/extensions/inline-skill-invocation.js";
|
|
3
|
+
import skillVisibility from "../../src/extensions/skill-visibility.js";
|
|
4
|
+
|
|
5
|
+
export default function piSkillful(pi: ExtensionAPI) {
|
|
6
|
+
inlineSkillInvocation(pi);
|
|
7
|
+
skillVisibility(pi);
|
|
8
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-skillful",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi package with skill invocation and visibility improvements.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "jvm",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/jvm/pi-skillful.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/jvm/pi-skillful/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/jvm/pi-skillful#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"pi",
|
|
20
|
+
"skills"
|
|
21
|
+
],
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./extensions/pi-skillful"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions",
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
"SECURITY.md",
|
|
34
|
+
"CONTRIBUTING.md",
|
|
35
|
+
"CODE_OF_CONDUCT.md"
|
|
36
|
+
],
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
39
|
+
"@mariozechner/pi-tui": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
43
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
44
|
+
"@types/node": "^25.6.0",
|
|
45
|
+
"typescript": "^6.0.3"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"check": "tsc --noEmit",
|
|
49
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20.6.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type SkillfulScope = "global" | "project";
|
|
6
|
+
|
|
7
|
+
export interface SkillfulSettings {
|
|
8
|
+
hiddenSkills: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PiSettingsDocument {
|
|
12
|
+
skillful?: Partial<SkillfulSettings>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SKILLFUL_SETTINGS_KEY = "skillful";
|
|
17
|
+
|
|
18
|
+
export function globalSettingsPath(): string {
|
|
19
|
+
return join(homedir(), ".pi", "agent", "settings.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function projectSettingsPath(cwd: string): string {
|
|
23
|
+
return join(cwd, ".pi", "settings.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function settingsPath(scope: SkillfulScope, cwd: string): string {
|
|
27
|
+
return scope === "global" ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeSkillName(name: string): string {
|
|
31
|
+
return name.trim().replace(/^skill:/, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeSkillNames(names: Iterable<string>): string[] {
|
|
35
|
+
return Array.from(
|
|
36
|
+
new Set(
|
|
37
|
+
Array.from(names)
|
|
38
|
+
.map(normalizeSkillName)
|
|
39
|
+
.filter((name) => name.length > 0)
|
|
40
|
+
.sort(),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readSettingsDocument(path: string): Promise<PiSettingsDocument> {
|
|
46
|
+
try {
|
|
47
|
+
const raw = await readFile(path, "utf-8");
|
|
48
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
49
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as PiSettingsDocument) : {};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function readSkillfulSettings(path: string): Promise<SkillfulSettings> {
|
|
59
|
+
const settings = await readSettingsDocument(path);
|
|
60
|
+
const skillful = settings[SKILLFUL_SETTINGS_KEY];
|
|
61
|
+
const hiddenSkills =
|
|
62
|
+
skillful && typeof skillful === "object" && Array.isArray(skillful.hiddenSkills)
|
|
63
|
+
? skillful.hiddenSkills.filter((name): name is string => typeof name === "string")
|
|
64
|
+
: [];
|
|
65
|
+
|
|
66
|
+
return { hiddenSkills: normalizeSkillNames(hiddenSkills) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function readScopedSkillfulSettings(cwd: string): Promise<Record<SkillfulScope, SkillfulSettings>> {
|
|
70
|
+
const [global, project] = await Promise.all([
|
|
71
|
+
readSkillfulSettings(globalSettingsPath()),
|
|
72
|
+
readSkillfulSettings(projectSettingsPath(cwd)),
|
|
73
|
+
]);
|
|
74
|
+
return { global, project };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readEffectiveHiddenSkills(cwd: string): Promise<Set<string>> {
|
|
78
|
+
const scoped = await readScopedSkillfulSettings(cwd);
|
|
79
|
+
return new Set([...scoped.global.hiddenSkills, ...scoped.project.hiddenSkills]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function writeHiddenSkills(
|
|
83
|
+
scope: SkillfulScope,
|
|
84
|
+
cwd: string,
|
|
85
|
+
hiddenSkills: Iterable<string>,
|
|
86
|
+
): Promise<SkillfulSettings> {
|
|
87
|
+
const path = settingsPath(scope, cwd);
|
|
88
|
+
const document = await readSettingsDocument(path);
|
|
89
|
+
const updated = normalizeSkillNames(hiddenSkills);
|
|
90
|
+
|
|
91
|
+
if (updated.length === 0) {
|
|
92
|
+
delete document[SKILLFUL_SETTINGS_KEY];
|
|
93
|
+
} else {
|
|
94
|
+
document[SKILLFUL_SETTINGS_KEY] = {
|
|
95
|
+
...(document[SKILLFUL_SETTINGS_KEY] && typeof document[SKILLFUL_SETTINGS_KEY] === "object"
|
|
96
|
+
? document[SKILLFUL_SETTINGS_KEY]
|
|
97
|
+
: {}),
|
|
98
|
+
hiddenSkills: updated,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (scope === "project" && Object.keys(document).length === 0) {
|
|
103
|
+
await unlinkIfExists(path);
|
|
104
|
+
return { hiddenSkills: updated };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await mkdir(dirname(path), { recursive: true });
|
|
108
|
+
await writeFile(path, `${JSON.stringify(document, null, 2)}\n`, "utf-8");
|
|
109
|
+
return { hiddenSkills: updated };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function updateHiddenSkills(
|
|
113
|
+
scope: SkillfulScope,
|
|
114
|
+
cwd: string,
|
|
115
|
+
updater: (current: string[]) => string[],
|
|
116
|
+
): Promise<SkillfulSettings> {
|
|
117
|
+
const path = settingsPath(scope, cwd);
|
|
118
|
+
const current = await readSkillfulSettings(path);
|
|
119
|
+
return writeHiddenSkills(scope, cwd, updater(current.hiddenSkills));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function unlinkIfExists(path: string): Promise<void> {
|
|
123
|
+
try {
|
|
124
|
+
await unlink(path);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { readSkillBlock, sourceInfoToSkill, type SkillCommandInfo } from "../skills.js";
|
|
3
|
+
|
|
4
|
+
const SKILL_INVOCATION_PATTERN = /(^|[^\w/-])\/skill:([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?=$|[^a-z0-9-])/g;
|
|
5
|
+
|
|
6
|
+
export default function inlineSkillInvocation(pi: ExtensionAPI) {
|
|
7
|
+
pi.on("input", async (event, ctx) => {
|
|
8
|
+
if (event.source === "extension" || !event.text.includes("/skill:")) {
|
|
9
|
+
return { action: "continue" };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const skillsByName = getSkillsByName(pi);
|
|
13
|
+
const invocations = findInvocations(event.text);
|
|
14
|
+
if (invocations.length === 0) return { action: "continue" };
|
|
15
|
+
|
|
16
|
+
const unknown = invocations.filter((invocation) => !skillsByName.has(invocation.name));
|
|
17
|
+
if (unknown.length > 0) {
|
|
18
|
+
ctx.ui.notify(
|
|
19
|
+
`Unknown skill invocation(s): ${Array.from(new Set(unknown.map((invocation) => invocation.name))).join(", ")}`,
|
|
20
|
+
"warning",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const blocks = new Map<string, string>();
|
|
25
|
+
for (const invocation of invocations) {
|
|
26
|
+
const skill = skillsByName.get(invocation.name);
|
|
27
|
+
if (!skill || blocks.has(invocation.name)) continue;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
blocks.set(invocation.name, await readSkillBlock(skill));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
ctx.ui.notify(
|
|
33
|
+
`Failed to read skill ${invocation.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
34
|
+
"warning",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (blocks.size === 0) return { action: "continue" };
|
|
40
|
+
|
|
41
|
+
const expanded = event.text.replace(SKILL_INVOCATION_PATTERN, (fullMatch: string, prefix: string, name: string) => {
|
|
42
|
+
const block = blocks.get(name);
|
|
43
|
+
return block ? `${prefix}\n\n${block}\n\n` : fullMatch;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { action: "transform", text: normalizeBlankLines(expanded), images: event.images };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getSkillsByName(pi: ExtensionAPI): Map<string, SkillCommandInfo> {
|
|
52
|
+
const result = new Map<string, SkillCommandInfo>();
|
|
53
|
+
for (const command of pi.getCommands()) {
|
|
54
|
+
if (command.source !== "skill") continue;
|
|
55
|
+
const skill = sourceInfoToSkill(command);
|
|
56
|
+
if (skill) result.set(skill.name, skill);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findInvocations(text: string): Array<{ name: string }> {
|
|
62
|
+
const result: Array<{ name: string }> = [];
|
|
63
|
+
const pattern = new RegExp(SKILL_INVOCATION_PATTERN.source, "g");
|
|
64
|
+
for (const match of text.matchAll(pattern)) {
|
|
65
|
+
result.push({ name: match[2] });
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeBlankLines(text: string): string {
|
|
71
|
+
return text.replace(/\n{4,}/g, "\n\n\n").trim();
|
|
72
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
formatSkillsForPrompt,
|
|
4
|
+
getSettingsListTheme,
|
|
5
|
+
InteractiveMode,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type Skill,
|
|
8
|
+
type Theme,
|
|
9
|
+
} from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { type Component, Key, matchesKey, type SettingItem, SettingsList, truncateToWidth, type TUI } from "@mariozechner/pi-tui";
|
|
11
|
+
import {
|
|
12
|
+
normalizeSkillName,
|
|
13
|
+
normalizeSkillNames,
|
|
14
|
+
readEffectiveHiddenSkills,
|
|
15
|
+
readScopedSkillfulSettings,
|
|
16
|
+
type SkillfulScope,
|
|
17
|
+
writeHiddenSkills,
|
|
18
|
+
} from "../config.js";
|
|
19
|
+
|
|
20
|
+
const SKILLS_SECTION_PATTERN = /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
|
|
21
|
+
const SCOPES: SkillfulScope[] = ["global", "project"];
|
|
22
|
+
const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
|
|
23
|
+
const STARTUP_SKILL_LIST_PATCH_KEY = Symbol.for("pi-skillful.startupSkillListPatchInstalled");
|
|
24
|
+
|
|
25
|
+
interface SkillVisibilityStore {
|
|
26
|
+
hiddenSkillsByCwd: Map<string, Set<string>>;
|
|
27
|
+
lastHiddenSkills: Set<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const skillVisibilityStore = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
|
|
31
|
+
hiddenSkillsByCwd: new Map<string, Set<string>>(),
|
|
32
|
+
lastHiddenSkills: new Set<string>(),
|
|
33
|
+
}) as SkillVisibilityStore;
|
|
34
|
+
|
|
35
|
+
interface SkillListItem {
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function skillVisibility(pi: ExtensionAPI) {
|
|
41
|
+
installStartupSkillListPatch();
|
|
42
|
+
|
|
43
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
44
|
+
await refreshHiddenSkillCache(ctx.cwd);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
48
|
+
const hidden = await refreshHiddenSkillCache(ctx.cwd);
|
|
49
|
+
if (hidden.size === 0 || !event.systemPromptOptions.skills?.length) return;
|
|
50
|
+
|
|
51
|
+
const filteredSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
|
|
52
|
+
hidden.has(skill.name) ? { ...skill, disableModelInvocation: true } : skill,
|
|
53
|
+
);
|
|
54
|
+
const replacement = formatSkillsForPrompt(filteredSkills);
|
|
55
|
+
|
|
56
|
+
if (!SKILLS_SECTION_PATTERN.test(event.systemPrompt)) return;
|
|
57
|
+
return { systemPrompt: event.systemPrompt.replace(SKILLS_SECTION_PATTERN, replacement) };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
pi.registerCommand("skillful", {
|
|
61
|
+
description: "Open the pi-skillful skill visibility menu.",
|
|
62
|
+
handler: async (_args, ctx) => {
|
|
63
|
+
if (!ctx.hasUI) {
|
|
64
|
+
ctx.ui.notify("/skillful requires interactive UI", "warning");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const skills = getSkillItems(pi);
|
|
69
|
+
if (skills.length === 0) {
|
|
70
|
+
ctx.ui.notify("No skills are currently loaded.", "info");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const scoped = await readScopedSkillfulSettings(ctx.cwd);
|
|
75
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
76
|
+
new SkillfulVisibilityMenu({
|
|
77
|
+
cwd: ctx.cwd,
|
|
78
|
+
skills,
|
|
79
|
+
hiddenByScope: {
|
|
80
|
+
global: new Set(scoped.global.hiddenSkills),
|
|
81
|
+
project: new Set(scoped.project.hiddenSkills),
|
|
82
|
+
},
|
|
83
|
+
theme,
|
|
84
|
+
tui,
|
|
85
|
+
notify: (message, type) => ctx.ui.notify(message, type),
|
|
86
|
+
done,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function refreshHiddenSkillCache(cwd: string): Promise<Set<string>> {
|
|
94
|
+
const hidden = await readEffectiveHiddenSkills(cwd);
|
|
95
|
+
skillVisibilityStore.hiddenSkillsByCwd.set(cwd, hidden);
|
|
96
|
+
skillVisibilityStore.lastHiddenSkills = hidden;
|
|
97
|
+
return hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function installStartupSkillListPatch(): void {
|
|
101
|
+
type StartupResourceLoader = {
|
|
102
|
+
getSkills: () => { skills: Skill[]; diagnostics: unknown[] };
|
|
103
|
+
};
|
|
104
|
+
type InteractiveModeWithStartupResources = {
|
|
105
|
+
showLoadedResources?: (options?: unknown) => void;
|
|
106
|
+
session?: { resourceLoader?: StartupResourceLoader };
|
|
107
|
+
sessionManager?: { getCwd?: () => string };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const prototype = (InteractiveMode as unknown as { prototype: InteractiveModeWithStartupResources }).prototype;
|
|
111
|
+
const patchState = prototype as InteractiveModeWithStartupResources & Record<PropertyKey, unknown>;
|
|
112
|
+
if (patchState[STARTUP_SKILL_LIST_PATCH_KEY]) return;
|
|
113
|
+
|
|
114
|
+
const original = prototype.showLoadedResources;
|
|
115
|
+
if (typeof original !== "function") return;
|
|
116
|
+
|
|
117
|
+
prototype.showLoadedResources = function showLoadedResourcesWithSkillfulVisibility(
|
|
118
|
+
this: InteractiveModeWithStartupResources,
|
|
119
|
+
options?: unknown,
|
|
120
|
+
): void {
|
|
121
|
+
const loader = this.session?.resourceLoader;
|
|
122
|
+
const originalGetSkills = loader?.getSkills;
|
|
123
|
+
if (!loader || typeof originalGetSkills !== "function") {
|
|
124
|
+
original.call(this, options);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const cwd = this.sessionManager?.getCwd?.();
|
|
129
|
+
const hidden = (cwd ? skillVisibilityStore.hiddenSkillsByCwd.get(cwd) : undefined) ?? skillVisibilityStore.lastHiddenSkills;
|
|
130
|
+
|
|
131
|
+
loader.getSkills = () => {
|
|
132
|
+
const result = originalGetSkills.call(loader);
|
|
133
|
+
return {
|
|
134
|
+
...result,
|
|
135
|
+
skills: result.skills.map((skill) => ({
|
|
136
|
+
...skill,
|
|
137
|
+
name: formatStartupSkillName(skill.name, hidden.has(skill.name)),
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
original.call(this, options);
|
|
144
|
+
} finally {
|
|
145
|
+
loader.getSkills = originalGetSkills;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
patchState[STARTUP_SKILL_LIST_PATCH_KEY] = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatStartupSkillName(name: string, hidden: boolean): string {
|
|
153
|
+
return hidden ? `${name} \x1b[31;2m●\x1b[22;39m` : `${name} \x1b[32m●\x1b[39m`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|
|
157
|
+
const byName = new Map<string, SkillListItem>();
|
|
158
|
+
for (const command of pi.getCommands()) {
|
|
159
|
+
if (command.source !== "skill") continue;
|
|
160
|
+
const name = normalizeSkillName(command.name);
|
|
161
|
+
if (!name) continue;
|
|
162
|
+
byName.set(name, {
|
|
163
|
+
name,
|
|
164
|
+
description: command.description ?? "",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class SkillfulVisibilityMenu implements Component {
|
|
172
|
+
private readonly cwd: string;
|
|
173
|
+
private readonly skills: SkillListItem[];
|
|
174
|
+
private readonly hiddenByScope: Record<SkillfulScope, Set<string>>;
|
|
175
|
+
private readonly theme: Theme;
|
|
176
|
+
private readonly tui: TUI;
|
|
177
|
+
private readonly notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
178
|
+
private readonly done: () => void;
|
|
179
|
+
private readonly topBorder: DynamicBorder;
|
|
180
|
+
private readonly bottomBorder: DynamicBorder;
|
|
181
|
+
private scope: SkillfulScope = "project";
|
|
182
|
+
private settingsList: SettingsList;
|
|
183
|
+
private saveQueue: Promise<void> = Promise.resolve();
|
|
184
|
+
|
|
185
|
+
constructor(options: {
|
|
186
|
+
cwd: string;
|
|
187
|
+
skills: SkillListItem[];
|
|
188
|
+
hiddenByScope: Record<SkillfulScope, Set<string>>;
|
|
189
|
+
theme: Theme;
|
|
190
|
+
tui: TUI;
|
|
191
|
+
notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
192
|
+
done: () => void;
|
|
193
|
+
}) {
|
|
194
|
+
this.cwd = options.cwd;
|
|
195
|
+
this.skills = options.skills;
|
|
196
|
+
this.hiddenByScope = options.hiddenByScope;
|
|
197
|
+
this.theme = options.theme;
|
|
198
|
+
this.tui = options.tui;
|
|
199
|
+
this.notify = options.notify;
|
|
200
|
+
this.done = options.done;
|
|
201
|
+
this.topBorder = new DynamicBorder((text: string) => this.theme.fg("accent", text));
|
|
202
|
+
this.bottomBorder = new DynamicBorder((text: string) => this.theme.fg("accent", text));
|
|
203
|
+
this.settingsList = this.createSettingsList();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render(width: number): string[] {
|
|
207
|
+
return [
|
|
208
|
+
...this.topBorder.render(width),
|
|
209
|
+
truncateToWidth(` ${this.theme.bold(this.theme.fg("accent", "pi-skillful"))} ${this.renderTabs()}`, width),
|
|
210
|
+
truncateToWidth(this.theme.fg("dim", " Toggle skills shown in the model-invocation system prompt"), width),
|
|
211
|
+
"",
|
|
212
|
+
...this.settingsList.render(width),
|
|
213
|
+
"",
|
|
214
|
+
truncateToWidth(this.theme.fg("dim", " Tab/←/→ switch global/project · Enter/Space toggle · Esc close"), width),
|
|
215
|
+
...this.bottomBorder.render(width),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
handleInput(data: string): void {
|
|
220
|
+
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
|
221
|
+
this.switchScope(1);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (matchesKey(data, Key.shift(Key.tab)) || matchesKey(data, Key.left)) {
|
|
225
|
+
this.switchScope(-1);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.settingsList.handleInput(data);
|
|
230
|
+
this.tui.requestRender();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
invalidate(): void {
|
|
234
|
+
this.topBorder.invalidate();
|
|
235
|
+
this.bottomBorder.invalidate();
|
|
236
|
+
this.settingsList.invalidate();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private renderTabs(): string {
|
|
240
|
+
return SCOPES
|
|
241
|
+
.map((scope) => {
|
|
242
|
+
const label = scope === "global" ? "Global" : "Project";
|
|
243
|
+
return scope === this.scope
|
|
244
|
+
? this.theme.bg("selectedBg", this.theme.fg("accent", ` ${label} `))
|
|
245
|
+
: this.theme.fg("muted", ` ${label} `);
|
|
246
|
+
})
|
|
247
|
+
.join(" ");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private switchScope(direction: 1 | -1): void {
|
|
251
|
+
const current = SCOPES.indexOf(this.scope);
|
|
252
|
+
this.scope = SCOPES[(current + direction + SCOPES.length) % SCOPES.length];
|
|
253
|
+
this.settingsList = this.createSettingsList();
|
|
254
|
+
this.tui.requestRender();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private createSettingsList(): SettingsList {
|
|
258
|
+
const items: SettingItem[] = this.skills.map((skill) => {
|
|
259
|
+
const hidden = this.hiddenByScope[this.scope].has(skill.name);
|
|
260
|
+
return {
|
|
261
|
+
id: skill.name,
|
|
262
|
+
label: skill.name,
|
|
263
|
+
description: skill.description || "No skill description provided.",
|
|
264
|
+
currentValue: hidden ? "off" : "on",
|
|
265
|
+
values: ["on", "off"],
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return new SettingsList(
|
|
270
|
+
items,
|
|
271
|
+
12,
|
|
272
|
+
getSettingsListTheme(),
|
|
273
|
+
(skillName, newValue) => {
|
|
274
|
+
const hidden = this.hiddenByScope[this.scope];
|
|
275
|
+
if (newValue === "off") hidden.add(skillName);
|
|
276
|
+
else hidden.delete(skillName);
|
|
277
|
+
this.persistScope(this.scope);
|
|
278
|
+
},
|
|
279
|
+
this.done,
|
|
280
|
+
{ enableSearch: true },
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private persistScope(scope: SkillfulScope): void {
|
|
285
|
+
const snapshot = normalizeSkillNames(this.hiddenByScope[scope]);
|
|
286
|
+
this.saveQueue = this.saveQueue
|
|
287
|
+
.catch(() => undefined)
|
|
288
|
+
.then(async () => {
|
|
289
|
+
await writeHiddenSkills(scope, this.cwd, snapshot);
|
|
290
|
+
await refreshHiddenSkillCache(this.cwd);
|
|
291
|
+
})
|
|
292
|
+
.catch((error) => {
|
|
293
|
+
this.notify(`Failed to save ${scope} skill visibility: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SkillCommandInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
baseDir: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function stripFrontmatter(markdown: string): string {
|
|
11
|
+
const normalized = markdown.replace(/^\uFEFF/, "");
|
|
12
|
+
const match = normalized.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
13
|
+
return match ? normalized.slice(match[0].length) : normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readSkillBlock(skill: SkillCommandInfo): Promise<string> {
|
|
17
|
+
const content = await readFile(skill.path, "utf-8");
|
|
18
|
+
const body = stripFrontmatter(content).trim();
|
|
19
|
+
return `<skill name="${escapeAttribute(skill.name)}" location="${escapeAttribute(skill.path)}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sourceInfoToSkill(command: {
|
|
23
|
+
name: string;
|
|
24
|
+
sourceInfo: { path: string; baseDir?: string };
|
|
25
|
+
}): SkillCommandInfo | null {
|
|
26
|
+
if (!command.name.startsWith("skill:")) return null;
|
|
27
|
+
const name = command.name.slice("skill:".length);
|
|
28
|
+
if (!name) return null;
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
path: command.sourceInfo.path,
|
|
32
|
+
baseDir: command.sourceInfo.baseDir ?? dirname(command.sourceInfo.path),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function escapeAttribute(value: string): string {
|
|
37
|
+
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
38
|
+
}
|