@pasko70/pibo 1.0.4 → 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.
Files changed (34) hide show
  1. package/README.md +5 -1
  2. package/dist/apps/chat/agent-profiles.js +30 -4
  3. package/dist/apps/chat/agent-store.js +46 -3
  4. package/dist/apps/chat/model-catalog.js +42 -0
  5. package/dist/apps/chat/web-app.js +232 -7
  6. package/dist/apps/chat-ui/assets/{dist-DHE-yBLH.js → dist-8e4L8ciw.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-BvO6SpaM.js → dist-B7bbI3Qz.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-_DPv0-QF.js → dist-CMZOa5ik.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-_oM7g0eh.js → dist-CNLWH2qh.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-CG_gRSwv.js → dist-CUqpST5w.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-laadbMWg.js → dist-CqqUC8Ce.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-L1B-KWZO.js → dist-CspjOeBj.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-nFui3f-r.js → dist-D2N4-LIc.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BPv5MdlL.js → dist-D9L4MSRF.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-D4HhiFWB.js → dist-DNcnQRPF.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-B5rcjVCj.js → dist-v4pjHlB7.js} +1 -1
  17. package/dist/apps/chat-ui/assets/index-D2Qxambd.js +151 -0
  18. package/dist/apps/chat-ui/assets/{index-Dk-opWl3.css → index-DXx368XK.css} +1 -1
  19. package/dist/apps/chat-ui/index.html +2 -2
  20. package/dist/core/model-defaults.js +53 -0
  21. package/dist/core/profiles.js +16 -0
  22. package/dist/core/runtime.js +8 -6
  23. package/dist/core/session-router.js +2 -0
  24. package/dist/gateway/server.js +2 -0
  25. package/dist/plugins/builtin.js +2 -1
  26. package/dist/plugins/registry.js +20 -1
  27. package/dist/user-skills/installer.js +211 -0
  28. package/dist/user-skills/manager.js +35 -0
  29. package/dist/user-skills/store.js +228 -0
  30. package/dist/user-skills/types.js +1 -0
  31. package/package.json +3 -3
  32. package/dist/apps/chat-ui/assets/index-BdcqTugD.js +0 -151
  33. /package/{.codex/skills → skills/builtin}/pi-agent-harness/SKILL.md +0 -0
  34. /package/{.codex/skills → skills/builtin}/pi-agent-harness/agents/openai.yaml +0 -0
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { isAbsolute, resolve } from "node:path";
3
3
  import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashToolDefinition, getAgentDir, InteractiveMode, SessionManager, } from "@mariozechner/pi-coding-agent";
4
4
  import { DEFAULT_BUILTIN_TOOL_NAMES, } from "./profiles.js";
5
+ import { loadPiboModelDefaults, selectRequestedModelProfile } from "./model-defaults.js";
5
6
  import { createDefaultPiboProfile } from "../plugins/builtin.js";
6
7
  import { createSubagentToolDefinitions, createSubagentToolName, } from "../subagents/tool.js";
7
8
  import { createRunToolDefinitions } from "../runs/tools.js";
@@ -201,7 +202,7 @@ export async function createPiboRuntime(options = {}) {
201
202
  services,
202
203
  sessionManager: runtimeSessionManager,
203
204
  sessionStartEvent,
204
- model: resolveProfileModel(profile, services),
205
+ model: resolveProfileModel(profile, services, runtimeCwd),
205
206
  thinkingLevel: options.thinkingLevel,
206
207
  customTools,
207
208
  noTools: profile.builtinTools === "disabled" ? "builtin" : undefined,
@@ -229,15 +230,16 @@ export async function createPiboRuntime(options = {}) {
229
230
  sessionManager,
230
231
  });
231
232
  }
