@polpo-ai/node 0.11.1
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 +13 -0
- package/dist/adapters/engine.d.ts +51 -0
- package/dist/adapters/engine.d.ts.map +1 -0
- package/dist/adapters/engine.js +316 -0
- package/dist/adapters/engine.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/loop-engine.d.ts +36 -0
- package/dist/adapters/loop-engine.d.ts.map +1 -0
- package/dist/adapters/loop-engine.js +534 -0
- package/dist/adapters/loop-engine.js.map +1 -0
- package/dist/adapters/node-filesystem.d.ts +22 -0
- package/dist/adapters/node-filesystem.d.ts.map +1 -0
- package/dist/adapters/node-filesystem.js +59 -0
- package/dist/adapters/node-filesystem.js.map +1 -0
- package/dist/adapters/node-shell.d.ts +5 -0
- package/dist/adapters/node-shell.d.ts.map +1 -0
- package/dist/adapters/node-shell.js +34 -0
- package/dist/adapters/node-shell.js.map +1 -0
- package/dist/adapters/node-spawner.d.ts +14 -0
- package/dist/adapters/node-spawner.d.ts.map +1 -0
- package/dist/adapters/node-spawner.js +103 -0
- package/dist/adapters/node-spawner.js.map +1 -0
- package/dist/assessment/assessor.d.ts +6 -0
- package/dist/assessment/assessor.d.ts.map +1 -0
- package/dist/assessment/assessor.js +37 -0
- package/dist/assessment/assessor.js.map +1 -0
- package/dist/assessment/index.d.ts +6 -0
- package/dist/assessment/index.d.ts.map +1 -0
- package/dist/assessment/index.js +6 -0
- package/dist/assessment/index.js.map +1 -0
- package/dist/assessment/llm-review.d.ts +34 -0
- package/dist/assessment/llm-review.d.ts.map +1 -0
- package/dist/assessment/llm-review.js +669 -0
- package/dist/assessment/llm-review.js.map +1 -0
- package/dist/assessment/schemas.d.ts +5 -0
- package/dist/assessment/schemas.d.ts.map +1 -0
- package/dist/assessment/schemas.js +5 -0
- package/dist/assessment/schemas.js.map +1 -0
- package/dist/assessment/scoring.d.ts +5 -0
- package/dist/assessment/scoring.d.ts.map +1 -0
- package/dist/assessment/scoring.js +5 -0
- package/dist/assessment/scoring.js.map +1 -0
- package/dist/assessment/transcript-parser.d.ts +31 -0
- package/dist/assessment/transcript-parser.d.ts.map +1 -0
- package/dist/assessment/transcript-parser.js +203 -0
- package/dist/assessment/transcript-parser.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/core/adapter.d.ts +62 -0
- package/dist/core/adapter.d.ts.map +1 -0
- package/dist/core/adapter.js +2 -0
- package/dist/core/adapter.js.map +1 -0
- package/dist/core/assessment-orchestrator.d.ts +18 -0
- package/dist/core/assessment-orchestrator.d.ts.map +1 -0
- package/dist/core/assessment-orchestrator.js +93 -0
- package/dist/core/assessment-orchestrator.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +272 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +10 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +11 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/deadlock-resolver.d.ts +35 -0
- package/dist/core/deadlock-resolver.d.ts.map +1 -0
- package/dist/core/deadlock-resolver.js +317 -0
- package/dist/core/deadlock-resolver.js.map +1 -0
- package/dist/core/drizzle-sqlite-schema.d.ts +8 -0
- package/dist/core/drizzle-sqlite-schema.d.ts.map +1 -0
- package/dist/core/drizzle-sqlite-schema.js +250 -0
- package/dist/core/drizzle-sqlite-schema.js.map +1 -0
- package/dist/core/events.d.ts +26 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +39 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +31 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/ink.d.ts +165 -0
- package/dist/core/ink.d.ts.map +1 -0
- package/dist/core/ink.js +445 -0
- package/dist/core/ink.js.map +1 -0
- package/dist/core/orchestrator.d.ts +387 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +1015 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/question-detector.d.ts +19 -0
- package/dist/core/question-detector.d.ts.map +1 -0
- package/dist/core/question-detector.js +29 -0
- package/dist/core/question-detector.js.map +1 -0
- package/dist/core/runner.d.ts +14 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +299 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/session-reader.d.ts +46 -0
- package/dist/core/session-reader.d.ts.map +1 -0
- package/dist/core/session-reader.js +166 -0
- package/dist/core/session-reader.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/index.d.ts +8 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +21 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/pi-client.d.ts +8 -0
- package/dist/llm/pi-client.d.ts.map +1 -0
- package/dist/llm/pi-client.js +8 -0
- package/dist/llm/pi-client.js.map +1 -0
- package/dist/llm/prompts.d.ts +14 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +1618 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/retry.d.ts +5 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +5 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/llm/skills.d.ts +265 -0
- package/dist/llm/skills.d.ts.map +1 -0
- package/dist/llm/skills.js +804 -0
- package/dist/llm/skills.js.map +1 -0
- package/dist/quality/index.d.ts +3 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +3 -0
- package/dist/quality/index.js.map +1 -0
- package/dist/quality/quality-controller.d.ts +2 -0
- package/dist/quality/quality-controller.d.ts.map +1 -0
- package/dist/quality/quality-controller.js +2 -0
- package/dist/quality/quality-controller.js.map +1 -0
- package/dist/quality/sla-monitor.d.ts +2 -0
- package/dist/quality/sla-monitor.d.ts.map +1 -0
- package/dist/quality/sla-monitor.js +2 -0
- package/dist/quality/sla-monitor.js.map +1 -0
- package/dist/scheduling/cron.d.ts +2 -0
- package/dist/scheduling/cron.d.ts.map +1 -0
- package/dist/scheduling/cron.js +2 -0
- package/dist/scheduling/cron.js.map +1 -0
- package/dist/scheduling/index.d.ts +3 -0
- package/dist/scheduling/index.d.ts.map +1 -0
- package/dist/scheduling/index.js +3 -0
- package/dist/scheduling/index.js.map +1 -0
- package/dist/scheduling/scheduler.d.ts +2 -0
- package/dist/scheduling/scheduler.d.ts.map +1 -0
- package/dist/scheduling/scheduler.js +2 -0
- package/dist/scheduling/scheduler.js.map +1 -0
- package/dist/server/app.d.ts +19 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +302 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/deps.d.ts +157 -0
- package/dist/server/deps.d.ts.map +1 -0
- package/dist/server/deps.js +9 -0
- package/dist/server/deps.js.map +1 -0
- package/dist/server/index.d.ts +43 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +128 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware/auth.d.ts +3 -0
- package/dist/server/middleware/auth.d.ts.map +1 -0
- package/dist/server/middleware/auth.js +35 -0
- package/dist/server/middleware/auth.js.map +1 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.d.ts.map +1 -0
- package/dist/server/middleware/error.js +44 -0
- package/dist/server/middleware/error.js.map +1 -0
- package/dist/server/middleware/rate-limit.d.ts +16 -0
- package/dist/server/middleware/rate-limit.d.ts.map +1 -0
- package/dist/server/middleware/rate-limit.js +51 -0
- package/dist/server/middleware/rate-limit.js.map +1 -0
- package/dist/server/routes/config.d.ts +25 -0
- package/dist/server/routes/config.d.ts.map +1 -0
- package/dist/server/routes/config.js +446 -0
- package/dist/server/routes/config.js.map +1 -0
- package/dist/server/routes/files.d.ts +11 -0
- package/dist/server/routes/files.d.ts.map +1 -0
- package/dist/server/routes/files.js +514 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/filesystem.d.ts +3 -0
- package/dist/server/routes/filesystem.d.ts.map +1 -0
- package/dist/server/routes/filesystem.js +152 -0
- package/dist/server/routes/filesystem.js.map +1 -0
- package/dist/server/routes/providers.d.ts +7 -0
- package/dist/server/routes/providers.d.ts.map +1 -0
- package/dist/server/routes/providers.js +256 -0
- package/dist/server/routes/providers.js.map +1 -0
- package/dist/server/routes/skills.d.ts +11 -0
- package/dist/server/routes/skills.d.ts.map +1 -0
- package/dist/server/routes/skills.js +543 -0
- package/dist/server/routes/skills.js.map +1 -0
- package/dist/server/security.d.ts +34 -0
- package/dist/server/security.d.ts.map +1 -0
- package/dist/server/security.js +118 -0
- package/dist/server/security.js.map +1 -0
- package/dist/server/sse-bridge.d.ts +36 -0
- package/dist/server/sse-bridge.d.ts.map +1 -0
- package/dist/server/sse-bridge.js +110 -0
- package/dist/server/sse-bridge.js.map +1 -0
- package/dist/server/types.d.ts +69 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/setup/env-persistence.d.ts +9 -0
- package/dist/setup/env-persistence.d.ts.map +1 -0
- package/dist/setup/env-persistence.js +42 -0
- package/dist/setup/env-persistence.js.map +1 -0
- package/dist/setup/index.d.ts +32 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +24 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/models.d.ts +20 -0
- package/dist/setup/models.d.ts.map +1 -0
- package/dist/setup/models.js +33 -0
- package/dist/setup/models.js.map +1 -0
- package/dist/setup/providers.d.ts +26 -0
- package/dist/setup/providers.d.ts.map +1 -0
- package/dist/setup/providers.js +31 -0
- package/dist/setup/providers.js.map +1 -0
- package/dist/vault/encrypted-store.d.ts +45 -0
- package/dist/vault/encrypted-store.d.ts.map +1 -0
- package/dist/vault/encrypted-store.js +149 -0
- package/dist/vault/encrypted-store.js.map +1 -0
- package/dist/vault/index.d.ts +4 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/vault/index.js +3 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/resolver.d.ts +8 -0
- package/dist/vault/resolver.d.ts.map +1 -0
- package/dist/vault/resolver.js +8 -0
- package/dist/vault/resolver.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polpo Skills System
|
|
3
|
+
*
|
|
4
|
+
* Skills are markdown files (SKILL.md) with YAML frontmatter that provide
|
|
5
|
+
* specialized knowledge and workflows to agents. They are loaded as additional
|
|
6
|
+
* system prompt context at spawn time.
|
|
7
|
+
*
|
|
8
|
+
* Filesystem layout:
|
|
9
|
+
*
|
|
10
|
+
* .polpo/skills/ ← shared skill pool (installed by `polpo skills add`)
|
|
11
|
+
* frontend-design/SKILL.md
|
|
12
|
+
* testing/SKILL.md
|
|
13
|
+
*
|
|
14
|
+
* .polpo/agents/ ← per-agent skill assignment via symlinks
|
|
15
|
+
* dev-1/skills/
|
|
16
|
+
* frontend-design -> ../../../skills/frontend-design
|
|
17
|
+
* testing -> ../../../skills/testing
|
|
18
|
+
* reviewer/skills/
|
|
19
|
+
* testing -> ../../../skills/testing
|
|
20
|
+
*
|
|
21
|
+
* Discovery:
|
|
22
|
+
*
|
|
23
|
+
* Project-level:
|
|
24
|
+
* .polpo/skills/ ← primary (managed by Polpo)
|
|
25
|
+
*
|
|
26
|
+
* User-level:
|
|
27
|
+
* ~/.polpo/skills/ ← global skills shared across all projects
|
|
28
|
+
*
|
|
29
|
+
* Assignment priority:
|
|
30
|
+
* 1. .polpo/agents/<name>/skills/ (symlinks → hard enforcement)
|
|
31
|
+
* 2. AgentConfig.skills[] names resolved against the pool (soft/config-based)
|
|
32
|
+
*/
|
|
33
|
+
import { resolve, basename, join } from "node:path";
|
|
34
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, lstatSync, realpathSync, mkdirSync, symlinkSync, rmSync, cpSync, } from "node:fs";
|
|
35
|
+
import { tmpdir } from "node:os";
|
|
36
|
+
import { execSync } from "node:child_process";
|
|
37
|
+
import { parse as parseYaml } from "yaml";
|
|
38
|
+
import { getPolpoDir, getGlobalPolpoDir, POLPO_DIR_NAME } from "../core/constants.js";
|
|
39
|
+
// ── Parsing ──
|
|
40
|
+
/**
|
|
41
|
+
* Parse SKILL.md YAML frontmatter.
|
|
42
|
+
* Returns null if no frontmatter block found at all.
|
|
43
|
+
*
|
|
44
|
+
* Note: `name` is NOT required in frontmatter — the skills.sh spec only
|
|
45
|
+
* requires `name` + `description`, but the name can fall back to the
|
|
46
|
+
* directory name at the caller site. We return `name` as undefined when
|
|
47
|
+
* the frontmatter doesn't contain it.
|
|
48
|
+
*/
|
|
49
|
+
export function parseSkillFrontmatter(content) {
|
|
50
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
51
|
+
if (!match)
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
const fm = parseYaml(match[1]);
|
|
55
|
+
if (!fm || typeof fm !== "object")
|
|
56
|
+
return null;
|
|
57
|
+
// Must have at least name or description to be considered a valid skill
|
|
58
|
+
if (!fm.name && !fm.description)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
name: fm.name ?? undefined,
|
|
62
|
+
description: fm.description ?? "",
|
|
63
|
+
allowedTools: fm["allowed-tools"] ?? fm.allowedTools,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Extract the markdown body (everything after the frontmatter block). */
|
|
71
|
+
function extractBody(content) {
|
|
72
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
73
|
+
return match ? match[1].trim() : content.trim();
|
|
74
|
+
}
|
|
75
|
+
// ── Discovery ──
|
|
76
|
+
/** Scan a single skills directory and return discovered skills. */
|
|
77
|
+
function scanSkillsDir(dir, source) {
|
|
78
|
+
const skills = [];
|
|
79
|
+
if (!existsSync(dir))
|
|
80
|
+
return skills;
|
|
81
|
+
try {
|
|
82
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
83
|
+
// Follow symlinks — the entry might be a symlink to a skill dir
|
|
84
|
+
const entryPath = resolve(dir, entry.name);
|
|
85
|
+
let isDir = entry.isDirectory();
|
|
86
|
+
if (entry.isSymbolicLink()) {
|
|
87
|
+
try {
|
|
88
|
+
const real = realpathSync(entryPath);
|
|
89
|
+
const stat = lstatSync(real);
|
|
90
|
+
isDir = stat.isDirectory();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue; /* broken symlink */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!isDir)
|
|
97
|
+
continue;
|
|
98
|
+
const skillPath = resolve(entryPath, "SKILL.md");
|
|
99
|
+
if (!existsSync(skillPath))
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
const raw = readFileSync(skillPath, "utf-8");
|
|
103
|
+
const fm = parseSkillFrontmatter(raw);
|
|
104
|
+
// Use frontmatter name if available, otherwise directory name
|
|
105
|
+
const name = fm?.name ?? entry.name;
|
|
106
|
+
skills.push({
|
|
107
|
+
name,
|
|
108
|
+
description: fm?.description ?? "",
|
|
109
|
+
allowedTools: fm?.allowedTools,
|
|
110
|
+
source,
|
|
111
|
+
path: entryPath,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch { /* skip unreadable */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch { /* skip unreadable dir */ }
|
|
118
|
+
return skills;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Discover ALL available skills across all sources.
|
|
122
|
+
* Returns deduplicated list (first occurrence wins by name).
|
|
123
|
+
*
|
|
124
|
+
* Search order:
|
|
125
|
+
* 1. <polpoDir>/skills/ — project-level pool (managed by `polpo skills add`)
|
|
126
|
+
* 2. ~/.polpo/skills/ — user-level global pool (shared across projects)
|
|
127
|
+
*/
|
|
128
|
+
export function discoverSkills(cwd, polpoDir) {
|
|
129
|
+
const effectivePolpoDir = polpoDir ?? getPolpoDir(cwd);
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const all = [];
|
|
132
|
+
const dirs = [
|
|
133
|
+
{ dir: resolve(effectivePolpoDir, "skills"), source: "project" },
|
|
134
|
+
{ dir: resolve(getGlobalPolpoDir(), "skills"), source: "global" },
|
|
135
|
+
];
|
|
136
|
+
for (const { dir, source } of dirs) {
|
|
137
|
+
for (const skill of scanSkillsDir(dir, source)) {
|
|
138
|
+
if (!seen.has(skill.name)) {
|
|
139
|
+
seen.add(skill.name);
|
|
140
|
+
all.push(skill);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Enrich with index metadata (tags, category) from skills-index.json
|
|
145
|
+
const index = loadSkillIndex(effectivePolpoDir);
|
|
146
|
+
if (index) {
|
|
147
|
+
for (const skill of all) {
|
|
148
|
+
const entry = index[skill.name];
|
|
149
|
+
if (entry) {
|
|
150
|
+
if (entry.tags)
|
|
151
|
+
skill.tags = entry.tags;
|
|
152
|
+
if (entry.category)
|
|
153
|
+
skill.category = entry.category;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return all;
|
|
158
|
+
}
|
|
159
|
+
// ── Skills Index (tags & categories) ──
|
|
160
|
+
const SKILLS_INDEX_FILE = "skills-index.json";
|
|
161
|
+
/**
|
|
162
|
+
* Load the skills index from `.polpo/skills-index.json`.
|
|
163
|
+
* Returns null if the file doesn't exist or is invalid.
|
|
164
|
+
*/
|
|
165
|
+
export function loadSkillIndex(polpoDir) {
|
|
166
|
+
const indexPath = join(polpoDir, SKILLS_INDEX_FILE);
|
|
167
|
+
if (!existsSync(indexPath))
|
|
168
|
+
return null;
|
|
169
|
+
try {
|
|
170
|
+
const raw = readFileSync(indexPath, "utf-8");
|
|
171
|
+
const parsed = JSON.parse(raw);
|
|
172
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
173
|
+
return null;
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Save the full skills index to `.polpo/skills-index.json`.
|
|
182
|
+
*/
|
|
183
|
+
export function saveSkillIndex(polpoDir, index) {
|
|
184
|
+
const indexPath = join(polpoDir, SKILLS_INDEX_FILE);
|
|
185
|
+
mkdirSync(polpoDir, { recursive: true });
|
|
186
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2) + "\n", "utf-8");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Update a single skill's entry in the skills index.
|
|
190
|
+
* Creates the index file if it doesn't exist.
|
|
191
|
+
* Merges with existing entry (tags/category are replaced individually).
|
|
192
|
+
*/
|
|
193
|
+
export function updateSkillIndex(polpoDir, skillName, entry) {
|
|
194
|
+
const index = loadSkillIndex(polpoDir) ?? {};
|
|
195
|
+
index[skillName] = { ...index[skillName], ...entry };
|
|
196
|
+
// Remove empty fields
|
|
197
|
+
if (index[skillName].tags?.length === 0)
|
|
198
|
+
delete index[skillName].tags;
|
|
199
|
+
if (!index[skillName].category)
|
|
200
|
+
delete index[skillName].category;
|
|
201
|
+
// Remove empty entries
|
|
202
|
+
if (Object.keys(index[skillName]).length === 0)
|
|
203
|
+
delete index[skillName];
|
|
204
|
+
saveSkillIndex(polpoDir, index);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Remove a skill's entry from the skills index.
|
|
208
|
+
*/
|
|
209
|
+
export function removeSkillFromIndex(polpoDir, skillName) {
|
|
210
|
+
const index = loadSkillIndex(polpoDir);
|
|
211
|
+
if (!index || !index[skillName])
|
|
212
|
+
return;
|
|
213
|
+
delete index[skillName];
|
|
214
|
+
saveSkillIndex(polpoDir, index);
|
|
215
|
+
}
|
|
216
|
+
// ── Per-agent loading ──
|
|
217
|
+
/**
|
|
218
|
+
* Get the skills assigned to a specific agent.
|
|
219
|
+
*
|
|
220
|
+
* Priority:
|
|
221
|
+
* 1. .polpo/agents/<agentName>/skills/ directory (symlinks to pool skills)
|
|
222
|
+
* 2. AgentConfig.skills[] names resolved against the full pool
|
|
223
|
+
*
|
|
224
|
+
* Returns loaded skills with full content ready for system prompt injection.
|
|
225
|
+
*/
|
|
226
|
+
export function loadAgentSkills(cwd, polpoDir, agentName, configSkillNames) {
|
|
227
|
+
const agentSkillsDir = resolve(polpoDir, "agents", agentName, "skills");
|
|
228
|
+
// Strategy 1: agent has a skills dir with symlinks → hard enforcement
|
|
229
|
+
if (existsSync(agentSkillsDir)) {
|
|
230
|
+
const skills = scanSkillsDir(agentSkillsDir, "project");
|
|
231
|
+
return skills.map(s => loadSkillContent(s)).filter((s) => s !== null);
|
|
232
|
+
}
|
|
233
|
+
// Strategy 2: resolve config skill names against the pool
|
|
234
|
+
if (configSkillNames && configSkillNames.length > 0) {
|
|
235
|
+
const pool = discoverSkills(cwd, polpoDir);
|
|
236
|
+
const poolMap = new Map(pool.map(s => [s.name, s]));
|
|
237
|
+
const loaded = [];
|
|
238
|
+
for (const name of configSkillNames) {
|
|
239
|
+
const info = poolMap.get(name);
|
|
240
|
+
if (info) {
|
|
241
|
+
const skill = loadSkillContent(info);
|
|
242
|
+
if (skill)
|
|
243
|
+
loaded.push(skill);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return loaded;
|
|
247
|
+
}
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
/** Load SKILL.md content for a discovered skill. Returns null if unreadable. */
|
|
251
|
+
export function loadSkillContent(info) {
|
|
252
|
+
const skillPath = resolve(info.path, "SKILL.md");
|
|
253
|
+
try {
|
|
254
|
+
const raw = readFileSync(skillPath, "utf-8");
|
|
255
|
+
return {
|
|
256
|
+
...info,
|
|
257
|
+
content: extractBody(raw),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get a skill's full content by name.
|
|
266
|
+
* Searches the specified pool (agent or orchestrator) and returns the loaded skill,
|
|
267
|
+
* or null if not found / unreadable.
|
|
268
|
+
*/
|
|
269
|
+
export function getSkillByName(cwd, polpoDir, name, pool = "agent") {
|
|
270
|
+
const skills = pool === "orchestrator"
|
|
271
|
+
? discoverOrchestratorSkills(polpoDir)
|
|
272
|
+
: discoverSkills(cwd, polpoDir);
|
|
273
|
+
const info = skills.find(s => s.name === name);
|
|
274
|
+
if (!info)
|
|
275
|
+
return null;
|
|
276
|
+
return loadSkillContent(info);
|
|
277
|
+
}
|
|
278
|
+
// ── Skill assignment helpers ──
|
|
279
|
+
/**
|
|
280
|
+
* Assign a skill to an agent by creating a symlink.
|
|
281
|
+
* Creates .polpo/agents/<agentName>/skills/<skillName> → <skillPath>
|
|
282
|
+
*/
|
|
283
|
+
export function assignSkillToAgent(polpoDir, agentName, skillName, skillPath) {
|
|
284
|
+
const agentSkillsDir = resolve(polpoDir, "agents", agentName, "skills");
|
|
285
|
+
mkdirSync(agentSkillsDir, { recursive: true });
|
|
286
|
+
const linkPath = resolve(agentSkillsDir, skillName);
|
|
287
|
+
if (!existsSync(linkPath)) {
|
|
288
|
+
symlinkSync(skillPath, linkPath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Remove a skill assignment (symlink) from a specific agent.
|
|
293
|
+
* Returns true if the symlink existed and was removed, false otherwise.
|
|
294
|
+
*/
|
|
295
|
+
export function unassignSkillFromAgent(polpoDir, agentName, skillName) {
|
|
296
|
+
const linkPath = resolve(polpoDir, "agents", agentName, "skills", skillName);
|
|
297
|
+
if (!existsSync(linkPath))
|
|
298
|
+
return false;
|
|
299
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Build the skill injection block for an agent's system prompt.
|
|
304
|
+
* Returns empty string if no skills are assigned.
|
|
305
|
+
*/
|
|
306
|
+
export function buildSkillPrompt(skills) {
|
|
307
|
+
if (skills.length === 0)
|
|
308
|
+
return "";
|
|
309
|
+
const parts = [
|
|
310
|
+
`\n## Assigned Skills\n`,
|
|
311
|
+
`You have ${skills.length} skill${skills.length > 1 ? "s" : ""} loaded. Use this knowledge when applicable:\n`,
|
|
312
|
+
];
|
|
313
|
+
for (const skill of skills) {
|
|
314
|
+
parts.push(`### ${skill.name}`);
|
|
315
|
+
if (skill.description)
|
|
316
|
+
parts.push(`> ${skill.description}\n`);
|
|
317
|
+
parts.push(skill.content);
|
|
318
|
+
parts.push(""); // blank line between skills
|
|
319
|
+
}
|
|
320
|
+
return parts.join("\n");
|
|
321
|
+
}
|
|
322
|
+
export function parseSkillSource(input) {
|
|
323
|
+
// Local path
|
|
324
|
+
if (input.startsWith("/") || input.startsWith("./") || input.startsWith("../") || input === ".") {
|
|
325
|
+
return { type: "local", url: resolve(input) };
|
|
326
|
+
}
|
|
327
|
+
// Full GitHub URL
|
|
328
|
+
const ghUrlMatch = input.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
329
|
+
if (ghUrlMatch) {
|
|
330
|
+
const ownerRepo = ghUrlMatch[1].replace(/\.git$/, "");
|
|
331
|
+
return {
|
|
332
|
+
type: "github",
|
|
333
|
+
url: `https://github.com/${ownerRepo}.git`,
|
|
334
|
+
ownerRepo,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// owner/repo shorthand
|
|
338
|
+
if (/^[^/]+\/[^/]+$/.test(input)) {
|
|
339
|
+
return {
|
|
340
|
+
type: "github",
|
|
341
|
+
url: `https://github.com/${input}.git`,
|
|
342
|
+
ownerRepo: input,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
// Assume it's a git URL
|
|
346
|
+
return { type: "github", url: input };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Scan a directory tree for SKILL.md files.
|
|
350
|
+
* Returns an array of skill directories (parent of each SKILL.md).
|
|
351
|
+
*
|
|
352
|
+
* Searches known skill locations per the skills.sh spec:
|
|
353
|
+
* - Root (if SKILL.md exists)
|
|
354
|
+
* - skills/, .agents/skills/, .claude/skills/, .polpo/skills/
|
|
355
|
+
* - Any other subdirectory with SKILL.md (recursive, max 3 levels)
|
|
356
|
+
*/
|
|
357
|
+
function findSkillDirsInRepo(repoDir) {
|
|
358
|
+
const found = [];
|
|
359
|
+
// Check root
|
|
360
|
+
if (existsSync(join(repoDir, "SKILL.md"))) {
|
|
361
|
+
found.push(repoDir);
|
|
362
|
+
}
|
|
363
|
+
// Standard locations used by skills.sh repos
|
|
364
|
+
const standardDirs = [
|
|
365
|
+
"skills", ".agents/skills", ".claude/skills", ".polpo/skills",
|
|
366
|
+
];
|
|
367
|
+
for (const rel of standardDirs) {
|
|
368
|
+
const dir = join(repoDir, rel);
|
|
369
|
+
if (!existsSync(dir))
|
|
370
|
+
continue;
|
|
371
|
+
for (const skill of scanSubdirs(dir)) {
|
|
372
|
+
found.push(skill);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// If nothing found in standard locations, recurse up to 3 levels
|
|
376
|
+
if (found.length === 0) {
|
|
377
|
+
deepScan(repoDir, 0, 3, found);
|
|
378
|
+
}
|
|
379
|
+
return found;
|
|
380
|
+
}
|
|
381
|
+
/** Scan immediate subdirectories for SKILL.md */
|
|
382
|
+
function scanSubdirs(dir) {
|
|
383
|
+
const results = [];
|
|
384
|
+
if (!existsSync(dir))
|
|
385
|
+
return results;
|
|
386
|
+
try {
|
|
387
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
388
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
389
|
+
continue;
|
|
390
|
+
const entryPath = resolve(dir, entry.name);
|
|
391
|
+
if (existsSync(join(entryPath, "SKILL.md"))) {
|
|
392
|
+
results.push(entryPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch { /* skip */ }
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
/** Recursive scan for SKILL.md up to maxDepth. */
|
|
400
|
+
function deepScan(dir, depth, maxDepth, results) {
|
|
401
|
+
if (depth > maxDepth)
|
|
402
|
+
return;
|
|
403
|
+
try {
|
|
404
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
405
|
+
if (entry.name.startsWith(".") && entry.name !== POLPO_DIR_NAME && entry.name !== ".agents")
|
|
406
|
+
continue;
|
|
407
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
408
|
+
continue;
|
|
409
|
+
if (!entry.isDirectory())
|
|
410
|
+
continue;
|
|
411
|
+
const entryPath = resolve(dir, entry.name);
|
|
412
|
+
if (existsSync(join(entryPath, "SKILL.md"))) {
|
|
413
|
+
results.push(entryPath);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
deepScan(entryPath, depth + 1, maxDepth, results);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch { /* skip */ }
|
|
421
|
+
}
|
|
422
|
+
/** Read SKILL.md from a directory and extract metadata. */
|
|
423
|
+
function readFoundSkill(skillDir) {
|
|
424
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
425
|
+
if (!existsSync(skillFile))
|
|
426
|
+
return null;
|
|
427
|
+
try {
|
|
428
|
+
const raw = readFileSync(skillFile, "utf-8");
|
|
429
|
+
const fm = parseSkillFrontmatter(raw);
|
|
430
|
+
const dirName = basename(skillDir);
|
|
431
|
+
return {
|
|
432
|
+
name: fm?.name ?? dirName,
|
|
433
|
+
description: fm?.description ?? "",
|
|
434
|
+
path: skillDir,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Install skills from a source (GitHub repo or local path) into the
|
|
443
|
+
* project's .polpo/skills/ pool.
|
|
444
|
+
*
|
|
445
|
+
* @param source - GitHub owner/repo, full URL, or local path
|
|
446
|
+
* @param polpoDir - The .polpo directory path
|
|
447
|
+
* @param options.skillNames - Only install specific skill names (undefined = all)
|
|
448
|
+
* @param options.global - Install to ~/.polpo/skills/ instead of project
|
|
449
|
+
* @param options.force - Overwrite existing skills
|
|
450
|
+
*/
|
|
451
|
+
export function installSkills(source, polpoDir, options = {}) {
|
|
452
|
+
const result = { installed: [], skipped: [], errors: [] };
|
|
453
|
+
const parsed = parseSkillSource(source);
|
|
454
|
+
let sourceDir;
|
|
455
|
+
let clonedTmpDir = null;
|
|
456
|
+
// Resolve source to a local directory
|
|
457
|
+
if (parsed.type === "local") {
|
|
458
|
+
if (!existsSync(parsed.url)) {
|
|
459
|
+
result.errors.push(`Local path not found: ${parsed.url}`);
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
sourceDir = parsed.url;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
// Clone to tmp
|
|
466
|
+
try {
|
|
467
|
+
clonedTmpDir = join(tmpdir(), `polpo-skills-${Date.now()}`);
|
|
468
|
+
execSync(`git clone --depth 1 --quiet "${parsed.url}" "${clonedTmpDir}"`, {
|
|
469
|
+
stdio: "pipe",
|
|
470
|
+
timeout: 60_000,
|
|
471
|
+
});
|
|
472
|
+
sourceDir = clonedTmpDir;
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
476
|
+
result.errors.push(`Failed to clone ${parsed.url}: ${msg}`);
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
// Discover skills in source
|
|
482
|
+
const skillDirs = findSkillDirsInRepo(sourceDir);
|
|
483
|
+
const found = [];
|
|
484
|
+
for (const dir of skillDirs) {
|
|
485
|
+
const skill = readFoundSkill(dir);
|
|
486
|
+
if (skill)
|
|
487
|
+
found.push(skill);
|
|
488
|
+
}
|
|
489
|
+
if (found.length === 0) {
|
|
490
|
+
result.errors.push(`No skills found in ${source}`);
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
// Filter by requested names
|
|
494
|
+
const toInstall = options.skillNames
|
|
495
|
+
? found.filter(s => options.skillNames.includes(s.name))
|
|
496
|
+
: found;
|
|
497
|
+
if (options.skillNames && toInstall.length === 0) {
|
|
498
|
+
result.errors.push(`Requested skills not found: ${options.skillNames.join(", ")}. ` +
|
|
499
|
+
`Available: ${found.map(s => s.name).join(", ")}`);
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
// Target directory
|
|
503
|
+
const targetBase = options.global
|
|
504
|
+
? join(getGlobalPolpoDir(), "skills")
|
|
505
|
+
: join(polpoDir, "skills");
|
|
506
|
+
mkdirSync(targetBase, { recursive: true });
|
|
507
|
+
// Install each skill
|
|
508
|
+
for (const skill of toInstall) {
|
|
509
|
+
const targetDir = join(targetBase, skill.name);
|
|
510
|
+
if (existsSync(targetDir) && !options.force) {
|
|
511
|
+
result.skipped.push(skill);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
// Remove existing if force
|
|
516
|
+
if (existsSync(targetDir)) {
|
|
517
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
518
|
+
}
|
|
519
|
+
// Copy skill directory
|
|
520
|
+
cpSync(skill.path, targetDir, { recursive: true });
|
|
521
|
+
result.installed.push(skill);
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
525
|
+
result.errors.push(`Failed to install "${skill.name}": ${msg}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
// Cleanup cloned repo
|
|
531
|
+
if (clonedTmpDir && existsSync(clonedTmpDir)) {
|
|
532
|
+
try {
|
|
533
|
+
rmSync(clonedTmpDir, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
catch { /* best effort */ }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Remove a skill from the pool.
|
|
542
|
+
* Returns true if removed, false if not found.
|
|
543
|
+
*/
|
|
544
|
+
export function removeSkill(polpoDir, name, global = false) {
|
|
545
|
+
const targetBase = global
|
|
546
|
+
? join(getGlobalPolpoDir(), "skills")
|
|
547
|
+
: join(polpoDir, "skills");
|
|
548
|
+
const targetDir = join(targetBase, name);
|
|
549
|
+
if (!existsSync(targetDir))
|
|
550
|
+
return false;
|
|
551
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Create a new skill in the agent skill pool (.polpo/skills/).
|
|
556
|
+
* Writes a SKILL.md with YAML frontmatter and markdown body.
|
|
557
|
+
* Returns the absolute path to the created skill directory.
|
|
558
|
+
*/
|
|
559
|
+
export function createAgentSkill(polpoDir, name, description, content, options) {
|
|
560
|
+
const targetBase = options?.global
|
|
561
|
+
? join(getGlobalPolpoDir(), "skills")
|
|
562
|
+
: join(polpoDir, "skills");
|
|
563
|
+
const targetDir = join(targetBase, name);
|
|
564
|
+
mkdirSync(targetDir, { recursive: true });
|
|
565
|
+
const fmLines = [`---`, `name: ${name}`, `description: ${description}`];
|
|
566
|
+
if (options?.allowedTools?.length) {
|
|
567
|
+
fmLines.push(`allowed-tools:`);
|
|
568
|
+
for (const t of options.allowedTools)
|
|
569
|
+
fmLines.push(` - ${t}`);
|
|
570
|
+
}
|
|
571
|
+
fmLines.push(`---`, ``);
|
|
572
|
+
const skillMd = fmLines.join("\n") + content;
|
|
573
|
+
writeFileSync(join(targetDir, "SKILL.md"), skillMd, "utf-8");
|
|
574
|
+
return targetDir;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* List skills with their per-agent assignments.
|
|
578
|
+
*
|
|
579
|
+
* Checks both assignment methods:
|
|
580
|
+
* 1. Symlinks in .polpo/agents/<name>/skills/ (hard enforcement)
|
|
581
|
+
* 2. AgentConfig.skills[] names (soft/config-based)
|
|
582
|
+
*
|
|
583
|
+
* @param agentNames - All known agent names (from config + filesystem)
|
|
584
|
+
* @param agentConfigSkills - Optional map of agentName → configured skill names
|
|
585
|
+
* (from AgentConfig.skills[]). When provided, config-based assignments are included.
|
|
586
|
+
*/
|
|
587
|
+
export function listSkillsWithAssignments(cwd, polpoDir, agentNames, agentConfigSkills) {
|
|
588
|
+
const pool = discoverSkills(cwd, polpoDir);
|
|
589
|
+
const result = [];
|
|
590
|
+
for (const skill of pool) {
|
|
591
|
+
const assignedTo = new Set();
|
|
592
|
+
for (const agentName of agentNames) {
|
|
593
|
+
// Strategy 1: check symlink in .polpo/agents/<name>/skills/<skillName>
|
|
594
|
+
const agentSkillsDir = resolve(polpoDir, "agents", agentName, "skills");
|
|
595
|
+
if (existsSync(agentSkillsDir)) {
|
|
596
|
+
const linkPath = resolve(agentSkillsDir, skill.name);
|
|
597
|
+
if (existsSync(linkPath)) {
|
|
598
|
+
assignedTo.add(agentName);
|
|
599
|
+
continue; // already assigned, skip config check
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Strategy 2: check AgentConfig.skills[] from config
|
|
603
|
+
const configSkills = agentConfigSkills?.get(agentName);
|
|
604
|
+
if (configSkills?.includes(skill.name)) {
|
|
605
|
+
assignedTo.add(agentName);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
result.push({ ...skill, assignedTo: [...assignedTo] });
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
// ═══════════════════════════════════════════════════════
|
|
613
|
+
// ORCHESTRATOR SKILLS — separate pool in .polpo/.agent/skills/
|
|
614
|
+
// ═══════════════════════════════════════════════════════
|
|
615
|
+
/** The subdirectory name for the orchestrator's own config/skills. */
|
|
616
|
+
const ORCHESTRATOR_AGENT_DIR = ".agent";
|
|
617
|
+
/**
|
|
618
|
+
* Discover skills available to the orchestrator.
|
|
619
|
+
*
|
|
620
|
+
* Search order:
|
|
621
|
+
* 1. <polpoDir>/.agent/skills/ — project-level orchestrator skills
|
|
622
|
+
* 2. ~/.polpo/.agent/skills/ — global orchestrator skills
|
|
623
|
+
*/
|
|
624
|
+
export function discoverOrchestratorSkills(polpoDir) {
|
|
625
|
+
const seen = new Set();
|
|
626
|
+
const all = [];
|
|
627
|
+
const dirs = [
|
|
628
|
+
{ dir: resolve(polpoDir, ORCHESTRATOR_AGENT_DIR, "skills"), source: "project" },
|
|
629
|
+
{ dir: resolve(getGlobalPolpoDir(), ORCHESTRATOR_AGENT_DIR, "skills"), source: "global" },
|
|
630
|
+
];
|
|
631
|
+
for (const { dir, source } of dirs) {
|
|
632
|
+
for (const skill of scanSkillsDir(dir, source)) {
|
|
633
|
+
if (!seen.has(skill.name)) {
|
|
634
|
+
seen.add(skill.name);
|
|
635
|
+
all.push(skill);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return all;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Load orchestrator skills by name from the orchestrator pool.
|
|
643
|
+
*
|
|
644
|
+
* If `skillNames` is provided, only those skills are loaded.
|
|
645
|
+
* If omitted or empty, ALL discovered orchestrator skills are loaded.
|
|
646
|
+
*/
|
|
647
|
+
export function loadOrchestratorSkills(polpoDir, skillNames) {
|
|
648
|
+
const pool = discoverOrchestratorSkills(polpoDir);
|
|
649
|
+
// If no filter, load everything in the pool
|
|
650
|
+
const toLoad = skillNames && skillNames.length > 0
|
|
651
|
+
? pool.filter(s => skillNames.includes(s.name))
|
|
652
|
+
: pool;
|
|
653
|
+
return toLoad
|
|
654
|
+
.map(s => loadSkillContent(s))
|
|
655
|
+
.filter((s) => s !== null);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Install skills into the orchestrator's pool (.polpo/.agent/skills/).
|
|
659
|
+
*
|
|
660
|
+
* Same mechanics as `installSkills()` but targets the orchestrator directory.
|
|
661
|
+
*/
|
|
662
|
+
export function installOrchestratorSkills(source, polpoDir, options = {}) {
|
|
663
|
+
const result = { installed: [], skipped: [], errors: [] };
|
|
664
|
+
const parsed = parseSkillSource(source);
|
|
665
|
+
let sourceDir;
|
|
666
|
+
let clonedTmpDir = null;
|
|
667
|
+
if (parsed.type === "local") {
|
|
668
|
+
if (!existsSync(parsed.url)) {
|
|
669
|
+
result.errors.push(`Local path not found: ${parsed.url}`);
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
sourceDir = parsed.url;
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
try {
|
|
676
|
+
clonedTmpDir = join(tmpdir(), `polpo-orch-skills-${Date.now()}`);
|
|
677
|
+
execSync(`git clone --depth 1 --quiet "${parsed.url}" "${clonedTmpDir}"`, {
|
|
678
|
+
stdio: "pipe",
|
|
679
|
+
timeout: 60_000,
|
|
680
|
+
});
|
|
681
|
+
sourceDir = clonedTmpDir;
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
685
|
+
result.errors.push(`Failed to clone ${parsed.url}: ${msg}`);
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const skillDirs = findSkillDirsInRepo(sourceDir);
|
|
691
|
+
const found = [];
|
|
692
|
+
for (const dir of skillDirs) {
|
|
693
|
+
const skill = readFoundSkill(dir);
|
|
694
|
+
if (skill)
|
|
695
|
+
found.push(skill);
|
|
696
|
+
}
|
|
697
|
+
if (found.length === 0) {
|
|
698
|
+
result.errors.push(`No skills found in ${source}`);
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
const toInstall = options.skillNames
|
|
702
|
+
? found.filter(s => options.skillNames.includes(s.name))
|
|
703
|
+
: found;
|
|
704
|
+
if (options.skillNames && toInstall.length === 0) {
|
|
705
|
+
result.errors.push(`Requested skills not found: ${options.skillNames.join(", ")}. ` +
|
|
706
|
+
`Available: ${found.map(s => s.name).join(", ")}`);
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
const targetBase = options.global
|
|
710
|
+
? join(getGlobalPolpoDir(), ORCHESTRATOR_AGENT_DIR, "skills")
|
|
711
|
+
: join(polpoDir, ORCHESTRATOR_AGENT_DIR, "skills");
|
|
712
|
+
mkdirSync(targetBase, { recursive: true });
|
|
713
|
+
for (const skill of toInstall) {
|
|
714
|
+
const targetDir = join(targetBase, skill.name);
|
|
715
|
+
if (existsSync(targetDir) && !options.force) {
|
|
716
|
+
result.skipped.push(skill);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
if (existsSync(targetDir)) {
|
|
721
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
722
|
+
}
|
|
723
|
+
cpSync(skill.path, targetDir, { recursive: true });
|
|
724
|
+
result.installed.push(skill);
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
728
|
+
result.errors.push(`Failed to install "${skill.name}": ${msg}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
finally {
|
|
733
|
+
if (clonedTmpDir && existsSync(clonedTmpDir)) {
|
|
734
|
+
try {
|
|
735
|
+
rmSync(clonedTmpDir, { recursive: true, force: true });
|
|
736
|
+
}
|
|
737
|
+
catch { /* best effort */ }
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Remove a skill from the orchestrator's pool.
|
|
744
|
+
* Returns true if removed, false if not found.
|
|
745
|
+
*/
|
|
746
|
+
export function removeOrchestratorSkill(polpoDir, name, global = false) {
|
|
747
|
+
const targetBase = global
|
|
748
|
+
? join(getGlobalPolpoDir(), ORCHESTRATOR_AGENT_DIR, "skills")
|
|
749
|
+
: join(polpoDir, ORCHESTRATOR_AGENT_DIR, "skills");
|
|
750
|
+
const targetDir = join(targetBase, name);
|
|
751
|
+
if (!existsSync(targetDir))
|
|
752
|
+
return false;
|
|
753
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Create a new skill in the orchestrator's pool by writing a SKILL.md file.
|
|
758
|
+
* Returns the absolute path to the created skill directory.
|
|
759
|
+
*/
|
|
760
|
+
export function createOrchestratorSkill(polpoDir, name, description, content, options) {
|
|
761
|
+
const targetBase = options?.global
|
|
762
|
+
? join(getGlobalPolpoDir(), ORCHESTRATOR_AGENT_DIR, "skills")
|
|
763
|
+
: join(polpoDir, ORCHESTRATOR_AGENT_DIR, "skills");
|
|
764
|
+
const targetDir = join(targetBase, name);
|
|
765
|
+
mkdirSync(targetDir, { recursive: true });
|
|
766
|
+
const fmLines = [`---`, `name: ${name}`, `description: ${description}`];
|
|
767
|
+
if (options?.allowedTools?.length) {
|
|
768
|
+
fmLines.push(`allowed-tools:`);
|
|
769
|
+
for (const t of options.allowedTools)
|
|
770
|
+
fmLines.push(` - ${t}`);
|
|
771
|
+
}
|
|
772
|
+
fmLines.push(`---`, ``);
|
|
773
|
+
const skillMd = fmLines.join("\n") + content;
|
|
774
|
+
writeFileSync(join(targetDir, "SKILL.md"), skillMd, "utf-8");
|
|
775
|
+
return targetDir;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Update an existing skill in the orchestrator's pool.
|
|
779
|
+
* Only provided fields are changed. Returns true if updated, false if not found.
|
|
780
|
+
*/
|
|
781
|
+
export function updateOrchestratorSkill(polpoDir, name, updates, global = false) {
|
|
782
|
+
const targetBase = global
|
|
783
|
+
? join(getGlobalPolpoDir(), ORCHESTRATOR_AGENT_DIR, "skills")
|
|
784
|
+
: join(polpoDir, ORCHESTRATOR_AGENT_DIR, "skills");
|
|
785
|
+
const skillFile = join(targetBase, name, "SKILL.md");
|
|
786
|
+
if (!existsSync(skillFile))
|
|
787
|
+
return false;
|
|
788
|
+
const raw = readFileSync(skillFile, "utf-8");
|
|
789
|
+
const fm = parseSkillFrontmatter(raw);
|
|
790
|
+
const oldBody = extractBody(raw);
|
|
791
|
+
const newDesc = updates.description ?? fm?.description ?? "";
|
|
792
|
+
const newTools = updates.allowedTools ?? fm?.allowedTools;
|
|
793
|
+
const newBody = updates.content ?? oldBody;
|
|
794
|
+
const fmLines = [`---`, `name: ${name}`, `description: ${newDesc}`];
|
|
795
|
+
if (newTools?.length) {
|
|
796
|
+
fmLines.push(`allowed-tools:`);
|
|
797
|
+
for (const t of newTools)
|
|
798
|
+
fmLines.push(` - ${t}`);
|
|
799
|
+
}
|
|
800
|
+
fmLines.push(`---`, ``);
|
|
801
|
+
writeFileSync(skillFile, fmLines.join("\n") + newBody, "utf-8");
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
//# sourceMappingURL=skills.js.map
|