@pasko70/pibo 1.0.5 → 1.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/README.md +5 -1
- package/dist/apps/chat/agent-profiles.js +26 -4
- package/dist/apps/chat/model-catalog.js +42 -0
- package/dist/apps/chat/web-app.js +192 -7
- package/dist/apps/chat-ui/assets/{dist-aMr2VYOu.js → dist-8e4L8ciw.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BGxHKHPX.js → dist-B7bbI3Qz.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-u2OzzOTm.js → dist-CMZOa5ik.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CI0Xj2Cp.js → dist-CNLWH2qh.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-zgYUl2B-.js → dist-CUqpST5w.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-wiRpAHIv.js → dist-CqqUC8Ce.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CNlhGq5i.js → dist-CspjOeBj.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CaNOvqMY.js → dist-D2N4-LIc.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DpmEro5R.js → dist-D9L4MSRF.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Di1ScJ22.js → dist-DNcnQRPF.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DrM-H3e1.js → dist-v4pjHlB7.js} +1 -1
- package/dist/apps/chat-ui/assets/index-D2Qxambd.js +151 -0
- package/dist/apps/chat-ui/assets/{index-Dk-opWl3.css → index-DXx368XK.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/gateway/server.js +2 -0
- package/dist/plugins/builtin.js +2 -1
- package/dist/plugins/registry.js +17 -1
- package/dist/user-skills/installer.js +211 -0
- package/dist/user-skills/manager.js +35 -0
- package/dist/user-skills/store.js +228 -0
- package/dist/user-skills/types.js +1 -0
- package/package.json +3 -3
- package/dist/apps/chat-ui/assets/index-B_E935fP.js +0 -151
- /package/{.codex/skills → skills/builtin}/pi-agent-harness/SKILL.md +0 -0
- /package/{.codex/skills → skills/builtin}/pi-agent-harness/agents/openai.yaml +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { defaultUserSkillDir, parseSkillMd } from "./store.js";
|
|
4
|
+
export function parseSkillUrl(url) {
|
|
5
|
+
const trimmed = url.trim();
|
|
6
|
+
// skills.sh: https://skills.sh/{owner}/skills/{skill-name}
|
|
7
|
+
// skills.sh: https://skills.sh/{owner}/{repo}/{skill-name}
|
|
8
|
+
if (trimmed.startsWith("https://skills.sh/")) {
|
|
9
|
+
const rest = trimmed.slice("https://skills.sh/".length);
|
|
10
|
+
const parts = rest.split("/").filter(Boolean);
|
|
11
|
+
if (parts.length >= 3) {
|
|
12
|
+
// Format: {owner}/{repo}/{skill-name} (e.g. softaworks/agent-toolkit/writing-clearly)
|
|
13
|
+
const owner = parts[0];
|
|
14
|
+
const repo = parts[1];
|
|
15
|
+
const skillName = parts[2];
|
|
16
|
+
return { owner, repo, path: `skills/${skillName}`, skillName };
|
|
17
|
+
}
|
|
18
|
+
if (parts.length >= 2 && parts[1] === "skills") {
|
|
19
|
+
const owner = parts[0];
|
|
20
|
+
const skillName = parts[2] ?? parts[0];
|
|
21
|
+
return { owner, repo: "skills", path: `skills/${skillName}`, skillName };
|
|
22
|
+
}
|
|
23
|
+
if (parts.length === 2) {
|
|
24
|
+
// Format: {owner}/skills or {owner}/{repo}
|
|
25
|
+
if (parts[1] === "skills") {
|
|
26
|
+
return { owner: parts[0], repo: "skills" };
|
|
27
|
+
}
|
|
28
|
+
return { owner: parts[0], repo: parts[1] };
|
|
29
|
+
}
|
|
30
|
+
if (parts.length === 1) {
|
|
31
|
+
return { owner: parts[0], repo: "skills" };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// GitHub tree URL: https://github.com/{owner}/{repo}/tree/{branch}/{path}
|
|
35
|
+
const githubTreeMatch = trimmed.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/tree\/[^\/]+\/(.+)$/);
|
|
36
|
+
if (githubTreeMatch) {
|
|
37
|
+
const owner = githubTreeMatch[1];
|
|
38
|
+
const repo = githubTreeMatch[2];
|
|
39
|
+
const path = githubTreeMatch[3];
|
|
40
|
+
const skillName = path.split("/").pop() ?? repo;
|
|
41
|
+
return { owner, repo, path, skillName };
|
|
42
|
+
}
|
|
43
|
+
// GitHub shorthand or repo URL: https://github.com/{owner}/{repo}
|
|
44
|
+
const githubRepoMatch = trimmed.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)$/);
|
|
45
|
+
if (githubRepoMatch) {
|
|
46
|
+
return { owner: githubRepoMatch[1], repo: githubRepoMatch[2] };
|
|
47
|
+
}
|
|
48
|
+
// Bare shorthand: {owner}/{repo} or {owner}/{repo}/{skill-path}
|
|
49
|
+
const shorthandMatch = trimmed.match(/^([^\/\s]+)\/([^\/\s]+)(?:\/(.+))?$/);
|
|
50
|
+
if (shorthandMatch && !trimmed.startsWith("http")) {
|
|
51
|
+
const owner = shorthandMatch[1];
|
|
52
|
+
const repo = shorthandMatch[2];
|
|
53
|
+
const path = shorthandMatch[3];
|
|
54
|
+
const skillName = path?.split("/").pop() ?? repo;
|
|
55
|
+
return { owner, repo, path, skillName };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
async function fetchGitHubContents(owner, repo, path) {
|
|
60
|
+
const apiUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`;
|
|
61
|
+
const response = await fetch(apiUrl, {
|
|
62
|
+
headers: { Accept: "application/vnd.github.v3+json", "User-Agent": "pibo-skills-installer" },
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text().catch(() => "");
|
|
66
|
+
throw new Error(`GitHub API error (${response.status}): ${text || response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
const data = (await response.json());
|
|
69
|
+
if (!Array.isArray(data)) {
|
|
70
|
+
throw new Error(`Expected directory listing from GitHub API, got: ${typeof data}`);
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
async function fetchGitHubFile(downloadUrl) {
|
|
75
|
+
const response = await fetch(downloadUrl, { headers: { "User-Agent": "pibo-skills-installer" } });
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`Failed to download file (${response.status}): ${downloadUrl}`);
|
|
78
|
+
}
|
|
79
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
80
|
+
}
|
|
81
|
+
async function downloadDirectory(owner, repo, path, targetDir) {
|
|
82
|
+
const items = await fetchGitHubContents(owner, repo, path);
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
const targetPath = join(targetDir, item.name);
|
|
85
|
+
if (item.type === "file") {
|
|
86
|
+
if (!item.download_url)
|
|
87
|
+
continue;
|
|
88
|
+
const content = await fetchGitHubFile(item.download_url);
|
|
89
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
90
|
+
writeFileSync(targetPath, content);
|
|
91
|
+
}
|
|
92
|
+
else if (item.type === "dir") {
|
|
93
|
+
mkdirSync(targetPath, { recursive: true });
|
|
94
|
+
await downloadDirectory(owner, repo, item.path, targetPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function findSkillMdPath(items) {
|
|
99
|
+
return items.find((item) => item.type === "file" && item.name.toLowerCase() === "skill.md");
|
|
100
|
+
}
|
|
101
|
+
async function findSkillDirectory(owner, repo, path) {
|
|
102
|
+
if (path) {
|
|
103
|
+
const items = await fetchGitHubContents(owner, repo, path);
|
|
104
|
+
const skillMd = await findSkillMdPath(items);
|
|
105
|
+
if (skillMd) {
|
|
106
|
+
return { path, skillName: path.split("/").pop() ?? repo };
|
|
107
|
+
}
|
|
108
|
+
// If no SKILL.md at this path, search subdirectories one level deep
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
if (item.type === "dir") {
|
|
111
|
+
const subItems = await fetchGitHubContents(owner, repo, item.path).catch(() => []);
|
|
112
|
+
const subSkillMd = await findSkillMdPath(subItems);
|
|
113
|
+
if (subSkillMd) {
|
|
114
|
+
return { path: item.path, skillName: item.name };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Try common skill directories
|
|
120
|
+
const candidates = ["skills", "skill", "src/skills", "agents/skills"];
|
|
121
|
+
for (const candidate of candidates) {
|
|
122
|
+
const items = await fetchGitHubContents(owner, repo, candidate).catch(() => []);
|
|
123
|
+
const skillMd = await findSkillMdPath(items);
|
|
124
|
+
if (skillMd) {
|
|
125
|
+
return { path: candidate, skillName: candidate };
|
|
126
|
+
}
|
|
127
|
+
// Search one level deep
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
if (item.type === "dir") {
|
|
130
|
+
const subItems = await fetchGitHubContents(owner, repo, item.path).catch(() => []);
|
|
131
|
+
const subSkillMd = await findSkillMdPath(subItems);
|
|
132
|
+
if (subSkillMd) {
|
|
133
|
+
return { path: item.path, skillName: item.name };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Search repository root for SKILL.md
|
|
139
|
+
const rootItems = await fetchGitHubContents(owner, repo, "").catch(() => []);
|
|
140
|
+
const rootSkillMd = await findSkillMdPath(rootItems);
|
|
141
|
+
if (rootSkillMd) {
|
|
142
|
+
return { path: "", skillName: repo };
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Could not find a SKILL.md in ${owner}/${repo}${path ? `/${path}` : ""}`);
|
|
145
|
+
}
|
|
146
|
+
export async function installSkillFromUrl(url, cwd = process.cwd()) {
|
|
147
|
+
const source = parseSkillUrl(url);
|
|
148
|
+
if (!source) {
|
|
149
|
+
throw new Error(`Unsupported skill URL format: ${url}`);
|
|
150
|
+
}
|
|
151
|
+
const { owner, repo, path, skillName } = source;
|
|
152
|
+
const resolved = path
|
|
153
|
+
? { path, skillName: skillName ?? path.split("/").pop() ?? repo }
|
|
154
|
+
: await findSkillDirectory(owner, repo);
|
|
155
|
+
const targetName = resolved.skillName;
|
|
156
|
+
const targetDir = join(defaultUserSkillDir(cwd), targetName);
|
|
157
|
+
if (existsSync(targetDir)) {
|
|
158
|
+
throw new Error(`A skill named "${targetName}" already exists. Delete it first or choose a different name.`);
|
|
159
|
+
}
|
|
160
|
+
mkdirSync(targetDir, { recursive: true });
|
|
161
|
+
try {
|
|
162
|
+
await downloadDirectory(owner, repo, resolved.path, targetDir);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// Clean up on failure
|
|
166
|
+
try {
|
|
167
|
+
const { rmSync } = await import("node:fs");
|
|
168
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// ignore cleanup errors
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
// Read SKILL.md to extract name and description
|
|
176
|
+
const skillMdPath = join(targetDir, "SKILL.md");
|
|
177
|
+
if (!existsSync(skillMdPath)) {
|
|
178
|
+
throw new Error(`Download completed but no SKILL.md found in ${targetName}`);
|
|
179
|
+
}
|
|
180
|
+
const { readFileSync } = await import("node:fs");
|
|
181
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
182
|
+
const parsed = parseSkillMd(content);
|
|
183
|
+
const name = parsed.name || targetName;
|
|
184
|
+
const description = parsed.description || "";
|
|
185
|
+
// Store the skill metadata
|
|
186
|
+
const { randomUUID } = await import("node:crypto");
|
|
187
|
+
const now = new Date().toISOString();
|
|
188
|
+
const skill = {
|
|
189
|
+
id: randomUUID(),
|
|
190
|
+
name,
|
|
191
|
+
description,
|
|
192
|
+
path: skillMdPath,
|
|
193
|
+
enabled: true,
|
|
194
|
+
source: url.includes("skills.sh") ? "skills.sh" : "github",
|
|
195
|
+
sourceUrl: url,
|
|
196
|
+
createdAt: now,
|
|
197
|
+
updatedAt: now,
|
|
198
|
+
};
|
|
199
|
+
const { loadUserSkillStore, saveUserSkillStore } = await import("./store.js");
|
|
200
|
+
const store = loadUserSkillStore(cwd);
|
|
201
|
+
const existing = store.skills.find((s) => s.name === name);
|
|
202
|
+
if (existing) {
|
|
203
|
+
const { rmSync } = await import("node:fs");
|
|
204
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
205
|
+
throw new Error(`A skill named "${name}" already exists. Delete it first or choose a different name.`);
|
|
206
|
+
}
|
|
207
|
+
store.skills.push(skill);
|
|
208
|
+
store.skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
209
|
+
saveUserSkillStore(store, cwd);
|
|
210
|
+
return skill;
|
|
211
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createUserSkill, deleteUserSkill, findUserSkill, listUserSkills, readSkillMarkdown, setUserSkillEnabled, updateUserSkill, } from "./store.js";
|
|
2
|
+
import { installSkillFromUrl } from "./installer.js";
|
|
3
|
+
export class UserSkillManager {
|
|
4
|
+
cwd;
|
|
5
|
+
constructor(cwd) {
|
|
6
|
+
this.cwd = cwd;
|
|
7
|
+
}
|
|
8
|
+
create(input) {
|
|
9
|
+
return createUserSkill(input, this.cwd);
|
|
10
|
+
}
|
|
11
|
+
update(id, input) {
|
|
12
|
+
return updateUserSkill(id, input, this.cwd);
|
|
13
|
+
}
|
|
14
|
+
remove(id) {
|
|
15
|
+
return deleteUserSkill(id, this.cwd);
|
|
16
|
+
}
|
|
17
|
+
setEnabled(id, enabled) {
|
|
18
|
+
return setUserSkillEnabled(id, enabled, this.cwd);
|
|
19
|
+
}
|
|
20
|
+
async installFromUrl(url) {
|
|
21
|
+
return installSkillFromUrl(url, this.cwd);
|
|
22
|
+
}
|
|
23
|
+
getSkillMarkdown(id) {
|
|
24
|
+
const skill = findUserSkill(id, this.cwd);
|
|
25
|
+
if (!skill)
|
|
26
|
+
return "";
|
|
27
|
+
return readSkillMarkdown(skill, this.cwd);
|
|
28
|
+
}
|
|
29
|
+
list() {
|
|
30
|
+
return listUserSkills(this.cwd);
|
|
31
|
+
}
|
|
32
|
+
get(id) {
|
|
33
|
+
return findUserSkill(id, this.cwd);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
const STORE_VERSION = 1;
|
|
5
|
+
const SKILL_DIR_NAME = "user-skills";
|
|
6
|
+
const STORE_FILE_NAME = "user-skills.json";
|
|
7
|
+
export function defaultUserSkillStorePath(cwd = process.cwd()) {
|
|
8
|
+
return resolve(cwd, ".pibo", STORE_FILE_NAME);
|
|
9
|
+
}
|
|
10
|
+
export function defaultUserSkillDir(cwd = process.cwd()) {
|
|
11
|
+
return resolve(cwd, ".pibo", SKILL_DIR_NAME);
|
|
12
|
+
}
|
|
13
|
+
export function ensureUserSkillStorage(cwd = process.cwd()) {
|
|
14
|
+
mkdirSync(defaultUserSkillDir(cwd), { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
export function loadUserSkillStore(cwd = process.cwd()) {
|
|
17
|
+
const path = defaultUserSkillStorePath(cwd);
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return { version: STORE_VERSION, skills: [] };
|
|
20
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
21
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
22
|
+
throw new Error(`Invalid user skills store at ${path}`);
|
|
23
|
+
}
|
|
24
|
+
const data = parsed;
|
|
25
|
+
if (data.version !== STORE_VERSION || !Array.isArray(data.skills)) {
|
|
26
|
+
throw new Error(`Unsupported user skills store at ${path}`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
version: STORE_VERSION,
|
|
30
|
+
skills: data.skills.map(sanitizeStoredSkill),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function saveUserSkillStore(data, cwd = process.cwd()) {
|
|
34
|
+
const path = defaultUserSkillStorePath(cwd);
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
writeFileSync(path, `${JSON.stringify({ version: STORE_VERSION, skills: data.skills }, null, 2)}\n`, "utf-8");
|
|
37
|
+
}
|
|
38
|
+
export function listUserSkills(cwd = process.cwd()) {
|
|
39
|
+
return loadUserSkillStore(cwd).skills;
|
|
40
|
+
}
|
|
41
|
+
export function findUserSkill(idOrName, cwd = process.cwd()) {
|
|
42
|
+
const lookup = idOrName.trim();
|
|
43
|
+
return listUserSkills(cwd).find((skill) => skill.id === lookup || skill.name === lookup);
|
|
44
|
+
}
|
|
45
|
+
function validateSkillName(name, existingId, cwd = process.cwd()) {
|
|
46
|
+
const trimmed = name.trim();
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
throw new Error("Skill name is required");
|
|
49
|
+
if (trimmed.length > 64)
|
|
50
|
+
throw new Error("Skill name is too long (max 64 characters)");
|
|
51
|
+
if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) {
|
|
52
|
+
throw new Error("Skill name must be lowercase kebab-case, e.g. my-skill");
|
|
53
|
+
}
|
|
54
|
+
const store = loadUserSkillStore(cwd);
|
|
55
|
+
const existing = store.skills.find((s) => s.name === trimmed && s.id !== existingId);
|
|
56
|
+
if (existing)
|
|
57
|
+
throw new Error(`Skill name "${trimmed}" already exists`);
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
export function readSkillMarkdown(skill, cwd = process.cwd()) {
|
|
61
|
+
const fullPath = resolve(cwd, skill.path);
|
|
62
|
+
if (!existsSync(fullPath))
|
|
63
|
+
return "";
|
|
64
|
+
return readFileSync(fullPath, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
export function createUserSkill(input, cwd = process.cwd()) {
|
|
67
|
+
ensureUserSkillStorage(cwd);
|
|
68
|
+
const name = validateSkillName(input.name, undefined, cwd);
|
|
69
|
+
const description = (input.description ?? "").trim();
|
|
70
|
+
const id = randomUUID();
|
|
71
|
+
const skillDir = join(defaultUserSkillDir(cwd), name);
|
|
72
|
+
mkdirSync(skillDir, { recursive: true });
|
|
73
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
74
|
+
const markdown = buildSkillMd(name, description, input.markdown ?? "");
|
|
75
|
+
writeFileSync(skillPath, markdown, "utf-8");
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const skill = {
|
|
78
|
+
id,
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
81
|
+
path: skillPath,
|
|
82
|
+
enabled: true,
|
|
83
|
+
source: "user-created",
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
};
|
|
87
|
+
const store = loadUserSkillStore(cwd);
|
|
88
|
+
store.skills.push(skill);
|
|
89
|
+
store.skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
90
|
+
saveUserSkillStore(store, cwd);
|
|
91
|
+
return skill;
|
|
92
|
+
}
|
|
93
|
+
export function updateUserSkill(id, input, cwd = process.cwd()) {
|
|
94
|
+
const store = loadUserSkillStore(cwd);
|
|
95
|
+
const index = store.skills.findIndex((s) => s.id === id);
|
|
96
|
+
if (index < 0)
|
|
97
|
+
throw new Error(`Skill "${id}" not found`);
|
|
98
|
+
const existing = store.skills[index];
|
|
99
|
+
let name = existing.name;
|
|
100
|
+
let description = existing.description;
|
|
101
|
+
if (input.name !== undefined) {
|
|
102
|
+
name = validateSkillName(input.name, existing.id, cwd);
|
|
103
|
+
}
|
|
104
|
+
if (input.description !== undefined) {
|
|
105
|
+
description = input.description.trim();
|
|
106
|
+
}
|
|
107
|
+
const skillDir = join(defaultUserSkillDir(cwd), name);
|
|
108
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
109
|
+
if (name !== existing.name) {
|
|
110
|
+
const oldDir = dirname(existing.path);
|
|
111
|
+
if (existsSync(oldDir) && oldDir !== skillDir) {
|
|
112
|
+
mkdirSync(dirname(skillDir), { recursive: true });
|
|
113
|
+
renameDirContents(oldDir, skillDir);
|
|
114
|
+
if (existsSync(oldDir)) {
|
|
115
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (input.markdown !== undefined) {
|
|
120
|
+
const markdown = buildSkillMd(name, description, input.markdown);
|
|
121
|
+
writeFileSync(skillPath, markdown, "utf-8");
|
|
122
|
+
}
|
|
123
|
+
else if (name !== existing.name || description !== existing.description) {
|
|
124
|
+
const currentMarkdown = existsSync(existing.path) ? readFileSync(existing.path, "utf-8") : "";
|
|
125
|
+
const { body } = parseSkillMd(currentMarkdown);
|
|
126
|
+
writeFileSync(skillPath, buildSkillMd(name, description, body), "utf-8");
|
|
127
|
+
}
|
|
128
|
+
const updated = {
|
|
129
|
+
...existing,
|
|
130
|
+
name,
|
|
131
|
+
description,
|
|
132
|
+
path: skillPath,
|
|
133
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
134
|
+
updatedAt: new Date().toISOString(),
|
|
135
|
+
};
|
|
136
|
+
store.skills[index] = updated;
|
|
137
|
+
store.skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
138
|
+
saveUserSkillStore(store, cwd);
|
|
139
|
+
return updated;
|
|
140
|
+
}
|
|
141
|
+
export function deleteUserSkill(id, cwd = process.cwd()) {
|
|
142
|
+
const store = loadUserSkillStore(cwd);
|
|
143
|
+
const index = store.skills.findIndex((s) => s.id === id);
|
|
144
|
+
if (index < 0)
|
|
145
|
+
return undefined;
|
|
146
|
+
const [removed] = store.skills.splice(index, 1);
|
|
147
|
+
const skillDir = dirname(removed.path);
|
|
148
|
+
if (existsSync(skillDir)) {
|
|
149
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
saveUserSkillStore(store, cwd);
|
|
152
|
+
return removed;
|
|
153
|
+
}
|
|
154
|
+
export function setUserSkillEnabled(id, enabled, cwd = process.cwd()) {
|
|
155
|
+
return updateUserSkill(id, { enabled }, cwd);
|
|
156
|
+
}
|
|
157
|
+
function sanitizeStoredSkill(value) {
|
|
158
|
+
const candidate = value;
|
|
159
|
+
if (!candidate || typeof candidate !== "object")
|
|
160
|
+
throw new Error("Invalid user skill entry");
|
|
161
|
+
if (typeof candidate.id !== "string" || typeof candidate.name !== "string" || typeof candidate.path !== "string") {
|
|
162
|
+
throw new Error("Invalid user skill entry");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
id: candidate.id,
|
|
166
|
+
name: candidate.name.trim(),
|
|
167
|
+
description: typeof candidate.description === "string" ? candidate.description.trim() : "",
|
|
168
|
+
path: candidate.path.trim(),
|
|
169
|
+
enabled: candidate.enabled !== false,
|
|
170
|
+
source: isValidSource(candidate.source) ? candidate.source : "user-created",
|
|
171
|
+
sourceUrl: typeof candidate.sourceUrl === "string" ? candidate.sourceUrl.trim() : undefined,
|
|
172
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : new Date().toISOString(),
|
|
173
|
+
updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function isValidSource(source) {
|
|
177
|
+
return source === "user-created" || source === "skills.sh" || source === "github";
|
|
178
|
+
}
|
|
179
|
+
export function parseSkillMd(content) {
|
|
180
|
+
const trimmed = content.trim();
|
|
181
|
+
if (!trimmed.startsWith("---")) {
|
|
182
|
+
return { name: "", description: "", body: trimmed };
|
|
183
|
+
}
|
|
184
|
+
const endIdx = trimmed.indexOf("---", 3);
|
|
185
|
+
if (endIdx === -1) {
|
|
186
|
+
return { name: "", description: "", body: trimmed };
|
|
187
|
+
}
|
|
188
|
+
const frontmatter = trimmed.slice(3, endIdx).trim();
|
|
189
|
+
const body = trimmed.slice(endIdx + 3).trimStart();
|
|
190
|
+
const parsed = parseSimpleYaml(frontmatter);
|
|
191
|
+
return {
|
|
192
|
+
name: typeof parsed.name === "string" ? parsed.name : "",
|
|
193
|
+
description: typeof parsed.description === "string" ? parsed.description : "",
|
|
194
|
+
body,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export function buildSkillMd(name, description, body) {
|
|
198
|
+
const cleanBody = body.trimStart();
|
|
199
|
+
return `---\nname: ${name}\ndescription: ${description}\n---\n\n${cleanBody}`;
|
|
200
|
+
}
|
|
201
|
+
function parseSimpleYaml(text) {
|
|
202
|
+
const result = {};
|
|
203
|
+
for (const line of text.split("\n")) {
|
|
204
|
+
const colonIdx = line.indexOf(":");
|
|
205
|
+
if (colonIdx <= 0)
|
|
206
|
+
continue;
|
|
207
|
+
const key = line.slice(0, colonIdx).trim();
|
|
208
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
209
|
+
if (key)
|
|
210
|
+
result[key] = value;
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
function renameDirContents(oldDir, newDir) {
|
|
215
|
+
mkdirSync(newDir, { recursive: true });
|
|
216
|
+
for (const entry of readdirSync(oldDir)) {
|
|
217
|
+
const oldPath = join(oldDir, entry);
|
|
218
|
+
const newPath = join(newDir, entry);
|
|
219
|
+
const stat = statSync(oldPath);
|
|
220
|
+
if (stat.isDirectory()) {
|
|
221
|
+
renameDirContents(oldPath, newPath);
|
|
222
|
+
rmSync(oldPath, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
writeFileSync(newPath, readFileSync(oldPath));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pasko70/pibo",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Minimal TypeScript wrapper around Pi Coding Agent.",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
8
|
"context",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"skills/builtin/pi-agent-harness/SKILL.md",
|
|
10
|
+
"skills/builtin/pi-agent-harness/agents/openai.yaml",
|
|
11
11
|
"README.md",
|
|
12
12
|
"src/mcp/LICENSE.mcp-cli"
|
|
13
13
|
],
|