@oh-my-pi/pi-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,352 @@
1
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { minimatch } from "minimatch";
4
+ import { skillCapability } from "../capability/skill";
5
+ import type { SourceMeta } from "../capability/types";
6
+ import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
7
+ import { loadSync } from "../discovery";
8
+ import { parseFrontmatter } from "../discovery/helpers";
9
+ import type { SkillsSettings } from "./settings-manager";
10
+
11
+ // Re-export SkillFrontmatter for backward compatibility
12
+ export type { ImportedSkillFrontmatter as SkillFrontmatter };
13
+
14
+ export interface Skill {
15
+ name: string;
16
+ description: string;
17
+ filePath: string;
18
+ baseDir: string;
19
+ source: string;
20
+ /** Source metadata for display */
21
+ _source?: SourceMeta;
22
+ }
23
+
24
+ export interface SkillWarning {
25
+ skillPath: string;
26
+ message: string;
27
+ }
28
+
29
+ export interface LoadSkillsResult {
30
+ skills: Skill[];
31
+ warnings: SkillWarning[];
32
+ }
33
+
34
+ export interface LoadSkillsFromDirOptions {
35
+ /** Directory to scan for skills */
36
+ dir: string;
37
+ /** Source identifier for these skills */
38
+ source: string;
39
+ }
40
+
41
+ /**
42
+ * Load skills from a directory recursively.
43
+ * Skills are directories containing a SKILL.md file with frontmatter including a description.
44
+ * @deprecated Use loadSync("skills") from discovery API instead
45
+ */
46
+ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {
47
+ const skills: Skill[] = [];
48
+ const warnings: SkillWarning[] = [];
49
+ const seenPaths = new Set<string>();
50
+
51
+ function addSkill(skillFile: string, skillDir: string, dirName: string) {
52
+ if (seenPaths.has(skillFile)) return;
53
+ try {
54
+ const content = readFileSync(skillFile, "utf-8");
55
+ const { frontmatter } = parseFrontmatter(content);
56
+ const name = (frontmatter.name as string) || dirName;
57
+ const description = frontmatter.description as string;
58
+
59
+ if (description) {
60
+ seenPaths.add(skillFile);
61
+ skills.push({
62
+ name,
63
+ description,
64
+ filePath: skillFile,
65
+ baseDir: skillDir,
66
+ source: options.source,
67
+ });
68
+ }
69
+ } catch {
70
+ // Skip invalid skills
71
+ }
72
+ }
73
+
74
+ function scanDir(dir: string) {
75
+ try {
76
+ const entries = readdirSync(dir, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
79
+
80
+ const fullPath = join(dir, entry.name);
81
+ if (entry.isDirectory()) {
82
+ const skillFile = join(fullPath, "SKILL.md");
83
+ try {
84
+ const stat = statSync(skillFile);
85
+ if (stat.isFile()) {
86
+ addSkill(skillFile, fullPath, entry.name);
87
+ }
88
+ } catch {
89
+ // No SKILL.md in this directory
90
+ }
91
+ scanDir(fullPath);
92
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
93
+ addSkill(fullPath, dir, basename(dir));
94
+ }
95
+ }
96
+ } catch (err) {
97
+ warnings.push({ skillPath: dir, message: `Failed to read directory: ${err}` });
98
+ }
99
+ }
100
+
101
+ scanDir(options.dir);
102
+
103
+ return { skills, warnings };
104
+ }
105
+
106
+ /**
107
+ * Scan a directory for SKILL.md files recursively.
108
+ * Used internally by loadSkills for custom directories.
109
+ */
110
+ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
111
+ const skills: Skill[] = [];
112
+ const warnings: SkillWarning[] = [];
113
+ const seenPaths = new Set<string>();
114
+
115
+ function addSkill(skillFile: string, skillDir: string, dirName: string) {
116
+ if (seenPaths.has(skillFile)) return;
117
+ try {
118
+ const content = readFileSync(skillFile, "utf-8");
119
+ const { frontmatter } = parseFrontmatter(content);
120
+ const name = (frontmatter.name as string) || dirName;
121
+ const description = frontmatter.description as string;
122
+
123
+ if (description) {
124
+ seenPaths.add(skillFile);
125
+ skills.push({
126
+ name,
127
+ description,
128
+ filePath: skillFile,
129
+ baseDir: skillDir,
130
+ source: "custom",
131
+ });
132
+ }
133
+ } catch {
134
+ // Skip invalid skills
135
+ }
136
+ }
137
+
138
+ function scanDir(currentDir: string) {
139
+ try {
140
+ const entries = readdirSync(currentDir, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
143
+
144
+ const fullPath = join(currentDir, entry.name);
145
+ if (entry.isDirectory()) {
146
+ const skillFile = join(fullPath, "SKILL.md");
147
+ try {
148
+ const stat = statSync(skillFile);
149
+ if (stat.isFile()) {
150
+ addSkill(skillFile, fullPath, entry.name);
151
+ }
152
+ } catch {
153
+ // No SKILL.md in this directory
154
+ }
155
+ scanDir(fullPath);
156
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
157
+ addSkill(fullPath, currentDir, basename(currentDir));
158
+ }
159
+ }
160
+ } catch (err) {
161
+ warnings.push({ skillPath: currentDir, message: `Failed to read directory: ${err}` });
162
+ }
163
+ }
164
+
165
+ scanDir(dir);
166
+
167
+ return { skills, warnings };
168
+ }
169
+
170
+ /**
171
+ * Format skills for inclusion in a system prompt.
172
+ * Uses XML format per Agent Skills standard.
173
+ * See: https://agentskills.io/integrate-skills
174
+ */
175
+ export function formatSkillsForPrompt(skills: Skill[]): string {
176
+ if (skills.length === 0) {
177
+ return "";
178
+ }
179
+
180
+ const lines = [
181
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
182
+ "Use the read tool to load a skill's file when the task matches its description.",
183
+ "",
184
+ "<available_skills>",
185
+ ];
186
+
187
+ for (const skill of skills) {
188
+ lines.push(" <skill>");
189
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
190
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
191
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
192
+ lines.push(" </skill>");
193
+ }
194
+
195
+ lines.push("</available_skills>");
196
+
197
+ return lines.join("\n");
198
+ }
199
+
200
+ function escapeXml(str: string): string {
201
+ return str
202
+ .replace(/&/g, "&amp;")
203
+ .replace(/</g, "&lt;")
204
+ .replace(/>/g, "&gt;")
205
+ .replace(/"/g, "&quot;")
206
+ .replace(/'/g, "&apos;");
207
+ }
208
+
209
+ export interface LoadSkillsOptions extends SkillsSettings {
210
+ /** Working directory for project-local skills. Default: process.cwd() */
211
+ cwd?: string;
212
+ }
213
+
214
+ /**
215
+ * Load skills from all configured locations.
216
+ * Returns skills and any validation warnings.
217
+ */
218
+ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
219
+ const {
220
+ cwd = process.cwd(),
221
+ enabled = true,
222
+ enableCodexUser = true,
223
+ enableClaudeUser = true,
224
+ enableClaudeProject = true,
225
+ enablePiUser = true,
226
+ enablePiProject = true,
227
+ customDirectories = [],
228
+ ignoredSkills = [],
229
+ includeSkills = [],
230
+ } = options;
231
+
232
+ // Early return if skills are disabled
233
+ if (!enabled) {
234
+ return { skills: [], warnings: [] };
235
+ }
236
+
237
+ // Helper to check if a source is enabled
238
+ function isSourceEnabled(source: SourceMeta): boolean {
239
+ const { provider, level } = source;
240
+ if (provider === "codex" && level === "user") return enableCodexUser;
241
+ if (provider === "claude" && level === "user") return enableClaudeUser;
242
+ if (provider === "claude" && level === "project") return enableClaudeProject;
243
+ if (provider === "native" && level === "user") return enablePiUser;
244
+ if (provider === "native" && level === "project") return enablePiProject;
245
+ // For other providers (gemini, cursor, etc.) or custom, default to enabled
246
+ return true;
247
+ }
248
+
249
+ // Use capability API to load all skills
250
+ const result = loadSync<CapabilitySkill>(skillCapability.id, { cwd });
251
+
252
+ const skillMap = new Map<string, Skill>();
253
+ const realPathSet = new Set<string>();
254
+ const collisionWarnings: SkillWarning[] = [];
255
+
256
+ // Check if skill name matches any of the include patterns
257
+ function matchesIncludePatterns(name: string): boolean {
258
+ if (includeSkills.length === 0) return true;
259
+ return includeSkills.some((pattern) => minimatch(name, pattern));
260
+ }
261
+
262
+ // Check if skill name matches any of the ignore patterns
263
+ function matchesIgnorePatterns(name: string): boolean {
264
+ if (ignoredSkills.length === 0) return false;
265
+ return ignoredSkills.some((pattern) => minimatch(name, pattern));
266
+ }
267
+
268
+ // Helper to add a skill to the map
269
+ function addSkill(capSkill: CapabilitySkill, sourceProvider: string) {
270
+ // Apply ignore filter (glob patterns) - takes precedence over include
271
+ if (matchesIgnorePatterns(capSkill.name)) {
272
+ return;
273
+ }
274
+ // Apply include filter (glob patterns)
275
+ if (!matchesIncludePatterns(capSkill.name)) {
276
+ return;
277
+ }
278
+
279
+ // Resolve symlinks to detect duplicate files
280
+ let realPath: string;
281
+ try {
282
+ realPath = realpathSync(capSkill.path);
283
+ } catch {
284
+ realPath = capSkill.path;
285
+ }
286
+
287
+ // Skip silently if we've already loaded this exact file (via symlink)
288
+ if (realPathSet.has(realPath)) {
289
+ return;
290
+ }
291
+
292
+ const existing = skillMap.get(capSkill.name);
293
+ if (existing) {
294
+ collisionWarnings.push({
295
+ skillPath: capSkill.path,
296
+ message: `name collision: "${capSkill.name}" already loaded from ${existing.filePath}, skipping this one`,
297
+ });
298
+ } else {
299
+ // Transform capability skill to legacy format
300
+ const skill: Skill = {
301
+ name: capSkill.name,
302
+ description: capSkill.frontmatter?.description || "",
303
+ filePath: capSkill.path,
304
+ baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
305
+ source: `${sourceProvider}:${capSkill.level}`,
306
+ _source: capSkill._source,
307
+ };
308
+ skillMap.set(capSkill.name, skill);
309
+ realPathSet.add(realPath);
310
+ }
311
+ }
312
+
313
+ // Process skills from capability API
314
+ for (const capSkill of result.items) {
315
+ // Check if this source is enabled
316
+ if (!isSourceEnabled(capSkill._source)) {
317
+ continue;
318
+ }
319
+
320
+ addSkill(capSkill, capSkill._source.provider);
321
+ }
322
+
323
+ // Process custom directories - scan directly without using full provider system
324
+ for (const dir of customDirectories) {
325
+ const customSkills = scanDirectoryForSkills(dir);
326
+ for (const s of customSkills.skills) {
327
+ // Convert to capability format for addSkill processing
328
+ const capSkill: CapabilitySkill = {
329
+ name: s.name,
330
+ path: s.filePath,
331
+ content: "",
332
+ frontmatter: { description: s.description },
333
+ level: "user",
334
+ _source: {
335
+ provider: "custom",
336
+ providerName: "Custom",
337
+ path: s.filePath,
338
+ level: "user",
339
+ },
340
+ };
341
+ addSkill(capSkill, "custom");
342
+ }
343
+ for (const warning of customSkills.warnings) {
344
+ collisionWarnings.push(warning);
345
+ }
346
+ }
347
+
348
+ return {
349
+ skills: Array.from(skillMap.values()),
350
+ warnings: [...result.warnings.map((w) => ({ skillPath: "", message: w })), ...collisionWarnings],
351
+ };
352
+ }
@@ -0,0 +1,132 @@
1
+ import { slashCommandCapability } from "../capability/slash-command";
2
+ import type { SlashCommand } from "../discovery";
3
+ import { loadSync } from "../discovery";
4
+ import { parseFrontmatter } from "../discovery/helpers";
5
+
6
+ /**
7
+ * Represents a custom slash command loaded from a file
8
+ */
9
+ export interface FileSlashCommand {
10
+ name: string;
11
+ description: string;
12
+ content: string;
13
+ source: string; // e.g., "via Claude Code (User)"
14
+ /** Source metadata for display */
15
+ _source?: { providerName: string; level: "user" | "project" | "native" };
16
+ }
17
+
18
+ /**
19
+ * Parse command arguments respecting quoted strings (bash-style)
20
+ * Returns array of arguments
21
+ */
22
+ export function parseCommandArgs(argsString: string): string[] {
23
+ const args: string[] = [];
24
+ let current = "";
25
+ let inQuote: string | null = null;
26
+
27
+ for (let i = 0; i < argsString.length; i++) {
28
+ const char = argsString[i];
29
+
30
+ if (inQuote) {
31
+ if (char === inQuote) {
32
+ inQuote = null;
33
+ } else {
34
+ current += char;
35
+ }
36
+ } else if (char === '"' || char === "'") {
37
+ inQuote = char;
38
+ } else if (char === " " || char === "\t") {
39
+ if (current) {
40
+ args.push(current);
41
+ current = "";
42
+ }
43
+ } else {
44
+ current += char;
45
+ }
46
+ }
47
+
48
+ if (current) {
49
+ args.push(current);
50
+ }
51
+
52
+ return args;
53
+ }
54
+
55
+ /**
56
+ * Substitute argument placeholders in command content
57
+ * Supports $1, $2, ... for positional args and $@ for all args
58
+ */
59
+ export function substituteArgs(content: string, args: string[]): string {
60
+ let result = content;
61
+
62
+ // Replace $@ with all args joined
63
+ result = result.replace(/\$@/g, args.join(" "));
64
+
65
+ // Replace $1, $2, etc. with positional args
66
+ result = result.replace(/\$(\d+)/g, (_, num) => {
67
+ const index = parseInt(num, 10) - 1;
68
+ return args[index] ?? "";
69
+ });
70
+
71
+ return result;
72
+ }
73
+
74
+ export interface LoadSlashCommandsOptions {
75
+ /** Working directory for project-local commands. Default: process.cwd() */
76
+ cwd?: string;
77
+ }
78
+
79
+ /**
80
+ * Load all custom slash commands using the capability API.
81
+ * Loads from all registered providers (builtin, user, project).
82
+ */
83
+ export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
84
+ const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
85
+
86
+ return result.items.map((cmd) => {
87
+ const { frontmatter, body } = parseFrontmatter(cmd.content);
88
+ const frontmatterDesc = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
89
+
90
+ // Get description from frontmatter or first non-empty line
91
+ let description = frontmatterDesc;
92
+ if (!description) {
93
+ const firstLine = body.split("\n").find((line) => line.trim());
94
+ if (firstLine) {
95
+ description = firstLine.slice(0, 60);
96
+ if (firstLine.length > 60) description += "...";
97
+ }
98
+ }
99
+
100
+ // Format source label: "via ProviderName Level"
101
+ const capitalizedLevel = cmd.level.charAt(0).toUpperCase() + cmd.level.slice(1);
102
+ const sourceStr = `via ${cmd._source.providerName} ${capitalizedLevel}`;
103
+
104
+ return {
105
+ name: cmd.name,
106
+ description,
107
+ content: body,
108
+ source: sourceStr,
109
+ _source: { providerName: cmd._source.providerName, level: cmd.level },
110
+ };
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Expand a slash command if it matches a file-based command.
116
+ * Returns the expanded content or the original text if not a slash command.
117
+ */
118
+ export function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {
119
+ if (!text.startsWith("/")) return text;
120
+
121
+ const spaceIndex = text.indexOf(" ");
122
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
123
+ const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
124
+
125
+ const fileCommand = fileCommands.find((cmd) => cmd.name === commandName);
126
+ if (fileCommand) {
127
+ const args = parseCommandArgs(argsString);
128
+ return substituteArgs(fileCommand.content, args);
129
+ }
130
+
131
+ return text;
132
+ }