232
- function resolveProfileModel(profile, services) {
233
- if (!profile.model)
233
+ function resolveProfileModel(profile, services, cwd) {
234
+ const requestedModel = selectRequestedModelProfile(profile, loadPiboModelDefaults(cwd));
235
+ if (!requestedModel)
234
236
  return undefined;
235
- const model = services.modelRegistry.find(profile.model.provider, profile.model.id);
237
+ const model = services.modelRegistry.find(requestedModel.provider, requestedModel.id);
236
238
  if (!model) {
237
- throw new Error(`Profile "${profile.profileName}" requests unknown model ${profile.model.provider}/${profile.model.id}.`);
239
+ throw new Error(`Profile "${profile.profileName}" requests unknown model ${requestedModel.provider}/${requestedModel.id}.`);
238
240
  }
239
241
  if (!services.modelRegistry.hasConfiguredAuth(model)) {
240
- throw new Error(`Profile "${profile.profileName}" requires configured auth for ${profile.model.provider}/${profile.model.id}.`);
242
+ throw new Error(`Profile "${profile.profileName}" requires configured auth for ${requestedModel.provider}/${requestedModel.id}.`);
241
243
  }
242
244
  return model;
243
245
  }
@@ -15,6 +15,8 @@ function profileForSession(baseProfile, piSessionId, parentPiSessionId) {
15
15
  sessionId: piSessionId,
16
16
  parentSessionId: parentPiSessionId,
17
17
  model: baseProfile.model,
18
+ mainModel: baseProfile.mainModel,
19
+ subagentModel: baseProfile.subagentModel,
18
20
  skills: baseProfile.skills,
19
21
  tools: baseProfile.tools,
20
22
  subagents: baseProfile.subagents,
@@ -186,6 +186,8 @@ export class PiboGatewayServer {
186
186
  removeProfile: (name) => this.pluginRegistry.removeProfile(name),
187
187
  upsertContextFile: (contextFile) => this.pluginRegistry.upsertContextFile(contextFile),
188
188
  removeContextFile: (key) => this.pluginRegistry.removeContextFile(key),
189
+ registerSkill: (skill) => this.pluginRegistry.registerSkill(skill),
190
+ unregisterSkill: (name) => this.pluginRegistry.unregisterSkill(name),
189
191
  emitProductEvent: (event) => this.pluginRegistry.emitProductEvent(event),
190
192
  subscribeProductEvents: (listener) => this.pluginRegistry.onProductEvent(listener),
191
193
  auth: this.pluginRegistry.getAuthService(),
@@ -63,7 +63,8 @@ export const piboCorePlugin = definePiboPlugin({
63
63
  register(api) {
64
64
  api.registerSkill({
65
65
  name: "pi-agent-harness",
66
- path: ".codex/skills/pi-agent-harness/SKILL.md",
66
+ path: "skills/builtin/pi-agent-harness/SKILL.md",
67
+ kind: "builtin",
67
68
  });
68
69
  api.registerTool(createWebSearchToolProfile());
69
70
  api.registerSubagents([
@@ -69,6 +69,12 @@ export class PiboPluginRegistry {
69
69
  registerSkill(skill) {
70
70
  this.addUnique(this.skills, skill.name, skill, "skill");
71
71
  }
72
+ unregisterSkill(name) {
73
+ return this.skills.delete(name);
74
+ }
75
+ getRegisteredSkillNames() {
76
+ return [...this.skills.keys()];
77
+ }
72
78
  registerContextFile(contextFile) {
73
79
  this.addUnique(this.contextFiles, contextFileKey(contextFile), contextFile, "context file");
74
80
  }
@@ -155,6 +161,9 @@ export class PiboPluginRegistry {
155
161
  subagents: sessionContext.subagents.filter((subagent) => subagent.enabled !== false),
156
162
  mcpServers: [...sessionContext.mcpServers],
157
163
  piPackages: sessionContext.piPackages.filter((pkg) => pkg.enabled !== false).map((pkg) => pkg.id),
164
+ model: sessionContext.model ? { ...sessionContext.model } : undefined,
165
+ mainModel: sessionContext.mainModel ? { ...sessionContext.mainModel } : undefined,
166
+ subagentModel: sessionContext.subagentModel ? { ...sessionContext.subagentModel } : undefined,
158
167
  builtinTools: sessionContext.builtinTools,
159
168
  builtinToolNames: [...sessionContext.builtinToolNames],
160
169
  autoContextFiles: sessionContext.autoContextFiles,
@@ -176,6 +185,9 @@ export class PiboPluginRegistry {
176
185
  skills: [...this.skills.values()].map((skill) => ({
177
186
  name: skill.name,
178
187
  path: skill.path,
188
+ kind: skill.kind ?? "plugin",
189
+ pluginId: skill.pluginId,
190
+ pluginName: skill.pluginId ? this.pluginNames.get(skill.pluginId) : undefined,
179
191
  })),
180
192
  subagents: [...this.subagents.values()].map((subagent) => ({
181
193
  name: subagent.name,
@@ -273,13 +285,20 @@ export class PiboPluginRegistry {
273
285
  }
274
286
  createApi(pluginId) {
275
287
  const withPluginToolContext = (tool) => ({ ...tool, pluginId: tool.pluginId ?? pluginId });
288
+ const withPluginSkillContext = (skill) => (skill.kind === "user"
289
+ ? skill
290
+ : {
291
+ ...skill,
292
+ kind: skill.kind ?? "plugin",
293
+ pluginId: skill.pluginId ?? pluginId,
294
+ });
276
295
  const withPluginContext = (contextFile) => (contextFile.source === "managed" ? contextFile : { ...contextFile, pluginId: contextFile.pluginId ?? pluginId });
277
296
  return {
278
297
  registerTool: (tool) => this.registerTool(withPluginToolContext(tool)),
279
298
  registerTools: (tools) => this.registerTools(tools.map(withPluginToolContext)),
280
299
  registerSubagent: (subagent) => this.registerSubagent(subagent),
281
300
  registerSubagents: (subagents) => this.registerSubagents(subagents),
282
- registerSkill: (skill) => this.registerSkill(skill),
301
+ registerSkill: (skill) => this.registerSkill(withPluginSkillContext(skill)),
283
302
  registerContextFile: (contextFile) => this.registerContextFile(withPluginContext(contextFile)),
284
303
  upsertContextFile: (contextFile) => this.upsertContextFile(withPluginContext(contextFile)),
285
304
  removeContextFile: (key) => this.removeContextFile(key),
@@ -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.4",
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
- ".codex/skills/pi-agent-harness/SKILL.md",
10
- ".codex/skills/pi-agent-harness/agents/openai.yaml",
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
  ],