cecli-dev 0.95.5__py3-none-any.whl

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 (366) hide show
  1. cecli/__init__.py +20 -0
  2. cecli/__main__.py +4 -0
  3. cecli/_version.py +34 -0
  4. cecli/args.py +1092 -0
  5. cecli/args_formatter.py +228 -0
  6. cecli/change_tracker.py +133 -0
  7. cecli/coders/__init__.py +38 -0
  8. cecli/coders/agent_coder.py +1872 -0
  9. cecli/coders/architect_coder.py +63 -0
  10. cecli/coders/ask_coder.py +8 -0
  11. cecli/coders/base_coder.py +3993 -0
  12. cecli/coders/chat_chunks.py +116 -0
  13. cecli/coders/context_coder.py +52 -0
  14. cecli/coders/copypaste_coder.py +269 -0
  15. cecli/coders/editblock_coder.py +656 -0
  16. cecli/coders/editblock_fenced_coder.py +9 -0
  17. cecli/coders/editblock_func_coder.py +140 -0
  18. cecli/coders/editor_diff_fenced_coder.py +8 -0
  19. cecli/coders/editor_editblock_coder.py +8 -0
  20. cecli/coders/editor_whole_coder.py +8 -0
  21. cecli/coders/help_coder.py +15 -0
  22. cecli/coders/patch_coder.py +705 -0
  23. cecli/coders/search_replace.py +757 -0
  24. cecli/coders/shell.py +37 -0
  25. cecli/coders/single_wholefile_func_coder.py +101 -0
  26. cecli/coders/udiff_coder.py +428 -0
  27. cecli/coders/udiff_simple.py +12 -0
  28. cecli/coders/wholefile_coder.py +143 -0
  29. cecli/coders/wholefile_func_coder.py +133 -0
  30. cecli/commands/__init__.py +192 -0
  31. cecli/commands/add.py +226 -0
  32. cecli/commands/agent.py +51 -0
  33. cecli/commands/architect.py +46 -0
  34. cecli/commands/ask.py +44 -0
  35. cecli/commands/chat_mode.py +0 -0
  36. cecli/commands/clear.py +37 -0
  37. cecli/commands/code.py +46 -0
  38. cecli/commands/command_prefix.py +44 -0
  39. cecli/commands/commit.py +52 -0
  40. cecli/commands/context.py +47 -0
  41. cecli/commands/context_blocks.py +124 -0
  42. cecli/commands/context_management.py +51 -0
  43. cecli/commands/copy.py +62 -0
  44. cecli/commands/copy_context.py +81 -0
  45. cecli/commands/core.py +287 -0
  46. cecli/commands/diff.py +68 -0
  47. cecli/commands/drop.py +217 -0
  48. cecli/commands/editor.py +78 -0
  49. cecli/commands/exit.py +55 -0
  50. cecli/commands/git.py +57 -0
  51. cecli/commands/help.py +140 -0
  52. cecli/commands/history_search.py +40 -0
  53. cecli/commands/lint.py +109 -0
  54. cecli/commands/list_sessions.py +56 -0
  55. cecli/commands/load.py +85 -0
  56. cecli/commands/load_session.py +48 -0
  57. cecli/commands/load_skill.py +68 -0
  58. cecli/commands/ls.py +75 -0
  59. cecli/commands/map.py +37 -0
  60. cecli/commands/map_refresh.py +35 -0
  61. cecli/commands/model.py +118 -0
  62. cecli/commands/models.py +41 -0
  63. cecli/commands/multiline_mode.py +38 -0
  64. cecli/commands/paste.py +91 -0
  65. cecli/commands/quit.py +32 -0
  66. cecli/commands/read_only.py +267 -0
  67. cecli/commands/read_only_stub.py +270 -0
  68. cecli/commands/reasoning_effort.py +70 -0
  69. cecli/commands/remove_skill.py +68 -0
  70. cecli/commands/report.py +40 -0
  71. cecli/commands/reset.py +88 -0
  72. cecli/commands/run.py +99 -0
  73. cecli/commands/save.py +49 -0
  74. cecli/commands/save_session.py +43 -0
  75. cecli/commands/settings.py +69 -0
  76. cecli/commands/test.py +58 -0
  77. cecli/commands/think_tokens.py +74 -0
  78. cecli/commands/tokens.py +207 -0
  79. cecli/commands/undo.py +145 -0
  80. cecli/commands/utils/__init__.py +0 -0
  81. cecli/commands/utils/base_command.py +131 -0
  82. cecli/commands/utils/helpers.py +142 -0
  83. cecli/commands/utils/registry.py +53 -0
  84. cecli/commands/utils/save_load_manager.py +98 -0
  85. cecli/commands/voice.py +78 -0
  86. cecli/commands/weak_model.py +123 -0
  87. cecli/commands/web.py +87 -0
  88. cecli/deprecated_args.py +185 -0
  89. cecli/diffs.py +129 -0
  90. cecli/dump.py +29 -0
  91. cecli/editor.py +147 -0
  92. cecli/exceptions.py +115 -0
  93. cecli/format_settings.py +26 -0
  94. cecli/help.py +119 -0
  95. cecli/help_pats.py +19 -0
  96. cecli/helpers/__init__.py +9 -0
  97. cecli/helpers/copypaste.py +123 -0
  98. cecli/helpers/coroutines.py +8 -0
  99. cecli/helpers/file_searcher.py +142 -0
  100. cecli/helpers/model_providers.py +552 -0
  101. cecli/helpers/plugin_manager.py +81 -0
  102. cecli/helpers/profiler.py +162 -0
  103. cecli/helpers/requests.py +77 -0
  104. cecli/helpers/similarity.py +98 -0
  105. cecli/helpers/skills.py +577 -0
  106. cecli/history.py +186 -0
  107. cecli/io.py +1782 -0
  108. cecli/linter.py +304 -0
  109. cecli/llm.py +101 -0
  110. cecli/main.py +1280 -0
  111. cecli/mcp/__init__.py +154 -0
  112. cecli/mcp/oauth.py +250 -0
  113. cecli/mcp/server.py +278 -0
  114. cecli/mdstream.py +243 -0
  115. cecli/models.py +1255 -0
  116. cecli/onboarding.py +301 -0
  117. cecli/prompts/__init__.py +0 -0
  118. cecli/prompts/agent.yml +71 -0
  119. cecli/prompts/architect.yml +35 -0
  120. cecli/prompts/ask.yml +31 -0
  121. cecli/prompts/base.yml +99 -0
  122. cecli/prompts/context.yml +60 -0
  123. cecli/prompts/copypaste.yml +5 -0
  124. cecli/prompts/editblock.yml +143 -0
  125. cecli/prompts/editblock_fenced.yml +106 -0
  126. cecli/prompts/editblock_func.yml +25 -0
  127. cecli/prompts/editor_diff_fenced.yml +115 -0
  128. cecli/prompts/editor_editblock.yml +121 -0
  129. cecli/prompts/editor_whole.yml +46 -0
  130. cecli/prompts/help.yml +37 -0
  131. cecli/prompts/patch.yml +110 -0
  132. cecli/prompts/single_wholefile_func.yml +24 -0
  133. cecli/prompts/udiff.yml +106 -0
  134. cecli/prompts/udiff_simple.yml +13 -0
  135. cecli/prompts/utils/__init__.py +0 -0
  136. cecli/prompts/utils/prompt_registry.py +167 -0
  137. cecli/prompts/utils/system.py +56 -0
  138. cecli/prompts/wholefile.yml +50 -0
  139. cecli/prompts/wholefile_func.yml +24 -0
  140. cecli/queries/tree-sitter-language-pack/README.md +7 -0
  141. cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  142. cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
  143. cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  144. cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
  145. cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
  146. cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
  147. cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
  148. cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  149. cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
  150. cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  151. cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
  152. cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
  153. cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  154. cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
  155. cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
  156. cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
  157. cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
  158. cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  159. cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  160. cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
  161. cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  162. cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  163. cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
  164. cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
  165. cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  166. cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
  167. cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
  168. cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  169. cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
  170. cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  171. cecli/queries/tree-sitter-languages/README.md +24 -0
  172. cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
  173. cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
  174. cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
  175. cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
  176. cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  177. cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
  178. cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
  179. cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
  180. cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
  181. cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
  182. cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  183. cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
  184. cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
  185. cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
  186. cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
  187. cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  188. cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  189. cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
  190. cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
  191. cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
  192. cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
  193. cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
  194. cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
  195. cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
  196. cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
  197. cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
  198. cecli/reasoning_tags.py +82 -0
  199. cecli/repo.py +626 -0
  200. cecli/repomap.py +1368 -0
  201. cecli/report.py +260 -0
  202. cecli/resources/__init__.py +3 -0
  203. cecli/resources/model-metadata.json +25751 -0
  204. cecli/resources/model-settings.yml +2394 -0
  205. cecli/resources/providers.json +67 -0
  206. cecli/run_cmd.py +143 -0
  207. cecli/scrape.py +295 -0
  208. cecli/sendchat.py +250 -0
  209. cecli/sessions.py +281 -0
  210. cecli/special.py +203 -0
  211. cecli/tools/__init__.py +72 -0
  212. cecli/tools/command.py +103 -0
  213. cecli/tools/command_interactive.py +113 -0
  214. cecli/tools/context_manager.py +175 -0
  215. cecli/tools/delete_block.py +154 -0
  216. cecli/tools/delete_line.py +120 -0
  217. cecli/tools/delete_lines.py +144 -0
  218. cecli/tools/extract_lines.py +281 -0
  219. cecli/tools/finished.py +35 -0
  220. cecli/tools/git_branch.py +132 -0
  221. cecli/tools/git_diff.py +49 -0
  222. cecli/tools/git_log.py +43 -0
  223. cecli/tools/git_remote.py +39 -0
  224. cecli/tools/git_show.py +37 -0
  225. cecli/tools/git_status.py +32 -0
  226. cecli/tools/grep.py +242 -0
  227. cecli/tools/indent_lines.py +195 -0
  228. cecli/tools/insert_block.py +263 -0
  229. cecli/tools/list_changes.py +71 -0
  230. cecli/tools/load_skill.py +51 -0
  231. cecli/tools/ls.py +77 -0
  232. cecli/tools/remove_skill.py +51 -0
  233. cecli/tools/replace_all.py +113 -0
  234. cecli/tools/replace_line.py +135 -0
  235. cecli/tools/replace_lines.py +180 -0
  236. cecli/tools/replace_text.py +186 -0
  237. cecli/tools/show_numbered_context.py +137 -0
  238. cecli/tools/thinking.py +52 -0
  239. cecli/tools/undo_change.py +82 -0
  240. cecli/tools/update_todo_list.py +148 -0
  241. cecli/tools/utils/base_tool.py +64 -0
  242. cecli/tools/utils/helpers.py +359 -0
  243. cecli/tools/utils/output.py +119 -0
  244. cecli/tools/utils/registry.py +145 -0
  245. cecli/tools/view_files_matching.py +138 -0
  246. cecli/tools/view_files_with_symbol.py +117 -0
  247. cecli/tui/__init__.py +83 -0
  248. cecli/tui/app.py +971 -0
  249. cecli/tui/io.py +566 -0
  250. cecli/tui/styles.tcss +117 -0
  251. cecli/tui/widgets/__init__.py +19 -0
  252. cecli/tui/widgets/completion_bar.py +331 -0
  253. cecli/tui/widgets/file_list.py +76 -0
  254. cecli/tui/widgets/footer.py +165 -0
  255. cecli/tui/widgets/input_area.py +320 -0
  256. cecli/tui/widgets/key_hints.py +16 -0
  257. cecli/tui/widgets/output.py +354 -0
  258. cecli/tui/widgets/status_bar.py +279 -0
  259. cecli/tui/worker.py +160 -0
  260. cecli/urls.py +16 -0
  261. cecli/utils.py +499 -0
  262. cecli/versioncheck.py +90 -0
  263. cecli/voice.py +90 -0
  264. cecli/waiting.py +38 -0
  265. cecli/watch.py +316 -0
  266. cecli/watch_prompts.py +12 -0
  267. cecli/website/Gemfile +8 -0
  268. cecli/website/_includes/blame.md +162 -0
  269. cecli/website/_includes/get-started.md +22 -0
  270. cecli/website/_includes/help-tip.md +5 -0
  271. cecli/website/_includes/help.md +24 -0
  272. cecli/website/_includes/install.md +5 -0
  273. cecli/website/_includes/keys.md +4 -0
  274. cecli/website/_includes/model-warnings.md +67 -0
  275. cecli/website/_includes/multi-line.md +22 -0
  276. cecli/website/_includes/python-m-aider.md +5 -0
  277. cecli/website/_includes/recording.css +228 -0
  278. cecli/website/_includes/recording.md +34 -0
  279. cecli/website/_includes/replit-pipx.md +9 -0
  280. cecli/website/_includes/works-best.md +1 -0
  281. cecli/website/_sass/custom/custom.scss +103 -0
  282. cecli/website/docs/config/adv-model-settings.md +2498 -0
  283. cecli/website/docs/config/agent-mode.md +320 -0
  284. cecli/website/docs/config/aider_conf.md +548 -0
  285. cecli/website/docs/config/api-keys.md +90 -0
  286. cecli/website/docs/config/custom-commands.md +187 -0
  287. cecli/website/docs/config/dotenv.md +493 -0
  288. cecli/website/docs/config/editor.md +127 -0
  289. cecli/website/docs/config/mcp.md +210 -0
  290. cecli/website/docs/config/model-aliases.md +173 -0
  291. cecli/website/docs/config/options.md +890 -0
  292. cecli/website/docs/config/reasoning.md +210 -0
  293. cecli/website/docs/config/skills.md +172 -0
  294. cecli/website/docs/config/tui.md +126 -0
  295. cecli/website/docs/config.md +44 -0
  296. cecli/website/docs/faq.md +379 -0
  297. cecli/website/docs/git.md +76 -0
  298. cecli/website/docs/index.md +47 -0
  299. cecli/website/docs/install/codespaces.md +39 -0
  300. cecli/website/docs/install/docker.md +48 -0
  301. cecli/website/docs/install/optional.md +100 -0
  302. cecli/website/docs/install/replit.md +8 -0
  303. cecli/website/docs/install.md +115 -0
  304. cecli/website/docs/languages.md +264 -0
  305. cecli/website/docs/legal/contributor-agreement.md +111 -0
  306. cecli/website/docs/legal/privacy.md +104 -0
  307. cecli/website/docs/llms/anthropic.md +77 -0
  308. cecli/website/docs/llms/azure.md +48 -0
  309. cecli/website/docs/llms/bedrock.md +132 -0
  310. cecli/website/docs/llms/cohere.md +34 -0
  311. cecli/website/docs/llms/deepseek.md +32 -0
  312. cecli/website/docs/llms/gemini.md +49 -0
  313. cecli/website/docs/llms/github.md +111 -0
  314. cecli/website/docs/llms/groq.md +36 -0
  315. cecli/website/docs/llms/lm-studio.md +39 -0
  316. cecli/website/docs/llms/ollama.md +75 -0
  317. cecli/website/docs/llms/openai-compat.md +39 -0
  318. cecli/website/docs/llms/openai.md +58 -0
  319. cecli/website/docs/llms/openrouter.md +78 -0
  320. cecli/website/docs/llms/other.md +117 -0
  321. cecli/website/docs/llms/vertex.md +50 -0
  322. cecli/website/docs/llms/warnings.md +10 -0
  323. cecli/website/docs/llms/xai.md +53 -0
  324. cecli/website/docs/llms.md +54 -0
  325. cecli/website/docs/more/analytics.md +127 -0
  326. cecli/website/docs/more/edit-formats.md +116 -0
  327. cecli/website/docs/more/infinite-output.md +192 -0
  328. cecli/website/docs/more-info.md +8 -0
  329. cecli/website/docs/recordings/auto-accept-architect.md +31 -0
  330. cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  331. cecli/website/docs/recordings/index.md +21 -0
  332. cecli/website/docs/recordings/model-accepts-settings.md +69 -0
  333. cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  334. cecli/website/docs/repomap.md +112 -0
  335. cecli/website/docs/scripting.md +100 -0
  336. cecli/website/docs/sessions.md +213 -0
  337. cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
  338. cecli/website/docs/troubleshooting/edit-errors.md +76 -0
  339. cecli/website/docs/troubleshooting/imports.md +62 -0
  340. cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
  341. cecli/website/docs/troubleshooting/support.md +79 -0
  342. cecli/website/docs/troubleshooting/token-limits.md +96 -0
  343. cecli/website/docs/troubleshooting/warnings.md +12 -0
  344. cecli/website/docs/troubleshooting.md +11 -0
  345. cecli/website/docs/usage/browser.md +57 -0
  346. cecli/website/docs/usage/caching.md +49 -0
  347. cecli/website/docs/usage/commands.md +133 -0
  348. cecli/website/docs/usage/conventions.md +119 -0
  349. cecli/website/docs/usage/copypaste.md +136 -0
  350. cecli/website/docs/usage/images-urls.md +48 -0
  351. cecli/website/docs/usage/lint-test.md +118 -0
  352. cecli/website/docs/usage/modes.md +211 -0
  353. cecli/website/docs/usage/not-code.md +179 -0
  354. cecli/website/docs/usage/notifications.md +87 -0
  355. cecli/website/docs/usage/tips.md +79 -0
  356. cecli/website/docs/usage/tutorials.md +30 -0
  357. cecli/website/docs/usage/voice.md +121 -0
  358. cecli/website/docs/usage/watch.md +294 -0
  359. cecli/website/docs/usage.md +102 -0
  360. cecli/website/share/index.md +101 -0
  361. cecli_dev-0.95.5.dist-info/METADATA +549 -0
  362. cecli_dev-0.95.5.dist-info/RECORD +366 -0
  363. cecli_dev-0.95.5.dist-info/WHEEL +5 -0
  364. cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
  365. cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
  366. cecli_dev-0.95.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,577 @@
