@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.
Files changed (238) hide show
  1. package/LICENSE +13 -0
  2. package/dist/adapters/engine.d.ts +51 -0
  3. package/dist/adapters/engine.d.ts.map +1 -0
  4. package/dist/adapters/engine.js +316 -0
  5. package/dist/adapters/engine.js.map +1 -0
  6. package/dist/adapters/index.d.ts +2 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +4 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/loop-engine.d.ts +36 -0
  11. package/dist/adapters/loop-engine.d.ts.map +1 -0
  12. package/dist/adapters/loop-engine.js +534 -0
  13. package/dist/adapters/loop-engine.js.map +1 -0
  14. package/dist/adapters/node-filesystem.d.ts +22 -0
  15. package/dist/adapters/node-filesystem.d.ts.map +1 -0
  16. package/dist/adapters/node-filesystem.js +59 -0
  17. package/dist/adapters/node-filesystem.js.map +1 -0
  18. package/dist/adapters/node-shell.d.ts +5 -0
  19. package/dist/adapters/node-shell.d.ts.map +1 -0
  20. package/dist/adapters/node-shell.js +34 -0
  21. package/dist/adapters/node-shell.js.map +1 -0
  22. package/dist/adapters/node-spawner.d.ts +14 -0
  23. package/dist/adapters/node-spawner.d.ts.map +1 -0
  24. package/dist/adapters/node-spawner.js +103 -0
  25. package/dist/adapters/node-spawner.js.map +1 -0
  26. package/dist/assessment/assessor.d.ts +6 -0
  27. package/dist/assessment/assessor.d.ts.map +1 -0
  28. package/dist/assessment/assessor.js +37 -0
  29. package/dist/assessment/assessor.js.map +1 -0
  30. package/dist/assessment/index.d.ts +6 -0
  31. package/dist/assessment/index.d.ts.map +1 -0
  32. package/dist/assessment/index.js +6 -0
  33. package/dist/assessment/index.js.map +1 -0
  34. package/dist/assessment/llm-review.d.ts +34 -0
  35. package/dist/assessment/llm-review.d.ts.map +1 -0
  36. package/dist/assessment/llm-review.js +669 -0
  37. package/dist/assessment/llm-review.js.map +1 -0
  38. package/dist/assessment/schemas.d.ts +5 -0
  39. package/dist/assessment/schemas.d.ts.map +1 -0
  40. package/dist/assessment/schemas.js +5 -0
  41. package/dist/assessment/schemas.js.map +1 -0
  42. package/dist/assessment/scoring.d.ts +5 -0
  43. package/dist/assessment/scoring.d.ts.map +1 -0
  44. package/dist/assessment/scoring.js +5 -0
  45. package/dist/assessment/scoring.js.map +1 -0
  46. package/dist/assessment/transcript-parser.d.ts +31 -0
  47. package/dist/assessment/transcript-parser.d.ts.map +1 -0
  48. package/dist/assessment/transcript-parser.js +203 -0
  49. package/dist/assessment/transcript-parser.js.map +1 -0
  50. package/dist/auth/index.d.ts +2 -0
  51. package/dist/auth/index.d.ts.map +1 -0
  52. package/dist/auth/index.js +4 -0
  53. package/dist/auth/index.js.map +1 -0
  54. package/dist/core/adapter.d.ts +62 -0
  55. package/dist/core/adapter.d.ts.map +1 -0
  56. package/dist/core/adapter.js +2 -0
  57. package/dist/core/adapter.js.map +1 -0
  58. package/dist/core/assessment-orchestrator.d.ts +18 -0
  59. package/dist/core/assessment-orchestrator.d.ts.map +1 -0
  60. package/dist/core/assessment-orchestrator.js +93 -0
  61. package/dist/core/assessment-orchestrator.js.map +1 -0
  62. package/dist/core/config.d.ts +17 -0
  63. package/dist/core/config.d.ts.map +1 -0
  64. package/dist/core/config.js +272 -0
  65. package/dist/core/config.js.map +1 -0
  66. package/dist/core/constants.d.ts +10 -0
  67. package/dist/core/constants.d.ts.map +1 -0
  68. package/dist/core/constants.js +11 -0
  69. package/dist/core/constants.js.map +1 -0
  70. package/dist/core/deadlock-resolver.d.ts +35 -0
  71. package/dist/core/deadlock-resolver.d.ts.map +1 -0
  72. package/dist/core/deadlock-resolver.js +317 -0
  73. package/dist/core/deadlock-resolver.js.map +1 -0
  74. package/dist/core/drizzle-sqlite-schema.d.ts +8 -0
  75. package/dist/core/drizzle-sqlite-schema.d.ts.map +1 -0
  76. package/dist/core/drizzle-sqlite-schema.js +250 -0
  77. package/dist/core/drizzle-sqlite-schema.js.map +1 -0
  78. package/dist/core/events.d.ts +26 -0
  79. package/dist/core/events.d.ts.map +1 -0
  80. package/dist/core/events.js +39 -0
  81. package/dist/core/events.js.map +1 -0
  82. package/dist/core/index.d.ts +37 -0
  83. package/dist/core/index.d.ts.map +1 -0
  84. package/dist/core/index.js +31 -0
  85. package/dist/core/index.js.map +1 -0
  86. package/dist/core/ink.d.ts +165 -0
  87. package/dist/core/ink.d.ts.map +1 -0
  88. package/dist/core/ink.js +445 -0
  89. package/dist/core/ink.js.map +1 -0
  90. package/dist/core/orchestrator.d.ts +387 -0
  91. package/dist/core/orchestrator.d.ts.map +1 -0
  92. package/dist/core/orchestrator.js +1015 -0
  93. package/dist/core/orchestrator.js.map +1 -0
  94. package/dist/core/question-detector.d.ts +19 -0
  95. package/dist/core/question-detector.d.ts.map +1 -0
  96. package/dist/core/question-detector.js +29 -0
  97. package/dist/core/question-detector.js.map +1 -0
  98. package/dist/core/runner.d.ts +14 -0
  99. package/dist/core/runner.d.ts.map +1 -0
  100. package/dist/core/runner.js +299 -0
  101. package/dist/core/runner.js.map +1 -0
  102. package/dist/core/session-reader.d.ts +46 -0
  103. package/dist/core/session-reader.d.ts.map +1 -0
  104. package/dist/core/session-reader.js +166 -0
  105. package/dist/core/session-reader.js.map +1 -0
  106. package/dist/index.d.ts +24 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +29 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/llm/index.d.ts +8 -0
  111. package/dist/llm/index.d.ts.map +1 -0
  112. package/dist/llm/index.js +21 -0
  113. package/dist/llm/index.js.map +1 -0
  114. package/dist/llm/pi-client.d.ts +8 -0
  115. package/dist/llm/pi-client.d.ts.map +1 -0
  116. package/dist/llm/pi-client.js +8 -0
  117. package/dist/llm/pi-client.js.map +1 -0
  118. package/dist/llm/prompts.d.ts +14 -0
  119. package/dist/llm/prompts.d.ts.map +1 -0
  120. package/dist/llm/prompts.js +1618 -0
  121. package/dist/llm/prompts.js.map +1 -0
  122. package/dist/llm/retry.d.ts +5 -0
  123. package/dist/llm/retry.d.ts.map +1 -0
  124. package/dist/llm/retry.js +5 -0
  125. package/dist/llm/retry.js.map +1 -0
  126. package/dist/llm/skills.d.ts +265 -0
  127. package/dist/llm/skills.d.ts.map +1 -0
  128. package/dist/llm/skills.js +804 -0
  129. package/dist/llm/skills.js.map +1 -0
  130. package/dist/quality/index.d.ts +3 -0
  131. package/dist/quality/index.d.ts.map +1 -0
  132. package/dist/quality/index.js +3 -0
  133. package/dist/quality/index.js.map +1 -0
  134. package/dist/quality/quality-controller.d.ts +2 -0
  135. package/dist/quality/quality-controller.d.ts.map +1 -0
  136. package/dist/quality/quality-controller.js +2 -0
  137. package/dist/quality/quality-controller.js.map +1 -0
  138. package/dist/quality/sla-monitor.d.ts +2 -0
  139. package/dist/quality/sla-monitor.d.ts.map +1 -0
  140. package/dist/quality/sla-monitor.js +2 -0
  141. package/dist/quality/sla-monitor.js.map +1 -0
  142. package/dist/scheduling/cron.d.ts +2 -0
  143. package/dist/scheduling/cron.d.ts.map +1 -0
  144. package/dist/scheduling/cron.js +2 -0
  145. package/dist/scheduling/cron.js.map +1 -0
  146. package/dist/scheduling/index.d.ts +3 -0
  147. package/dist/scheduling/index.d.ts.map +1 -0
  148. package/dist/scheduling/index.js +3 -0
  149. package/dist/scheduling/index.js.map +1 -0
  150. package/dist/scheduling/scheduler.d.ts +2 -0
  151. package/dist/scheduling/scheduler.d.ts.map +1 -0
  152. package/dist/scheduling/scheduler.js +2 -0
  153. package/dist/scheduling/scheduler.js.map +1 -0
  154. package/dist/server/app.d.ts +19 -0
  155. package/dist/server/app.d.ts.map +1 -0
  156. package/dist/server/app.js +302 -0
  157. package/dist/server/app.js.map +1 -0
  158. package/dist/server/deps.d.ts +157 -0
  159. package/dist/server/deps.d.ts.map +1 -0
  160. package/dist/server/deps.js +9 -0
  161. package/dist/server/deps.js.map +1 -0
  162. package/dist/server/index.d.ts +43 -0
  163. package/dist/server/index.d.ts.map +1 -0
  164. package/dist/server/index.js +128 -0
  165. package/dist/server/index.js.map +1 -0
  166. package/dist/server/middleware/auth.d.ts +3 -0
  167. package/dist/server/middleware/auth.d.ts.map +1 -0
  168. package/dist/server/middleware/auth.js +35 -0
  169. package/dist/server/middleware/auth.js.map +1 -0
  170. package/dist/server/middleware/error.d.ts +15 -0
  171. package/dist/server/middleware/error.d.ts.map +1 -0
  172. package/dist/server/middleware/error.js +44 -0
  173. package/dist/server/middleware/error.js.map +1 -0
  174. package/dist/server/middleware/rate-limit.d.ts +16 -0
  175. package/dist/server/middleware/rate-limit.d.ts.map +1 -0
  176. package/dist/server/middleware/rate-limit.js +51 -0
  177. package/dist/server/middleware/rate-limit.js.map +1 -0
  178. package/dist/server/routes/config.d.ts +25 -0
  179. package/dist/server/routes/config.d.ts.map +1 -0
  180. package/dist/server/routes/config.js +446 -0
  181. package/dist/server/routes/config.js.map +1 -0
  182. package/dist/server/routes/files.d.ts +11 -0
  183. package/dist/server/routes/files.d.ts.map +1 -0
  184. package/dist/server/routes/files.js +514 -0
  185. package/dist/server/routes/files.js.map +1 -0
  186. package/dist/server/routes/filesystem.d.ts +3 -0
  187. package/dist/server/routes/filesystem.d.ts.map +1 -0
  188. package/dist/server/routes/filesystem.js +152 -0
  189. package/dist/server/routes/filesystem.js.map +1 -0
  190. package/dist/server/routes/providers.d.ts +7 -0
  191. package/dist/server/routes/providers.d.ts.map +1 -0
  192. package/dist/server/routes/providers.js +256 -0
  193. package/dist/server/routes/providers.js.map +1 -0
  194. package/dist/server/routes/skills.d.ts +11 -0
  195. package/dist/server/routes/skills.d.ts.map +1 -0
  196. package/dist/server/routes/skills.js +543 -0
  197. package/dist/server/routes/skills.js.map +1 -0
  198. package/dist/server/security.d.ts +34 -0
  199. package/dist/server/security.d.ts.map +1 -0
  200. package/dist/server/security.js +118 -0
  201. package/dist/server/security.js.map +1 -0
  202. package/dist/server/sse-bridge.d.ts +36 -0
  203. package/dist/server/sse-bridge.d.ts.map +1 -0
  204. package/dist/server/sse-bridge.js +110 -0
  205. package/dist/server/sse-bridge.js.map +1 -0
  206. package/dist/server/types.d.ts +69 -0
  207. package/dist/server/types.d.ts.map +1 -0
  208. package/dist/server/types.js +2 -0
  209. package/dist/server/types.js.map +1 -0
  210. package/dist/setup/env-persistence.d.ts +9 -0
  211. package/dist/setup/env-persistence.d.ts.map +1 -0
  212. package/dist/setup/env-persistence.js +42 -0
  213. package/dist/setup/env-persistence.js.map +1 -0
  214. package/dist/setup/index.d.ts +32 -0
  215. package/dist/setup/index.d.ts.map +1 -0
  216. package/dist/setup/index.js +24 -0
  217. package/dist/setup/index.js.map +1 -0
  218. package/dist/setup/models.d.ts +20 -0
  219. package/dist/setup/models.d.ts.map +1 -0
  220. package/dist/setup/models.js +33 -0
  221. package/dist/setup/models.js.map +1 -0
  222. package/dist/setup/providers.d.ts +26 -0
  223. package/dist/setup/providers.d.ts.map +1 -0
  224. package/dist/setup/providers.js +31 -0
  225. package/dist/setup/providers.js.map +1 -0
  226. package/dist/vault/encrypted-store.d.ts +45 -0
  227. package/dist/vault/encrypted-store.d.ts.map +1 -0
  228. package/dist/vault/encrypted-store.js +149 -0
  229. package/dist/vault/encrypted-store.js.map +1 -0
  230. package/dist/vault/index.d.ts +4 -0
  231. package/dist/vault/index.d.ts.map +1 -0
  232. package/dist/vault/index.js +3 -0
  233. package/dist/vault/index.js.map +1 -0
  234. package/dist/vault/resolver.d.ts +8 -0
  235. package/dist/vault/resolver.d.ts.map +1 -0
  236. package/dist/vault/resolver.js +8 -0
  237. package/dist/vault/resolver.js.map +1 -0
  238. 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