pi-harness-runtime 1.1.95 → 1.1.97

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 (226) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/harness/loop-runtime.ts +3 -0
  3. package/index.ts +130 -29
  4. package/package.json +2 -1
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/architecture-generator/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-refactor/package.json +1 -1
  9. package/packages/autonomous-runtime/package.json +1 -1
  10. package/packages/cache-strategy/package.json +1 -1
  11. package/packages/capability-registry/package.json +1 -1
  12. package/packages/checkpoint/package.json +1 -1
  13. package/packages/cli-plugin-sdk/package.json +1 -1
  14. package/packages/clipboard/package.json +1 -1
  15. package/packages/clipboard-plugin/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/cookie-sanitizer/package.json +1 -1
  24. package/packages/cost-optimizer/package.json +1 -1
  25. package/packages/dependency-analyzer/package.json +1 -1
  26. package/packages/django-plugin/package.json +1 -1
  27. package/packages/doc-generator/package.json +1 -1
  28. package/packages/evaluation-engine/package.json +1 -1
  29. package/packages/evaluation-runner/package.json +1 -1
  30. package/packages/event-bus/package.json +1 -1
  31. package/packages/event-store/package.json +1 -1
  32. package/packages/experience-replay/package.json +1 -1
  33. package/packages/feedback-collector/package.json +1 -1
  34. package/packages/file-copy-helper/package.json +1 -1
  35. package/packages/framework-detector/package.json +1 -1
  36. package/packages/framework-plugin-sdk/package.json +1 -1
  37. package/packages/frappe-plugin/package.json +1 -1
  38. package/packages/generic-web-plugin/package.json +1 -1
  39. package/packages/health-monitor/package.json +1 -1
  40. package/packages/intent-analyzer/package.json +1 -1
  41. package/packages/jev-judge/package.json +1 -1
  42. package/packages/knowledge-graph/package.json +1 -1
  43. package/packages/knowledge-retrieval/package.json +1 -1
  44. package/packages/laravel-plugin/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/mcp-adapter/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/milestone-manager/package.json +1 -1
  49. package/packages/model-registry/package.json +1 -1
  50. package/packages/nextjs-plugin/package.json +1 -1
  51. package/packages/notification/package.json +1 -1
  52. package/packages/observability/package.json +1 -1
  53. package/packages/okf-indexer/package.json +1 -1
  54. package/packages/performance-optimizer/package.json +1 -1
  55. package/packages/privilege-broker/package.json +1 -1
  56. package/packages/project-analyzer/package.json +1 -1
  57. package/packages/project-bootstrap/package.json +1 -1
  58. package/packages/projection-engine/package.json +1 -1
  59. package/packages/prompt-compiler/package.json +1 -1
  60. package/packages/prompt-versioning/package.json +1 -1
  61. package/packages/provider-adapter-sdk/package.json +1 -1
  62. package/packages/provider-router/package.json +1 -1
  63. package/packages/provider-selector/package.json +1 -1
  64. package/packages/providers/package.json +1 -1
  65. package/packages/qdrant-skills/dist/client.d.ts +51 -0
  66. package/packages/qdrant-skills/dist/client.d.ts.map +1 -0
  67. package/packages/qdrant-skills/dist/client.js +105 -0
  68. package/packages/qdrant-skills/dist/client.js.map +1 -0
  69. package/packages/qdrant-skills/dist/embedder.d.ts +39 -0
  70. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -0
  71. package/packages/qdrant-skills/dist/embedder.js +101 -0
  72. package/packages/qdrant-skills/dist/embedder.js.map +1 -0
  73. package/packages/qdrant-skills/dist/index.d.ts +25 -0
  74. package/packages/qdrant-skills/dist/index.d.ts.map +1 -0
  75. package/packages/qdrant-skills/dist/index.js +25 -0
  76. package/packages/qdrant-skills/dist/index.js.map +1 -0
  77. package/packages/qdrant-skills/package.json +27 -0
  78. package/packages/qdrant-skills/src/client.ts +178 -0
  79. package/packages/qdrant-skills/src/embedder.ts +137 -0
  80. package/packages/qdrant-skills/src/index.ts +42 -0
  81. package/packages/quota-manager/package.json +1 -1
  82. package/packages/rate-limiter/package.json +1 -1
  83. package/packages/react-vite-plugin/package.json +1 -1
  84. package/packages/release-manager/package.json +1 -1
  85. package/packages/requirement-compiler/package.json +1 -1
  86. package/packages/runtime/package.json +1 -1
  87. package/packages/scheduler/package.json +1 -1
  88. package/packages/scheduler-adapter/package.json +1 -1
  89. package/packages/session/package.json +1 -1
  90. package/packages/session-api/package.json +1 -1
  91. package/packages/session-export/package.json +1 -1
  92. package/packages/shared-context/package.json +1 -1
  93. package/packages/skill-mcp-client/package.json +1 -1
  94. package/packages/skill-registry/package.json +1 -1
  95. package/packages/skills/dist/index.d.ts +40 -0
  96. package/packages/skills/dist/index.d.ts.map +1 -0
  97. package/packages/skills/dist/index.js +84 -0
  98. package/packages/skills/dist/index.js.map +1 -0
  99. package/packages/skills/dist/linter.d.ts +38 -0
  100. package/packages/skills/dist/linter.d.ts.map +1 -0
  101. package/packages/skills/dist/linter.js +323 -0
  102. package/packages/skills/dist/linter.js.map +1 -0
  103. package/packages/skills/dist/loader.d.ts +74 -0
  104. package/packages/skills/dist/loader.d.ts.map +1 -0
  105. package/packages/skills/dist/loader.js +280 -0
  106. package/packages/skills/dist/loader.js.map +1 -0
  107. package/packages/skills/dist/parser.d.ts +32 -0
  108. package/packages/skills/dist/parser.d.ts.map +1 -0
  109. package/packages/skills/dist/parser.js +459 -0
  110. package/packages/skills/dist/parser.js.map +1 -0
  111. package/packages/skills/dist/scanner.d.ts +33 -0
  112. package/packages/skills/dist/scanner.d.ts.map +1 -0
  113. package/packages/skills/dist/scanner.js +257 -0
  114. package/packages/skills/dist/scanner.js.map +1 -0
  115. package/packages/skills/dist/skill-commands.d.ts +33 -0
  116. package/packages/skills/dist/skill-commands.d.ts.map +1 -0
  117. package/packages/skills/dist/skill-commands.js +239 -0
  118. package/packages/skills/dist/skill-commands.js.map +1 -0
  119. package/packages/skills/dist/skill-tool.d.ts +120 -0
  120. package/packages/skills/dist/skill-tool.d.ts.map +1 -0
  121. package/packages/skills/dist/skill-tool.js +239 -0
  122. package/packages/skills/dist/skill-tool.js.map +1 -0
  123. package/packages/skills/dist/skill-versioning.d.ts +123 -0
  124. package/packages/skills/dist/skill-versioning.d.ts.map +1 -0
  125. package/packages/skills/dist/skill-versioning.js +240 -0
  126. package/packages/skills/dist/skill-versioning.js.map +1 -0
  127. package/packages/skills/dist/types.d.ts +226 -0
  128. package/packages/skills/dist/types.d.ts.map +1 -0
  129. package/packages/skills/dist/types.js +21 -0
  130. package/packages/skills/dist/types.js.map +1 -0
  131. package/packages/skills/package.json +46 -0
  132. package/packages/skills/src/index.ts +95 -0
  133. package/packages/skills/src/linter.ts +371 -0
  134. package/packages/skills/src/loader.ts +335 -0
  135. package/packages/skills/src/parser.ts +548 -0
  136. package/packages/skills/src/scanner.ts +322 -0
  137. package/packages/skills/src/skill-commands.ts +318 -0
  138. package/packages/skills/src/skill-tool.ts +324 -0
  139. package/packages/skills/src/skill-versioning.ts +345 -0
  140. package/packages/skills/src/types.ts +259 -0
  141. package/packages/skills/test/init-skills.test.ts +118 -0
  142. package/packages/sprint-planner/package.json +1 -1
  143. package/packages/subscription-engine/package.json +1 -1
  144. package/packages/task-compiler/package.json +1 -1
  145. package/packages/tencentdb-memory/package.json +1 -1
  146. package/packages/tencentdb-sync/package.json +1 -1
  147. package/packages/test-data-generator/package.json +1 -1
  148. package/packages/test-generator/package.json +1 -1
  149. package/packages/todo-bd-sync/package.json +1 -1
  150. package/packages/token-estimation/package.json +1 -1
  151. package/packages/token-optimizer/package.json +1 -1
  152. package/packages/trajectory/dist/src/curator.d.ts +189 -0
  153. package/packages/trajectory/dist/src/curator.d.ts.map +1 -0
  154. package/packages/trajectory/dist/src/curator.js +340 -0
  155. package/packages/trajectory/dist/src/curator.js.map +1 -0
  156. package/packages/trajectory/dist/src/journey-renderer.d.ts +53 -0
  157. package/packages/trajectory/dist/src/journey-renderer.d.ts.map +1 -0
  158. package/packages/trajectory/dist/src/journey-renderer.js +324 -0
  159. package/packages/trajectory/dist/src/journey-renderer.js.map +1 -0
  160. package/packages/trajectory/dist/src/learning-graph.d.ts +159 -0
  161. package/packages/trajectory/dist/src/learning-graph.d.ts.map +1 -0
  162. package/packages/trajectory/dist/src/learning-graph.js +258 -0
  163. package/packages/trajectory/dist/src/learning-graph.js.map +1 -0
  164. package/packages/trajectory/dist/src/memory-store.d.ts +146 -0
  165. package/packages/trajectory/dist/src/memory-store.d.ts.map +1 -0
  166. package/packages/trajectory/dist/src/memory-store.js +472 -0
  167. package/packages/trajectory/dist/src/memory-store.js.map +1 -0
  168. package/packages/trajectory/dist/src/review-fork.d.ts +164 -0
  169. package/packages/trajectory/dist/src/review-fork.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/review-fork.js +332 -0
  171. package/packages/trajectory/dist/src/review-fork.js.map +1 -0
  172. package/packages/trajectory/dist/src/write-approval.d.ts +136 -0
  173. package/packages/trajectory/dist/src/write-approval.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/write-approval.js +281 -0
  175. package/packages/trajectory/dist/src/write-approval.js.map +1 -0
  176. package/packages/trajectory/package.json +1 -1
  177. package/packages/trajectory/src/curator.ts +485 -0
  178. package/packages/trajectory/src/journey-renderer.ts +399 -0
  179. package/packages/trajectory/src/learning-graph.ts +363 -0
  180. package/packages/trajectory/src/memory-store.ts +578 -0
  181. package/packages/trajectory/src/review-fork.ts +450 -0
  182. package/packages/trajectory/src/write-approval.ts +372 -0
  183. package/packages/tui/package.json +1 -1
  184. package/packages/types/package.json +1 -1
  185. package/packages/workflow-events/package.json +1 -1
  186. package/packages/workspace-scanner/package.json +1 -1
  187. package/packages/worktree/package.json +1 -1
  188. package/packages/write-review/dist/blackboard.d.ts +98 -0
  189. package/packages/write-review/dist/blackboard.d.ts.map +1 -0
  190. package/packages/write-review/dist/blackboard.js +289 -0
  191. package/packages/write-review/dist/blackboard.js.map +1 -0
  192. package/packages/write-review/dist/event-bus.d.ts +106 -0
  193. package/packages/write-review/dist/event-bus.d.ts.map +1 -0
  194. package/packages/write-review/dist/event-bus.js +294 -0
  195. package/packages/write-review/dist/event-bus.js.map +1 -0
  196. package/packages/write-review/dist/gate.d.ts +37 -0
  197. package/packages/write-review/dist/gate.d.ts.map +1 -0
  198. package/packages/write-review/dist/gate.js +138 -0
  199. package/packages/write-review/dist/gate.js.map +1 -0
  200. package/packages/write-review/dist/index.d.ts +22 -0
  201. package/packages/write-review/dist/index.d.ts.map +1 -0
  202. package/packages/write-review/dist/index.js +60 -0
  203. package/packages/write-review/dist/index.js.map +1 -0
  204. package/packages/write-review/dist/injection.d.ts +40 -0
  205. package/packages/write-review/dist/injection.d.ts.map +1 -0
  206. package/packages/write-review/dist/injection.js +179 -0
  207. package/packages/write-review/dist/injection.js.map +1 -0
  208. package/packages/write-review/dist/review.d.ts +54 -0
  209. package/packages/write-review/dist/review.d.ts.map +1 -0
  210. package/packages/write-review/dist/review.js +154 -0
  211. package/packages/write-review/dist/review.js.map +1 -0
  212. package/packages/write-review/dist/trigger.d.ts +39 -0
  213. package/packages/write-review/dist/trigger.d.ts.map +1 -0
  214. package/packages/write-review/dist/trigger.js +107 -0
  215. package/packages/write-review/dist/trigger.js.map +1 -0
  216. package/packages/write-review/dist/types.d.ts +78 -0
  217. package/packages/write-review/dist/types.d.ts.map +1 -0
  218. package/packages/write-review/dist/types.js +26 -0
  219. package/packages/write-review/dist/types.js.map +1 -0
  220. package/packages/write-review/package.json +1 -1
  221. package/packages/write-review/src/event-bus.ts +351 -0
  222. package/packages/write-review/src/index.ts +1 -0
  223. package/packages/write-review/src/injection.ts +25 -3
  224. package/proactive-compact.ts +51 -7
  225. package/scripts/sync-skills.ts +275 -0
  226. package/scripts/sync-to-qdrant.ts +398 -0
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Skill Directory Scanner
3
+ *
4
+ * Recursively scans directories for skills (directories containing SKILL.md).
5
+ * Supports pi.dev Agent Skills locations:
6
+ * - ~/.pi-harness/skills
7
+ * - skills/ (relative to cwd)
8
+ * - .agents/skills/ (pi.dev compatible)
9
+ */
10
+
11
+ import { existsSync, readdirSync, statSync } from "node:fs";
12
+ import { join, resolve, normalize } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import type { ScanOptions, ScanResult, ScanError } from "./types.js";
15
+ import { parseSkillFile, scanSkillDirectory } from "./parser.js";
16
+
17
+ /**
18
+ * Default scan options
19
+ */
20
+ const DEFAULT_SCAN_OPTIONS: ScanOptions = {
21
+ directories: [
22
+ "~/.pi-harness/skills",
23
+ "skills",
24
+ ".agents/skills",
25
+ ],
26
+ maxDepth: 10,
27
+ includeHidden: false,
28
+ };
29
+
30
+ /**
31
+ * Expand ~ in paths to home directory
32
+ */
33
+ function expandPath(path: string): string {
34
+ if (path.startsWith("~/")) {
35
+ return join(homedir(), path.slice(2));
36
+ }
37
+ return path;
38
+ }
39
+
40
+ /**
41
+ * Check if a path should be included based on patterns
42
+ */
43
+ function matchesPattern(path: string, patterns: string[] | undefined): boolean {
44
+ if (!patterns || patterns.length === 0) {
45
+ return true;
46
+ }
47
+ for (const pattern of patterns) {
48
+ if (path.includes(pattern)) {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Recursively scan a directory for skills
57
+ */
58
+ function scanDirectory(
59
+ dir: string,
60
+ options: ScanOptions,
61
+ scannedPaths: Set<string>,
62
+ errors: ScanError[],
63
+ depth: number = 0
64
+ ): string[] {
65
+ const skillDirs: string[] = [];
66
+
67
+ // Skip if max depth exceeded
68
+ if (options.maxDepth !== undefined && depth > options.maxDepth) {
69
+ return skillDirs;
70
+ }
71
+
72
+ // Normalize and resolve path
73
+ const normalizedPath = normalize(expandPath(dir));
74
+
75
+ // Skip if already scanned
76
+ if (scannedPaths.has(normalizedPath)) {
77
+ return skillDirs;
78
+ }
79
+ scannedPaths.add(normalizedPath);
80
+
81
+ // Skip if doesn't exist
82
+ if (!existsSync(normalizedPath)) {
83
+ return skillDirs;
84
+ }
85
+
86
+ try {
87
+ const entries = readdirSync(normalizedPath);
88
+
89
+ for (const entry of entries) {
90
+ // Skip hidden directories if not included
91
+ if (entry.startsWith(".") && !options.includeHidden) {
92
+ continue;
93
+ }
94
+
95
+ const fullPath = join(normalizedPath, entry);
96
+
97
+ try {
98
+ const stat = statSync(fullPath);
99
+
100
+ if (stat.isDirectory()) {
101
+ // Check if this directory contains a SKILL.md
102
+ const skillMdPath = join(fullPath, "SKILL.md");
103
+
104
+ if (existsSync(skillMdPath)) {
105
+ // Found a skill!
106
+ skillDirs.push(fullPath);
107
+ } else {
108
+ // Recurse into subdirectory
109
+ const subSkills = scanDirectory(
110
+ fullPath,
111
+ options,
112
+ scannedPaths,
113
+ errors,
114
+ depth + 1
115
+ );
116
+ skillDirs.push(...subSkills);
117
+ }
118
+ }
119
+ } catch (err) {
120
+ errors.push({
121
+ path: fullPath,
122
+ error: err instanceof Error ? err.message : String(err),
123
+ });
124
+ }
125
+ }
126
+ } catch (err) {
127
+ errors.push({
128
+ path: normalizedPath,
129
+ error: err instanceof Error ? err.message : String(err),
130
+ });
131
+ }
132
+
133
+ return skillDirs;
134
+ }
135
+
136
+ /**
137
+ * Scan multiple directories for skills
138
+ */
139
+ export async function scanForSkills(
140
+ options?: Partial<ScanOptions>
141
+ ): Promise<ScanResult> {
142
+ const startTime = Date.now();
143
+ const mergedOptions: ScanOptions = {
144
+ ...DEFAULT_SCAN_OPTIONS,
145
+ ...options,
146
+ };
147
+
148
+ const scannedPaths = new Set<string>();
149
+ const errors: ScanError[] = [];
150
+ const skillDirs: string[] = [];
151
+
152
+ for (const dir of mergedOptions.directories) {
153
+ const dirs = scanDirectory(
154
+ dir,
155
+ mergedOptions,
156
+ scannedPaths,
157
+ errors,
158
+ 0
159
+ );
160
+ skillDirs.push(...dirs);
161
+ }
162
+
163
+ // Parse each skill directory
164
+ const skills = [];
165
+ for (const skillDir of skillDirs) {
166
+ try {
167
+ const skillMdPath = join(skillDir, "SKILL.md");
168
+ const skill = parseSkillFile(skillMdPath);
169
+
170
+ // Scan for references and examples
171
+ const { references, examples } = scanSkillDirectory(skillDir);
172
+ skill.references = references;
173
+ skill.examples = examples;
174
+
175
+ skills.push(skill);
176
+ } catch (err) {
177
+ errors.push({
178
+ path: skillDir,
179
+ error: err instanceof Error ? err.message : String(err),
180
+ });
181
+ }
182
+ }
183
+
184
+ return {
185
+ skills,
186
+ errors,
187
+ scannedPaths: Array.from(scannedPaths),
188
+ durationMs: Date.now() - startTime,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Scan for skills synchronously (for use at startup)
194
+ */
195
+ export function scanForSkillsSync(
196
+ options?: Partial<ScanOptions>
197
+ ): ScanResult {
198
+ const startTime = Date.now();
199
+ const mergedOptions: ScanOptions = {
200
+ ...DEFAULT_SCAN_OPTIONS,
201
+ ...options,
202
+ };
203
+
204
+ const scannedPaths = new Set<string>();
205
+ const errors: ScanError[] = [];
206
+ const skillDirs: string[] = [];
207
+
208
+ for (const dir of mergedOptions.directories) {
209
+ const dirs = scanDirectory(
210
+ dir,
211
+ mergedOptions,
212
+ scannedPaths,
213
+ errors,
214
+ 0
215
+ );
216
+ skillDirs.push(...dirs);
217
+ }
218
+
219
+ // Parse each skill directory
220
+ const skills = [];
221
+ for (const skillDir of skillDirs) {
222
+ try {
223
+ const skillMdPath = join(skillDir, "SKILL.md");
224
+ const skill = parseSkillFile(skillMdPath);
225
+
226
+ // Scan for references and examples
227
+ const { references, examples } = scanSkillDirectory(skillDir);
228
+ skill.references = references;
229
+ skill.examples = examples;
230
+
231
+ skills.push(skill);
232
+ } catch (err) {
233
+ errors.push({
234
+ path: skillDir,
235
+ error: err instanceof Error ? err.message : String(err),
236
+ });
237
+ }
238
+ }
239
+
240
+ return {
241
+ skills,
242
+ errors,
243
+ scannedPaths: Array.from(scannedPaths),
244
+ durationMs: Date.now() - startTime,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Resolve skill locations from options
250
+ */
251
+ export function resolveSkillLocations(locations?: string[]): string[] {
252
+ const defaults = DEFAULT_SCAN_OPTIONS.directories;
253
+
254
+ if (!locations || locations.length === 0) {
255
+ return defaults.map(expandPath);
256
+ }
257
+
258
+ return locations.map(expandPath);
259
+ }
260
+
261
+ /**
262
+ * Check if a path is a valid skill directory
263
+ */
264
+ export function isSkillDirectory(path: string): boolean {
265
+ const skillMdPath = join(expandPath(path), "SKILL.md");
266
+ return existsSync(skillMdPath);
267
+ }
268
+
269
+ /**
270
+ * Watch directories for skill changes
271
+ */
272
+ export function watchSkillDirectories(
273
+ directories: string[],
274
+ onChange: (event: "add" | "change" | "unlink", skillPath: string) => void,
275
+ options?: { pollInterval?: number }
276
+ ): () => void {
277
+ const pollInterval = options?.pollInterval ?? 5000;
278
+ const lastModified = new Map<string, number>();
279
+
280
+ const check = () => {
281
+ for (const dir of directories) {
282
+ const expandedDir = expandPath(dir);
283
+ if (!existsSync(expandedDir)) continue;
284
+
285
+ try {
286
+ const entries = readdirSync(expandedDir, { withFileTypes: true });
287
+
288
+ for (const entry of entries) {
289
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
290
+
291
+ const skillPath = join(expandedDir, entry.name);
292
+ const skillMdPath = join(skillPath, "SKILL.md");
293
+
294
+ if (!existsSync(skillMdPath)) continue;
295
+
296
+ try {
297
+ const stat = statSync(skillMdPath);
298
+ const mtime = stat.mtimeMs;
299
+ const lastMtime = lastModified.get(skillPath);
300
+
301
+ if (!lastMtime) {
302
+ lastModified.set(skillPath, mtime);
303
+ onChange("add", skillPath);
304
+ } else if (mtime > lastMtime) {
305
+ lastModified.set(skillPath, mtime);
306
+ onChange("change", skillPath);
307
+ }
308
+ } catch {
309
+ // File might have been deleted
310
+ }
311
+ }
312
+ } catch {
313
+ // Directory might not exist
314
+ }
315
+ }
316
+ };
317
+
318
+ const interval = setInterval(check, pollInterval);
319
+
320
+ // Return cleanup function
321
+ return () => clearInterval(interval);
322
+ }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Skill Commands - /skill:name slash command handler
3
+ *
4
+ * Registers /skill:name commands for explicit skill invocation.
5
+ * Based on pi.dev skill command specification.
6
+ */
7
+
8
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
9
+ import { getGlobalSkillRegistry } from "./loader.js";
10
+ import { scanForSkillsSync } from "./scanner.js";
11
+
12
+ /**
13
+ * Skill command configuration
14
+ */
15
+ export interface SkillCommandConfig {
16
+ /** Enable skill commands */
17
+ enabled: boolean;
18
+ /** Prefix for skill commands (default: "skill:") */
19
+ prefix: string;
20
+ }
21
+
22
+ /**
23
+ * Default configuration
24
+ */
25
+ export const DEFAULT_COMMAND_CONFIG: SkillCommandConfig = {
26
+ enabled: true,
27
+ prefix: "skill:",
28
+ };
29
+
30
+ /**
31
+ * Initialize skill commands with ExtensionAPI
32
+ */
33
+ export function initSkillCommands(
34
+ pi: ExtensionAPI,
35
+ config?: Partial<SkillCommandConfig>
36
+ ): void {
37
+ const cfg = { ...DEFAULT_COMMAND_CONFIG, ...config };
38
+
39
+ if (!cfg.enabled) return;
40
+
41
+ const registry = getGlobalSkillRegistry();
42
+
43
+ // Initialize registry with existing skills
44
+ const result = scanForSkillsSync();
45
+ for (const skill of result.skills) {
46
+ registry.register(skill);
47
+ }
48
+
49
+ // Register /skill command for listing
50
+ pi.registerCommand("skill", {
51
+ description: "Interact with skills. Usage: /skill [list|search <query>|info <name>]",
52
+ handler: async (args: string, ctx: ExtensionContext) => {
53
+ const parts = args.trim().split(/\s+/);
54
+ const subcommand = parts[0] || "list";
55
+ const query = parts.slice(1).join(" ");
56
+
57
+ switch (subcommand) {
58
+ case "list":
59
+ return handleSkillList(registry, ctx as ExtensionCommandContext);
60
+ case "search":
61
+ return handleSkillSearch(registry, query, ctx as ExtensionCommandContext);
62
+ case "info":
63
+ return handleSkillInfo(registry, query, ctx as ExtensionCommandContext);
64
+ default:
65
+ ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: /skill [list|search|info]`, "warning");
66
+ }
67
+ },
68
+ });
69
+
70
+ // Register individual skill commands dynamically
71
+ registerSkillCommands(pi, registry, cfg.prefix);
72
+ }
73
+
74
+ /**
75
+ * Register individual skill commands
76
+ */
77
+ function registerSkillCommands(
78
+ pi: ExtensionAPI,
79
+ registry: ReturnType<typeof getGlobalSkillRegistry>,
80
+ prefix: string
81
+ ): void {
82
+ // Get all registered skills
83
+ const skills = registry.list();
84
+
85
+ for (const skill of skills) {
86
+ const commandName = `${prefix}${skill.id}`;
87
+
88
+ // Skip if command name is too long or conflicts
89
+ if (commandName.length > 50) continue;
90
+
91
+ // Register the command
92
+ try {
93
+ pi.registerCommand(commandName, {
94
+ description: skill.frontmatter.description,
95
+ handler: async (_skillArgs: string, ctx: ExtensionContext) => {
96
+ // Load the full skill content
97
+ const loaded = await registry.load(skill.id, {
98
+ loadReferences: true,
99
+ loadExamples: true,
100
+ });
101
+
102
+ if (!loaded) {
103
+ ctx.ui.notify(`Skill not found: ${skill.id}`, "error");
104
+ return;
105
+ }
106
+
107
+ // Build response with skill content
108
+ const response: string[] = [];
109
+ response.push(`# ${loaded.frontmatter.name}`);
110
+ response.push(`**${loaded.frontmatter.description}**`);
111
+ response.push("");
112
+ response.push("---");
113
+ response.push(loaded.body);
114
+
115
+ // Append references
116
+ if (loaded.references.length > 0) {
117
+ response.push("");
118
+ response.push("## References");
119
+ for (const ref of loaded.references) {
120
+ response.push(`- ${ref.relativePath}`);
121
+ }
122
+ }
123
+
124
+ // Show skill details
125
+ ctx.ui.notify(
126
+ `Skill '${loaded.frontmatter.name}' - ${loaded.body.length} chars. ` +
127
+ `Use /skill info ${loaded.id} for full details.`,
128
+ "info"
129
+ );
130
+ },
131
+ });
132
+ } catch (err) {
133
+ // Skip if command registration fails (e.g., name conflict)
134
+ console.debug(`[SkillCommands] Skipped registering ${commandName}:`, err);
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Handle /skill list
141
+ */
142
+ async function handleSkillList(
143
+ registry: ReturnType<typeof getGlobalSkillRegistry>,
144
+ ctx: ExtensionCommandContext
145
+ ): Promise<void> {
146
+ const skills = registry.list();
147
+
148
+ if (skills.length === 0) {
149
+ ctx.ui.notify("No skills registered. Skills are loaded from ~/.pi-harness/skills/, skills/, or .agents/skills/", "info");
150
+ return;
151
+ }
152
+
153
+ const lines: string[] = ["# Available Skills", ""];
154
+
155
+ for (const skill of skills) {
156
+ const author = skill.hermes?.author ?? "unknown";
157
+ const usage = skill.hermes?.usage_count ?? 0;
158
+ lines.push(`## ${skill.frontmatter.name} ${author === "agent" ? "(agent)" : author === "curator" ? "(curator)" : ""}`);
159
+ lines.push(skill.frontmatter.description);
160
+ lines.push(`- Usage: ${usage}x`);
161
+ if (skill.hermes?.triggers?.length) {
162
+ lines.push(`- Triggers: ${skill.hermes.triggers.join(", ")}`);
163
+ }
164
+ lines.push("");
165
+ }
166
+
167
+ // Send as a message using the context's session if available
168
+ ctx.ui.notify(`Found ${skills.length} skills. Use /skill info <name> for details.`, "info");
169
+ }
170
+
171
+ /**
172
+ * Handle /skill search
173
+ */
174
+ async function handleSkillSearch(
175
+ registry: ReturnType<typeof getGlobalSkillRegistry>,
176
+ query: string,
177
+ ctx: ExtensionCommandContext
178
+ ): Promise<void> {
179
+ if (!query) {
180
+ ctx.ui.notify("Usage: /skill search <query>", "warning");
181
+ return;
182
+ }
183
+
184
+ const results = registry.find(query, { threshold: 0.2 });
185
+
186
+
187
+ if (results.length === 0) {
188
+ ctx.ui.notify(`No skills found matching: ${query}`, "info");
189
+ return;
190
+ }
191
+
192
+
193
+ const lines: string[] = [`# Skills matching: ${query}`, ""];
194
+
195
+ for (const result of results.slice(0, 10)) {
196
+ lines.push(`## ${result.skill.frontmatter.name} (${Math.round(result.score * 100)}% match)`);
197
+ lines.push(result.skill.frontmatter.description);
198
+ if (result.matchedTriggers.length > 0) {
199
+ lines.push(`Matched: ${result.matchedTriggers.join(", ")}`);
200
+ }
201
+ lines.push("");
202
+ }
203
+
204
+ ctx.ui.notify(`Found ${results.length} matching skills. Use /skill info <name> for details.`, "info");
205
+ }
206
+
207
+ /**
208
+ * Handle /skill info
209
+ */
210
+ async function handleSkillInfo(
211
+ registry: ReturnType<typeof getGlobalSkillRegistry>,
212
+ skillName: string,
213
+ ctx: ExtensionCommandContext
214
+ ): Promise<void> {
215
+ if (!skillName) {
216
+ ctx.ui.notify("Usage: /skill info <name>", "warning");
217
+ return;
218
+ }
219
+
220
+ const skill = registry.get(skillName);
221
+
222
+ if (!skill) {
223
+ ctx.ui.notify(`Skill not found: ${skillName}`, "error");
224
+ return;
225
+ }
226
+
227
+ // Load full content
228
+ const loaded = await registry.load(skill.id, { loadReferences: true });
229
+
230
+ if (!loaded) {
231
+ ctx.ui.notify(`Failed to load skill: ${skillName}`, "error");
232
+ return;
233
+ }
234
+
235
+ const lines: string[] = [];
236
+ lines.push(`# ${loaded.frontmatter.name}`);
237
+ lines.push("");
238
+ lines.push("**Description:** " + loaded.frontmatter.description);
239
+ if (loaded.frontmatter.version) {
240
+ lines.push("**Version:** " + loaded.frontmatter.version);
241
+ }
242
+ if (loaded.hermes?.author) {
243
+ lines.push("**Author:** " + loaded.hermes.author);
244
+ }
245
+ if (loaded.hermes?.confidence !== undefined) {
246
+ lines.push("**Confidence:** " + Math.round(loaded.hermes.confidence * 100) + "%");
247
+ }
248
+ if (loaded.hermes?.usage_count !== undefined) {
249
+ lines.push("**Used:** " + loaded.hermes.usage_count + " times");
250
+ }
251
+
252
+ lines.push("");
253
+ lines.push("## Content");
254
+ lines.push(loaded.body);
255
+
256
+ if (loaded.references.length > 0) {
257
+ lines.push("");
258
+ lines.push("## References");
259
+ for (const ref of loaded.references) {
260
+ lines.push(`- ${ref.relativePath}`);
261
+ }
262
+ }
263
+
264
+ if (loaded.hermes?.pitfalls?.length) {
265
+ lines.push("");
266
+ lines.push("## Pitfalls");
267
+ for (const pitfall of loaded.hermes.pitfalls) {
268
+ lines.push(`- **${pitfall.name}**: ${pitfall.why}`);
269
+ }
270
+ }
271
+
272
+ // Show skill content via notification (simplified approach)
273
+ ctx.ui.notify(`Skill '${loaded.frontmatter.name}' loaded with ${loaded.body.length} chars. Use /skill:${skill.id} to activate.`, "info");
274
+ }
275
+
276
+ /**
277
+ * Get skill index for system prompt injection
278
+ */
279
+ export function getSkillIndexForSystemPrompt(): string {
280
+ const registry = getGlobalSkillRegistry();
281
+ const skills = registry.list();
282
+
283
+ if (skills.length === 0) {
284
+ return "";
285
+ }
286
+
287
+ const lines: string[] = [];
288
+ lines.push("## Available Skills");
289
+ lines.push("");
290
+
291
+ for (const skill of skills) {
292
+ lines.push(`- **${skill.frontmatter.name}**: ${skill.frontmatter.description}`);
293
+ if (skill.hermes?.triggers?.length) {
294
+ lines.push(` Triggers: ${skill.hermes.triggers.join(", ")}`);
295
+ }
296
+ }
297
+
298
+ lines.push("");
299
+ lines.push("Use `/skill:name` to activate a skill.");
300
+
301
+ return lines.join("\n");
302
+ }
303
+
304
+ /**
305
+ * Refresh skill commands (re-register after skills change)
306
+ */
307
+ export function refreshSkillCommands(
308
+ pi: ExtensionAPI,
309
+ prefix?: string
310
+ ): void {
311
+ const cfg: SkillCommandConfig = {
312
+ enabled: true,
313
+ prefix: prefix ?? DEFAULT_COMMAND_CONFIG.prefix,
314
+ };
315
+
316
+ const registry = getGlobalSkillRegistry();
317
+ registerSkillCommands(pi, registry, cfg.prefix);
318
+ }