1
+ """
2
+ Skills helper for cecli.
3
+
4
+ This module provides functions for loading, parsing, and managing skills
5
+ according to the Skills specification.
6
+ """
7
+
8
+ import re
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import yaml
14
+
15
+
16
+ @dataclass
17
+ class SkillMetadata:
18
+ """Metadata for an skill."""
19
+
20
+ name: str
21
+ description: str
22
+ path: Path
23
+ license: Optional[str] = None
24
+ allowed_tools: List[str] = field(default_factory=list)
25
+ metadata: Dict[str, Any] = field(default_factory=dict)
26
+
27
+
28
+ @dataclass
29
+ class SkillContent:
30
+ """Complete skill content including metadata and instructions."""
31
+
32
+ metadata: SkillMetadata
33
+ frontmatter: Dict[str, Any]
34
+ instructions: str
35
+ references: Dict[str, Path] = field(default_factory=dict)
36
+ scripts: Dict[str, Path] = field(default_factory=dict)
37
+ assets: Dict[str, Path] = field(default_factory=dict)
38
+
39
+
40
+ class SkillsManager:
41
+ """Manager for loading and managing skills."""
42
+
43
+ def __init__(
44
+ self,
45
+ directory_paths: List[str],
46
+ include_list: Optional[List[str]] = None,
47
+ exclude_list: Optional[List[str]] = None,
48
+ git_root: Optional[str] = None,
49
+ coder=None,
50
+ ):
51
+ """
52
+ Initialize the skills manager.
53
+
54
+ Args:
55
+ directory_paths: List of directory paths to search for skills
56
+ include_list: Optional list of skill names to include (whitelist)
57
+ exclude_list: Optional list of skill names to exclude (blacklist)
58
+ git_root: Optional git root directory for relative path resolution
59
+ coder: Optional reference to the coder instance (weak reference)
60
+ """
61
+ self.directory_paths = [Path(p).expanduser().resolve() for p in directory_paths]
62
+ self.include_list = set(include_list) if include_list else None
63
+ self.exclude_list = set(exclude_list) if exclude_list else set()
64
+ self.git_root = Path(git_root).expanduser().resolve() if git_root else None
65
+ self.coder = coder # Weak reference to coder instance
66
+
67
+ # Cache for loaded skills
68
+ self._skills_cache: Dict[str, SkillContent] = {}
69
+ self._skill_metadata_cache: Dict[str, SkillMetadata] = {}
70
+ self._skills_find_cache: Optional[List[SkillMetadata]] = None
71
+
72
+ # Track which skills have been loaded via load_skill()
73
+ self._loaded_skills: set[str] = set()
74
+
75
+ def find_skills(self, reload: bool = False) -> List[SkillMetadata]:
76
+ """
77
+ Find all skills in the configured directory paths.
78
+
79
+ Args:
80
+ reload: If True, force reload from disk instead of using cache
81
+
82
+ Returns:
83
+ List of skill metadata objects
84
+ """
85
+ # Return cached results if available and not forced to reload
86
+ if not reload and self._skills_find_cache is not None:
87
+ return self._skills_find_cache
88
+
89
+ skills = []
90
+
91
+ for directory_path in self.directory_paths:
92
+ if not directory_path.exists():
93
+ continue
94
+
95
+ # Look for directories containing SKILL.md files
96
+ for skill_dir in directory_path.iterdir():
97
+ if not skill_dir.is_dir():
98
+ continue
99
+
100
+ skill_md_path = skill_dir / "SKILL.md"
101
+ if skill_md_path.exists():
102
+ try:
103
+ metadata = self._parse_skill_metadata(skill_md_path)
104
+ skill_name = metadata.name
105
+
106
+ # Apply include/exclude filters
107
+ if self.include_list and skill_name not in self.include_list:
108
+ continue
109
+ if skill_name in self.exclude_list:
110
+ continue
111
+
112
+ skills.append(metadata)
113
+ self._skill_metadata_cache[skill_name] = metadata
114
+ except Exception:
115
+ # Skip skills that can't be parsed
116
+ continue
117
+
118
+ # Cache the results
119
+ self._skills_find_cache = skills
120
+ return skills
121
+
122
+ def _parse_skill_metadata(self, skill_md_path: Path) -> SkillMetadata:
123
+ """
124
+ Parse the metadata from a SKILL.md file.
125
+
126
+ Args:
127
+ skill_md_path: Path to the SKILL.md file
128
+
129
+ Returns:
130
+ SkillMetadata object
131
+ """
132
+ content = skill_md_path.read_text(encoding="utf-8")
133
+
134
+ # Parse YAML frontmatter (between --- markers)
135
+ frontmatter_match = re.search(
136
+ r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL | re.MULTILINE
137
+ )
138
+ if not frontmatter_match:
139
+ raise ValueError(f"No YAML frontmatter found in {skill_md_path}")
140
+
141
+ frontmatter = yaml.safe_load(frontmatter_match.group(1))
142
+
143
+ # Extract required fields
144
+ name = frontmatter.get("name")
145
+ description = frontmatter.get("description")
146
+
147
+ if not name or not description:
148
+ raise ValueError(f"Missing required fields (name or description) in {skill_md_path}")
149
+
150
+ return SkillMetadata(
151
+ name=name,
152
+ description=description,
153
+ path=skill_md_path.parent,
154
+ license=frontmatter.get("license"),
155
+ allowed_tools=frontmatter.get("allowed-tools", []),
156
+ metadata=frontmatter.get("metadata", {}),
157
+ )
158
+
159
+ def get_skill_content(self, skill_name: str) -> Optional[SkillContent]:
160
+ """
161
+ Get skill content by name (loads and caches if not already loaded).
162
+
163
+ Args:
164
+ skill_name: Name of the skill to get
165
+
166
+ Returns:
167
+ SkillContent object or None if not found
168
+ """
169
+ # Check cache first
170
+ if skill_name in self._skills_cache:
171
+ return self._skills_cache[skill_name]
172
+
173
+ # Find the skill metadata
174
+ if skill_name not in self._skill_metadata_cache:
175
+ # Try to find it
176
+ skills = self.find_skills()
177
+ skill_metadata = next((s for s in skills if s.name == skill_name), None)
178
+ if not skill_metadata:
179
+ return None
180
+ self._skill_metadata_cache[skill_name] = skill_metadata
181
+ else:
182
+ skill_metadata = self._skill_metadata_cache[skill_name]
183
+
184
+ # Load the complete skill
185
+ skill_content = self._load_complete_skill(skill_metadata)
186
+ self._skills_cache[skill_name] = skill_content
187
+
188
+ return skill_content
189
+
190
+ def _load_complete_skill(self, metadata: SkillMetadata) -> SkillContent:
191
+ """
192
+ Load a complete skill including all components.
193
+
194
+ Args:
195
+ metadata: SkillMetadata object
196
+
197
+ Returns:
198
+ SkillContent object
199
+ """
200
+ skill_dir = metadata.path
201
+
202
+ # Load SKILL.md content
203
+ skill_md_path = skill_dir / "SKILL.md"
204
+ content = skill_md_path.read_text(encoding="utf-8")
205
+
206
+ # Parse frontmatter and instructions
207
+ frontmatter_match = re.search(
208
+ r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL | re.MULTILINE
209
+ )
210
+ if not frontmatter_match:
211
+ raise ValueError(f"No YAML frontmatter found in {skill_md_path}")
212
+
213
+ frontmatter = yaml.safe_load(frontmatter_match.group(1))
214
+ instructions = content[frontmatter_match.end() :].strip()
215
+
216
+ # Load references
217
+ references = self._load_references(skill_dir)
218
+
219
+ # Load scripts
220
+ scripts = self._load_scripts(skill_dir)
221
+
222
+ # Load assets
223
+ assets = self._load_assets(skill_dir)
224
+
225
+ return SkillContent(
226
+ metadata=metadata,
227
+ frontmatter=frontmatter,
228
+ instructions=instructions,
229
+ references=references,
230
+ scripts=scripts,
231
+ assets=assets,
232
+ )
233
+
234
+ def _load_references(self, skill_dir: Path) -> Dict[str, Path]:
235
+ """Load reference files from the references/ directory."""
236
+ references = {}
237
+ references_dir = skill_dir / "references"
238
+
239
+ if references_dir.exists():
240
+ for ref_file in references_dir.glob("**/*.md"):
241
+ try:
242
+ # Use relative path as key, store the Path object
243
+ rel_path = ref_file.relative_to(references_dir)
244
+ references[str(rel_path)] = ref_file
245
+ except Exception:
246
+ continue
247
+
248
+ return references
249
+
250
+ def _load_scripts(self, skill_dir: Path) -> Dict[str, Path]:
251
+ """Load script files from the scripts/ directory."""
252
+ scripts = {}
253
+ scripts_dir = skill_dir / "scripts"
254
+
255
+ if scripts_dir.exists():
256
+ for script_file in scripts_dir.glob("**/*"):
257
+ if script_file.is_file():
258
+ try:
259
+ # Use relative path as key, store the Path object
260
+ rel_path = script_file.relative_to(scripts_dir)
261
+ scripts[str(rel_path)] = script_file
262
+ except Exception:
263
+ continue
264
+
265
+ return scripts
266
+
267
+ def _load_assets(self, skill_dir: Path) -> Dict[str, Path]:
268
+ """Load asset files from the assets/ directory."""
269
+ assets = {}
270
+ assets_dir = skill_dir / "assets"
271
+
272
+ if assets_dir.exists():
273
+ for asset_file in assets_dir.glob("**/*"):
274
+ if asset_file.is_file():
275
+ try:
276
+ # Use relative path as key, store the Path object
277
+ rel_path = asset_file.relative_to(assets_dir)
278
+ assets[str(rel_path)] = asset_file
279
+ except Exception:
280
+ continue
281
+
282
+ return assets
283
+
284
+ def get_skill_summary(self, skill_name: str) -> Optional[str]:
285
+ """
286
+ Get a summary of a skill for display purposes.
287
+
288
+ Args:
289
+ skill_name: Name of the skill
290
+
291
+ Returns:
292
+ Summary string or None if skill not found
293
+ """
294
+ skill = self.get_skill_content(skill_name)
295
+ if not skill:
296
+ return None
297
+
298
+ summary = f"Skill: {skill.metadata.name}\n"
299
+ summary += f"Description: {skill.metadata.description}\n"
300
+
301
+ if skill.metadata.license:
302
+ summary += f"License: {skill.metadata.license}\n"
303
+
304
+ if skill.metadata.allowed_tools:
305
+ summary += f"Allowed tools: {', '.join(skill.metadata.allowed_tools)}\n"
306
+
307
+ summary += f"Path: {skill.metadata.path}\n"
308
+
309
+ # Count resources
310
+ ref_count = len(skill.references)
311
+ script_count = len(skill.scripts)
312
+ asset_count = len(skill.assets)
313
+
314
+ summary += (
315
+ f"Resources: {ref_count} references, {script_count} scripts, {asset_count} assets\n"
316
+ )
317
+
318
+ return summary
319
+
320
+ def get_all_skill_summaries(self) -> Dict[str, str]:
321
+ """
322
+ Get summaries for all available skills.
323
+
324
+ Returns:
325
+ Dictionary mapping skill names to summary strings
326
+ """
327
+ skills = self.find_skills()
328
+ summaries = {}
329
+
330
+ for skill_metadata in skills:
331
+ summary = self.get_skill_summary(skill_metadata.name)
332
+ if summary:
333
+ summaries[skill_metadata.name] = summary
334
+
335
+ return summaries
336
+
337
+ def load_skill(self, skill_name: str) -> str:
338
+ """
339
+ Add a skill to the loaded skills set for inclusion in context.
340
+
341
+ Returns:
342
+ Success or error message
343
+ """
344
+ if not skill_name:
345
+ return "Error: Skill name is required."
346
+
347
+ # Check if coder is available
348
+ if not self.coder:
349
+ return "Error: Skills manager not connected to a coder instance."
350
+
351
+ # Check if we're in agent mode
352
+ if not hasattr(self.coder, "edit_format") or self.coder.edit_format != "agent":
353
+ return "Error: Skill loading is only available in agent mode."
354
+
355
+ # Check if skill is already loaded
356
+ if skill_name in self._loaded_skills:
357
+ return f"Skill '{skill_name}' is already loaded."
358
+
359
+ # Find the skill to verify it exists
360
+ skills = self.find_skills()
361
+ skill_found = any(skill.name == skill_name for skill in skills)
362
+
363
+ if skill_found:
364
+ # Load the skill content
365
+ skill_content = self.get_skill_content(skill_name)
366
+
367
+ if skill_content:
368
+ # Add to loaded skills set
369
+ self._loaded_skills.add(skill_name)
370
+
371
+ result = f"Skill '{skill_name}' loaded successfully."
372
+
373
+ # Show skill summary
374
+ summary = self.get_skill_summary(skill_name)
375
+ if summary:
376
+ result += f"\n\n{summary}"
377
+ return result
378
+ else:
379
+ return f"Error: Skill '{skill_name}' found but could not be loaded."
380
+ else:
381
+ return f"Error: Skill '{skill_name}' not found in configured directories."
382
+
383
+ def remove_skill(self, skill_name: str) -> str:
384
+ """
385
+ Remove a skill from the loaded skills set.
386
+
387
+ Returns:
388
+ Success or error message
389
+ """
390
+ if not skill_name:
391
+ return "Error: Skill name is required."
392
+
393
+ # Check if coder is available
394
+ if not self.coder:
395
+ return "Error: Skills manager not connected to a coder instance."
396
+
397
+ # Check if we're in agent mode
398
+ if not hasattr(self.coder, "edit_format") or self.coder.edit_format != "agent":
399
+ return "Error: Skill removal is only available in agent mode."
400
+
401
+ # Check if skill is already removed
402
+ if skill_name not in self._loaded_skills:
403
+ return f"Skill '{skill_name}' is not loaded."
404
+
405
+ # Remove from loaded skills set
406
+ self._loaded_skills.remove(skill_name)
407
+
408
+ return f"Skill '{skill_name}' removed successfully."
409
+
410
+ @classmethod
411
+ def skill_summary_loader(
412
+ cls,
413
+ directory_paths: List[str],
414
+ include_list: Optional[List[str]] = None,
415
+ exclude_list: Optional[List[str]] = None,
416
+ git_root: Optional[str] = None,
417
+ ) -> str:
418
+ """
419
+ High-level function to load and summarize all available skills.
420
+
421
+ Args:
422
+ directory_paths: List of directory paths to search for skills
423
+ include_list: Optional list of skill names to include (whitelist)
424
+ exclude_list: Optional list of skill names to exclude (blacklist)
425
+ git_root: Optional git root directory for relative path resolution
426
+
427
+ Returns:
428
+ Formatted summary of all available skills
429
+ """
430
+ manager = cls(directory_paths, include_list, exclude_list, git_root)
431
+ summaries = manager.get_all_skill_summaries()
432
+
433
+ if not summaries:
434
+ return "No skills found in the specified directories."
435
+
436
+ result = f"Found {len(summaries)} skill(s):\n\n"
437
+
438
+ for i, (skill_name, summary) in enumerate(summaries.items(), 1):
439
+ result += f"{i}. {summary}\n"
440
+
441
+ return result
442
+
443
+ @staticmethod
444
+ def resolve_skill_directories(
445
+ base_paths: List[str], git_root: Optional[str] = None
446
+ ) -> List[Path]:
447
+ """
448
+ Resolve skill directory paths relative to various locations.
449
+
450
+ Args:
451
+ base_paths: List of base directory paths
452
+ git_root: Optional git root directory
453
+
454
+ Returns:
455
+ List of resolved Path objects
456
+ """
457
+ resolved_paths = []
458
+
459
+ for base_path in base_paths:
460
+ # Try to resolve relative to git root first
461
+ if git_root and not Path(base_path).is_absolute():
462
+ git_path = Path(git_root) / base_path
463
+ if git_path.exists():
464
+ resolved_paths.append(git_path.resolve())
465
+ continue
466
+
467
+ # Try as absolute or relative to current directory
468
+ try:
469
+ path = Path(base_path).expanduser().resolve()
470
+ if path.exists():
471
+ resolved_paths.append(path)
472
+ except Exception:
473
+ continue
474
+
475
+ return resolved_paths
476
+
477
+ def get_skills_content(self) -> Optional[str]:
478
+ """
479
+ Generate a context block with skill metadata and file paths for references, scripts, and assets.
480
+
481
+ Returns:
482
+ Formatted context block string with skill metadata and file paths or None if no skills available
483
+ """
484
+ try:
485
+ # Only return skills that have been explicitly loaded via load_skill()
486
+ if not self._loaded_skills:
487
+ return None
488
+
489
+ result = '<context name="loaded_skills">\n'
490
+ result += "## Loaded Skills Content\n\n"
491
+ result += f"Found {len(self._loaded_skills)} skill(s) in configured directories:\n\n"
492
+
493
+ for i, skill_name in enumerate(sorted(self._loaded_skills)):
494
+ # Load the complete skill (should be cached)
495
+ skill_content = self.get_skill_content(skill_name)
496
+ if not skill_content:
497
+ continue
498
+
499
+ result += f"### Skill {i}: {skill_content.metadata.name}\n\n"
500
+ result += f"**Description**: {skill_content.metadata.description}\n\n"
501
+
502
+ if skill_content.metadata.license:
503
+ result += f"**License**: {skill_content.metadata.license}\n\n"
504
+
505
+ if skill_content.metadata.allowed_tools:
506
+ result += (
507
+ f"**Allowed Tools**: {', '.join(skill_content.metadata.allowed_tools)}\n\n"
508
+ )
509
+
510
+ # Add instructions
511
+ result += "#### Instructions\n\n"
512
+ result += f"{skill_content.instructions}\n\n"
513
+
514
+ # Add references file paths
515
+ if skill_content.references:
516
+ result += "#### References\n\n"
517
+ result += f"Available reference files ({len(skill_content.references)}):\n\n"
518
+ for ref_name, ref_path in skill_content.references.items():
519
+ result += f"- **{ref_name}**: `{ref_path}`\n"
520
+ result += "\n"
521
+
522
+ # Add scripts file paths
523
+ if skill_content.scripts:
524
+ result += "#### Scripts\n\n"
525
+ result += f"Available script files ({len(skill_content.scripts)}):\n\n"
526
+ for script_name, script_path in skill_content.scripts.items():
527
+ result += f"- **{script_name}**: `{script_path}`\n"
528
+ result += "\n"
529
+
530
+ # Add assets file paths
531
+ if skill_content.assets:
532
+ result += f"#### Assets ({len(skill_content.assets)} file(s))\n\n"
533
+ result += "Available asset files:\n\n"
534
+ for asset_name, asset_path in skill_content.assets.items():
535
+ result += f"- **{asset_name}**: `{asset_path}`\n"
536
+ result += "\n"
537
+
538
+ result += "---\n\n"
539
+
540
+ result += "</context>"
541
+ return result
542
+ except Exception:
543
+ # We can't use io.tool_error here since we don't have access to io
544
+ # The caller should handle the exception
545
+ raise
546
+
547
+ def get_skills_context(self) -> Optional[str]:
548
+ """
549
+ Generate a context block for available skills.
550
+
551
+ Returns:
552
+ Formatted context block string or None if no skills available
553
+ """
554
+ try:
555
+ # Get skill summaries
556
+ summaries = self.get_all_skill_summaries()
557
+ if not summaries:
558
+ return None
559
+
560
+ result = '<context name="skills">\n'
561
+ result += "## Available Skills\n\n"
562
+ result += f"Found {len(summaries)} skill(s) in configured directories:\n\n"
563
+
564
+ for i, (skill_name, summary) in enumerate(summaries.items(), 1):
565
+ result += f"### Skill {i}: {skill_name}\n\n"
566
+ result += f"{summary}\n"
567
+
568
+ result += (
569
+ "Use the `LoadSkill` tool with the skill name if "
570
+ "the skill is relevant to the current task."
571
+ )
572
+ result += "</context>"
573
+ return result
574
+ except Exception:
575
+ # We can't use io.tool_error here since we don't have access to io
576
+ # The caller should handle the exception
577
+ raise