mycode-cli 0.1.0__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 (404) hide show
  1. mycode/__init__.py +1 -0
  2. mycode/cli/__init__.py +1 -0
  3. mycode/cli/chat.py +650 -0
  4. mycode/cli/main.py +245 -0
  5. mycode/cli/render.py +693 -0
  6. mycode/cli/runtime.py +271 -0
  7. mycode/cli/theme.py +109 -0
  8. mycode/core/__init__.py +30 -0
  9. mycode/core/agent.py +515 -0
  10. mycode/core/config.py +551 -0
  11. mycode/core/messages.py +166 -0
  12. mycode/core/models.py +144 -0
  13. mycode/core/models_catalog.json +2090 -0
  14. mycode/core/providers/__init__.py +86 -0
  15. mycode/core/providers/anthropic_like.py +366 -0
  16. mycode/core/providers/base.py +351 -0
  17. mycode/core/providers/gemini.py +321 -0
  18. mycode/core/providers/openai_chat.py +356 -0
  19. mycode/core/providers/openai_responses.py +326 -0
  20. mycode/core/session.py +537 -0
  21. mycode/core/system_prompt.md +10 -0
  22. mycode/core/system_prompt.py +319 -0
  23. mycode/core/tools.py +898 -0
  24. mycode/server/__init__.py +1 -0
  25. mycode/server/app.py +52 -0
  26. mycode/server/deps.py +29 -0
  27. mycode/server/routers/__init__.py +7 -0
  28. mycode/server/routers/chat.py +320 -0
  29. mycode/server/routers/sessions.py +77 -0
  30. mycode/server/routers/workspaces.py +92 -0
  31. mycode/server/run_manager.py +195 -0
  32. mycode/server/schemas.py +66 -0
  33. mycode/server/static/assets/EditDiff-C1ql7kft.js +12 -0
  34. mycode/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  35. mycode/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  36. mycode/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  37. mycode/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  38. mycode/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  39. mycode/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  40. mycode/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  41. mycode/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  42. mycode/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  43. mycode/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  44. mycode/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  45. mycode/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  46. mycode/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  47. mycode/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  48. mycode/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  49. mycode/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  50. mycode/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  51. mycode/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  52. mycode/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  53. mycode/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  54. mycode/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  55. mycode/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  56. mycode/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  57. mycode/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  58. mycode/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  59. mycode/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  60. mycode/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  61. mycode/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  62. mycode/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  63. mycode/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  64. mycode/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  65. mycode/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  66. mycode/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  67. mycode/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  68. mycode/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  69. mycode/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  70. mycode/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  71. mycode/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  72. mycode/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  73. mycode/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  74. mycode/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  75. mycode/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  76. mycode/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  77. mycode/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  78. mycode/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  79. mycode/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  80. mycode/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  81. mycode/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  82. mycode/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  83. mycode/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  84. mycode/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  85. mycode/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  86. mycode/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  87. mycode/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  88. mycode/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  89. mycode/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  90. mycode/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  91. mycode/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  92. mycode/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  93. mycode/server/static/assets/abap-BdImnpbu.js +1 -0
  94. mycode/server/static/assets/actionscript-3-CoDkCxhg.js +1 -0
  95. mycode/server/static/assets/ada-bCR0ucgS.js +1 -0
  96. mycode/server/static/assets/andromeeda-C4gqWexZ.js +1 -0
  97. mycode/server/static/assets/angular-html-DA-rfuFy.js +1 -0
  98. mycode/server/static/assets/angular-ts-BrjP3tb8.js +1 -0
  99. mycode/server/static/assets/apache-Pmp26Uib.js +1 -0
  100. mycode/server/static/assets/apex-D8_7TLub.js +1 -0
  101. mycode/server/static/assets/apl-CORt7UWP.js +1 -0
  102. mycode/server/static/assets/applescript-Co6uUVPk.js +1 -0
  103. mycode/server/static/assets/ara-BRHolxvo.js +1 -0
  104. mycode/server/static/assets/asciidoc-Ve4PFQV2.js +1 -0
  105. mycode/server/static/assets/asm-D_Q5rh1f.js +1 -0
  106. mycode/server/static/assets/astro-HNnZUWAn.js +1 -0
  107. mycode/server/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  108. mycode/server/static/assets/auto-render-xntwXHOX.js +261 -0
  109. mycode/server/static/assets/awk-DMzUqQB5.js +1 -0
  110. mycode/server/static/assets/ayu-dark-DYE7WIF3.js +1 -0
  111. mycode/server/static/assets/ayu-light-BA47KaF1.js +1 -0
  112. mycode/server/static/assets/ayu-mirage-32ctXXKs.js +1 -0
  113. mycode/server/static/assets/ballerina-BFfxhgS-.js +1 -0
  114. mycode/server/static/assets/bat-BkioyH1T.js +1 -0
  115. mycode/server/static/assets/beancount-k_qm7-4y.js +1 -0
  116. mycode/server/static/assets/berry-uYugtg8r.js +1 -0
  117. mycode/server/static/assets/bibtex-CHM0blh-.js +1 -0
  118. mycode/server/static/assets/bicep-Bmn6On1c.js +1 -0
  119. mycode/server/static/assets/bird2-BIv1doCn.js +1 -0
  120. mycode/server/static/assets/blade-BjGOyj-B.js +1 -0
  121. mycode/server/static/assets/bsl-BO_Y6i37.js +1 -0
  122. mycode/server/static/assets/c-BIGW1oBm.js +1 -0
  123. mycode/server/static/assets/c3-eo99z4R2.js +1 -0
  124. mycode/server/static/assets/cadence-Bv_4Rxtq.js +1 -0
  125. mycode/server/static/assets/cairo-KRGpt6FW.js +1 -0
  126. mycode/server/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  127. mycode/server/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  128. mycode/server/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  129. mycode/server/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  130. mycode/server/static/assets/clarity-D53aC0YG.js +1 -0
  131. mycode/server/static/assets/clojure-P80f7IUj.js +1 -0
  132. mycode/server/static/assets/cmake-D1j8_8rp.js +1 -0
  133. mycode/server/static/assets/cobol-nBiQ_Alo.js +1 -0
  134. mycode/server/static/assets/codeowners-Bp6g37R7.js +1 -0
  135. mycode/server/static/assets/codeql-DsOJ9woJ.js +1 -0
  136. mycode/server/static/assets/coffee-Ch7k5sss.js +1 -0
  137. mycode/server/static/assets/common-lisp-Cg-RD9OK.js +1 -0
  138. mycode/server/static/assets/coq-DkFqJrB1.js +1 -0
  139. mycode/server/static/assets/cpp-CofmeUqb.js +1 -0
  140. mycode/server/static/assets/crystal-DNxU26gB.js +1 -0
  141. mycode/server/static/assets/csharp-COcwbKMJ.js +1 -0
  142. mycode/server/static/assets/css-CLj8gQPS.js +1 -0
  143. mycode/server/static/assets/csv-fuZLfV_i.js +1 -0
  144. mycode/server/static/assets/cue-D82EKSYY.js +1 -0
  145. mycode/server/static/assets/cypher-COkxafJQ.js +1 -0
  146. mycode/server/static/assets/d-85-TOEBH.js +1 -0
  147. mycode/server/static/assets/dark-plus-C3mMm8J8.js +1 -0
  148. mycode/server/static/assets/dart-bE4Kk8sk.js +1 -0
  149. mycode/server/static/assets/dax-CEL-wOlO.js +1 -0
  150. mycode/server/static/assets/desktop-BmXAJ9_W.js +1 -0
  151. mycode/server/static/assets/diff-D97Zzqfu.js +1 -0
  152. mycode/server/static/assets/docker-BcOcwvcX.js +1 -0
  153. mycode/server/static/assets/dotenv-Da5cRb03.js +1 -0
  154. mycode/server/static/assets/dracula-BzJJZx-M.js +1 -0
  155. mycode/server/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  156. mycode/server/static/assets/dream-maker-BtqSS_iP.js +1 -0
  157. mycode/server/static/assets/edge-FbVlp4U3.js +1 -0
  158. mycode/server/static/assets/elixir-CkH2-t6x.js +1 -0
  159. mycode/server/static/assets/elm-DbKCFpqz.js +1 -0
  160. mycode/server/static/assets/emacs-lisp-CXvaQtF9.js +1 -0
  161. mycode/server/static/assets/erb-BYCe7drp.js +1 -0
  162. mycode/server/static/assets/erlang-DsQrWhSR.js +1 -0
  163. mycode/server/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  164. mycode/server/static/assets/everforest-light-C8M2exoo.js +1 -0
  165. mycode/server/static/assets/fennel-BYunw83y.js +1 -0
  166. mycode/server/static/assets/fish-BvzEVeQv.js +1 -0
  167. mycode/server/static/assets/fluent-C4IJs8-o.js +1 -0
  168. mycode/server/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  169. mycode/server/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
  170. mycode/server/static/assets/fsharp-CXgrBDvD.js +1 -0
  171. mycode/server/static/assets/gdresource-BOOCDP_w.js +1 -0
  172. mycode/server/static/assets/gdscript-C5YyOfLZ.js +1 -0
  173. mycode/server/static/assets/gdshader-DkwncUOv.js +1 -0
  174. mycode/server/static/assets/genie-D0YGMca9.js +1 -0
  175. mycode/server/static/assets/gherkin-DyxjwDmM.js +1 -0
  176. mycode/server/static/assets/git-commit-F4YmCXRG.js +1 -0
  177. mycode/server/static/assets/git-rebase-r7XF79zn.js +1 -0
  178. mycode/server/static/assets/github-dark-DHJKELXO.js +1 -0
  179. mycode/server/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  180. mycode/server/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  181. mycode/server/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  182. mycode/server/static/assets/github-light-DAi9KRSo.js +1 -0
  183. mycode/server/static/assets/github-light-default-D7oLnXFd.js +1 -0
  184. mycode/server/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  185. mycode/server/static/assets/gleam-BspZqrRM.js +1 -0
  186. mycode/server/static/assets/glimmer-js-ByusRIyA.js +1 -0
  187. mycode/server/static/assets/glimmer-ts-BfAWNZQY.js +1 -0
  188. mycode/server/static/assets/glsl-DplSGwfg.js +1 -0
  189. mycode/server/static/assets/gn-n2N0HUVH.js +1 -0
  190. mycode/server/static/assets/gnuplot-DdkO51Og.js +1 -0
  191. mycode/server/static/assets/go-C27-OAKa.js +1 -0
  192. mycode/server/static/assets/graphql-ChdNCCLP.js +1 -0
  193. mycode/server/static/assets/groovy-gcz8RCvz.js +1 -0
  194. mycode/server/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  195. mycode/server/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  196. mycode/server/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  197. mycode/server/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  198. mycode/server/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  199. mycode/server/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  200. mycode/server/static/assets/hack-i7_Ulhet.js +1 -0
  201. mycode/server/static/assets/haml-D5jkg6IW.js +1 -0
  202. mycode/server/static/assets/handlebars-BpdQsYii.js +1 -0
  203. mycode/server/static/assets/haskell-Df6bDoY_.js +1 -0
  204. mycode/server/static/assets/haxe-CzTSHFRz.js +1 -0
  205. mycode/server/static/assets/hcl-BWvSN4gD.js +1 -0
  206. mycode/server/static/assets/hjson-D5-asLiD.js +1 -0
  207. mycode/server/static/assets/hlsl-D3lLCCz7.js +1 -0
  208. mycode/server/static/assets/horizon-BUw7H-hv.js +1 -0
  209. mycode/server/static/assets/horizon-bright-CUuTKBJd.js +1 -0
  210. mycode/server/static/assets/houston-DnULxvSX.js +1 -0
  211. mycode/server/static/assets/html-derivative-DlHx6ybY.js +1 -0
  212. mycode/server/static/assets/html-pp8916En.js +1 -0
  213. mycode/server/static/assets/http-jrhK8wxY.js +1 -0
  214. mycode/server/static/assets/hurl-irOxFIW8.js +1 -0
  215. mycode/server/static/assets/hxml-Bvhsp5Yf.js +1 -0
  216. mycode/server/static/assets/hy-DFXneXwc.js +1 -0
  217. mycode/server/static/assets/imba-DGztddWO.js +1 -0
  218. mycode/server/static/assets/index-B4e4WQPq.css +1 -0
  219. mycode/server/static/assets/index-C2xTNJGd.js +203 -0
  220. mycode/server/static/assets/ini-BEwlwnbL.js +1 -0
  221. mycode/server/static/assets/java-CylS5w8V.js +1 -0
  222. mycode/server/static/assets/javascript-wDzz0qaB.js +1 -0
  223. mycode/server/static/assets/jinja-f2NsQr07.js +1 -0
  224. mycode/server/static/assets/jison-wvAkD_A8.js +1 -0
  225. mycode/server/static/assets/json-Cp-IABpG.js +1 -0
  226. mycode/server/static/assets/json5-C9tS-k6U.js +1 -0
  227. mycode/server/static/assets/jsonc-Des-eS-w.js +1 -0
  228. mycode/server/static/assets/jsonl-DcaNXYhu.js +1 -0
  229. mycode/server/static/assets/jsonnet-DFQXde-d.js +1 -0
  230. mycode/server/static/assets/jssm-C2t-YnRu.js +1 -0
  231. mycode/server/static/assets/jsx-g9-lgVsj.js +1 -0
  232. mycode/server/static/assets/julia-CxzCAyBv.js +1 -0
  233. mycode/server/static/assets/just-VxiPbLrw.js +1 -0
  234. mycode/server/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  235. mycode/server/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  236. mycode/server/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  237. mycode/server/static/assets/katex-DnJR2-55.css +1 -0
  238. mycode/server/static/assets/kdl-DV7GczEv.js +1 -0
  239. mycode/server/static/assets/kotlin-BdnUsdx6.js +1 -0
  240. mycode/server/static/assets/kusto-wEQ09or8.js +1 -0
  241. mycode/server/static/assets/laserwave-DUszq2jm.js +1 -0
  242. mycode/server/static/assets/latex-CWtU0Tv5.js +1 -0
  243. mycode/server/static/assets/lean-BZvkOJ9d.js +1 -0
  244. mycode/server/static/assets/less-B1dDrJ26.js +1 -0
  245. mycode/server/static/assets/light-plus-B7mTdjB0.js +1 -0
  246. mycode/server/static/assets/liquid-C0sCDyMI.js +1 -0
  247. mycode/server/static/assets/llvm-DjAJT7YJ.js +1 -0
  248. mycode/server/static/assets/log-2UxHyX5q.js +1 -0
  249. mycode/server/static/assets/logo-BtOb2qkB.js +1 -0
  250. mycode/server/static/assets/lua-BaeVxFsk.js +1 -0
  251. mycode/server/static/assets/luau-C-HG3fhB.js +1 -0
  252. mycode/server/static/assets/make-CHLpvVh8.js +1 -0
  253. mycode/server/static/assets/markdown-Cvjx9yec.js +1 -0
  254. mycode/server/static/assets/marko-DjSrsDqO.js +1 -0
  255. mycode/server/static/assets/material-theme-D5KoaKCx.js +1 -0
  256. mycode/server/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  257. mycode/server/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  258. mycode/server/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  259. mycode/server/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  260. mycode/server/static/assets/matlab-D7o27uSR.js +1 -0
  261. mycode/server/static/assets/mdc-DTYItulj.js +1 -0
  262. mycode/server/static/assets/mdx-Cmh6b_Ma.js +1 -0
  263. mycode/server/static/assets/mermaid-mWjccvbQ.js +1 -0
  264. mycode/server/static/assets/min-dark-CafNBF8u.js +1 -0
  265. mycode/server/static/assets/min-light-CTRr51gU.js +1 -0
  266. mycode/server/static/assets/mipsasm-CKIfxQSi.js +1 -0
  267. mycode/server/static/assets/mojo-rZm6bMo-.js +1 -0
  268. mycode/server/static/assets/monokai-D4h5O-jR.js +1 -0
  269. mycode/server/static/assets/moonbit-_H4v1dQx.js +1 -0
  270. mycode/server/static/assets/move-IF9eRakj.js +1 -0
  271. mycode/server/static/assets/narrat-DRg8JJMk.js +1 -0
  272. mycode/server/static/assets/nextflow-C-mBbutL.js +1 -0
  273. mycode/server/static/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  274. mycode/server/static/assets/nginx-BpAMiNFr.js +1 -0
  275. mycode/server/static/assets/night-owl-C39BiMTA.js +1 -0
  276. mycode/server/static/assets/night-owl-light-CMTm3GFP.js +1 -0
  277. mycode/server/static/assets/nim-BIad80T-.js +1 -0
  278. mycode/server/static/assets/nix-CwoSXNpI.js +1 -0
  279. mycode/server/static/assets/nord-Ddv68eIx.js +1 -0
  280. mycode/server/static/assets/nushell-Cz2AlsmD.js +1 -0
  281. mycode/server/static/assets/objective-c-DXmwc3jG.js +1 -0
  282. mycode/server/static/assets/objective-cpp-CLxacb5B.js +1 -0
  283. mycode/server/static/assets/ocaml-C0hk2d4L.js +1 -0
  284. mycode/server/static/assets/odin-BBf5iR-q.js +1 -0
  285. mycode/server/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  286. mycode/server/static/assets/one-light-C3Wv6jpd.js +1 -0
  287. mycode/server/static/assets/openscad-C4EeE6gA.js +1 -0
  288. mycode/server/static/assets/pascal-D93ZcfNL.js +1 -0
  289. mycode/server/static/assets/perl-NvoQZIq0.js +1 -0
  290. mycode/server/static/assets/php-R6g_5hLQ.js +1 -0
  291. mycode/server/static/assets/pkl-u5AG7uiY.js +1 -0
  292. mycode/server/static/assets/plastic-3e1v2bzS.js +1 -0
  293. mycode/server/static/assets/plsql-ChMvpjG-.js +1 -0
  294. mycode/server/static/assets/po-BTJTHyun.js +1 -0
  295. mycode/server/static/assets/poimandres-CS3Unz2-.js +1 -0
  296. mycode/server/static/assets/polar-C0HS_06l.js +1 -0
  297. mycode/server/static/assets/postcss-CXtECtnM.js +1 -0
  298. mycode/server/static/assets/powerquery-CEu0bR-o.js +1 -0
  299. mycode/server/static/assets/powershell-Dpen1YoG.js +1 -0
  300. mycode/server/static/assets/prisma-Dd19v3D-.js +1 -0
  301. mycode/server/static/assets/prolog-CbFg5uaA.js +1 -0
  302. mycode/server/static/assets/proto-C7zT0LnQ.js +1 -0
  303. mycode/server/static/assets/pug-DKIMFp6K.js +1 -0
  304. mycode/server/static/assets/puppet-BMWR74SV.js +1 -0
  305. mycode/server/static/assets/purescript-CklMAg4u.js +1 -0
  306. mycode/server/static/assets/python-B6aJPvgy.js +1 -0
  307. mycode/server/static/assets/qml-3beO22l8.js +1 -0
  308. mycode/server/static/assets/qmldir-C8lEn-DE.js +1 -0
  309. mycode/server/static/assets/qss-IeuSbFQv.js +1 -0
  310. mycode/server/static/assets/r-Dspwwk_N.js +1 -0
  311. mycode/server/static/assets/racket-BqYA7rlc.js +1 -0
  312. mycode/server/static/assets/raku-DXvB9xmW.js +1 -0
  313. mycode/server/static/assets/razor-BDqjjVU7.js +1 -0
  314. mycode/server/static/assets/red-bN70gL4F.js +1 -0
  315. mycode/server/static/assets/reg-C-SQnVFl.js +1 -0
  316. mycode/server/static/assets/regexp-CDVJQ6XC.js +1 -0
  317. mycode/server/static/assets/rel-C3B-1QV4.js +1 -0
  318. mycode/server/static/assets/riscv-BM1_JUlF.js +1 -0
  319. mycode/server/static/assets/ron-D8l8udqQ.js +1 -0
  320. mycode/server/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  321. mycode/server/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  322. mycode/server/static/assets/rose-pine-qdsjHGoJ.js +1 -0
  323. mycode/server/static/assets/rosmsg-BJDFO7_C.js +1 -0
  324. mycode/server/static/assets/rst-CRjBmOyv.js +1 -0
  325. mycode/server/static/assets/ruby-Wjq7vjNf.js +1 -0
  326. mycode/server/static/assets/rust-B1yitclQ.js +1 -0
  327. mycode/server/static/assets/sas-cz2c8ADy.js +1 -0
  328. mycode/server/static/assets/sass-Cj5Yp3dK.js +1 -0
  329. mycode/server/static/assets/scala-C151Ov-r.js +1 -0
  330. mycode/server/static/assets/scheme-C98Dy4si.js +1 -0
  331. mycode/server/static/assets/scss-D5BDwBP9.js +1 -0
  332. mycode/server/static/assets/sdbl-DVxCFoDh.js +1 -0
  333. mycode/server/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
  334. mycode/server/static/assets/shellscript-Yzrsuije.js +1 -0
  335. mycode/server/static/assets/shellsession-BADoaaVG.js +1 -0
  336. mycode/server/static/assets/slack-dark-BthQWCQV.js +1 -0
  337. mycode/server/static/assets/slack-ochin-DqwNpetd.js +1 -0
  338. mycode/server/static/assets/smalltalk-BERRCDM3.js +1 -0
  339. mycode/server/static/assets/snazzy-light-Bw305WKR.js +1 -0
  340. mycode/server/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  341. mycode/server/static/assets/solarized-light-L9t79GZl.js +1 -0
  342. mycode/server/static/assets/solidity-rGO070M0.js +1 -0
  343. mycode/server/static/assets/soy-8wufbnw4.js +1 -0
  344. mycode/server/static/assets/sparql-rVzFXLq3.js +1 -0
  345. mycode/server/static/assets/splunk-BtCnVYZw.js +1 -0
  346. mycode/server/static/assets/sql-BLtJtn59.js +1 -0
  347. mycode/server/static/assets/ssh-config-_ykCGR6B.js +1 -0
  348. mycode/server/static/assets/stata-BH5u7GGu.js +1 -0
  349. mycode/server/static/assets/stylus-BEDo0Tqx.js +1 -0
  350. mycode/server/static/assets/surrealql-Bq5Q-fJD.js +1 -0
  351. mycode/server/static/assets/svelte-Cy7k_4gC.js +1 -0
  352. mycode/server/static/assets/swift-D82vCrfD.js +1 -0
  353. mycode/server/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  354. mycode/server/static/assets/system-verilog-CnnmHF94.js +1 -0
  355. mycode/server/static/assets/systemd-4A_iFExJ.js +1 -0
  356. mycode/server/static/assets/talonscript-CkByrt1z.js +1 -0
  357. mycode/server/static/assets/tasl-QIJgUcNo.js +1 -0
  358. mycode/server/static/assets/tcl-dwOrl1Do.js +1 -0
  359. mycode/server/static/assets/templ-DhtptRzy.js +1 -0
  360. mycode/server/static/assets/terraform-BETggiCN.js +1 -0
  361. mycode/server/static/assets/tex-idrVyKtj.js +1 -0
  362. mycode/server/static/assets/tokyo-night-hegEt444.js +1 -0
  363. mycode/server/static/assets/toml-vGWfd6FD.js +1 -0
  364. mycode/server/static/assets/ts-tags-DQrlYJgV.js +1 -0
  365. mycode/server/static/assets/tsv-B_m7g4N7.js +1 -0
  366. mycode/server/static/assets/tsx-COt5Ahok.js +1 -0
  367. mycode/server/static/assets/turtle-BsS91CYL.js +1 -0
  368. mycode/server/static/assets/twig-xg9kU7Mw.js +1 -0
  369. mycode/server/static/assets/typescript-BPQ3VLAy.js +1 -0
  370. mycode/server/static/assets/typespec-CAFt9gP4.js +1 -0
  371. mycode/server/static/assets/typst-DHCkPAjA.js +1 -0
  372. mycode/server/static/assets/v-BcVCzyr7.js +1 -0
  373. mycode/server/static/assets/vala-CsfeWuGM.js +1 -0
  374. mycode/server/static/assets/vb-D17OF-Vu.js +1 -0
  375. mycode/server/static/assets/verilog-BQ8w6xss.js +1 -0
  376. mycode/server/static/assets/vesper-DU1UobuO.js +1 -0
  377. mycode/server/static/assets/vhdl-CeAyd5Ju.js +1 -0
  378. mycode/server/static/assets/viml-CJc9bBzg.js +1 -0
  379. mycode/server/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  380. mycode/server/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  381. mycode/server/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  382. mycode/server/static/assets/vue-D2xRrEX4.js +1 -0
  383. mycode/server/static/assets/vue-html-AaS7Mt5G.js +1 -0
  384. mycode/server/static/assets/vue-vine-BoDAl6tE.js +1 -0
  385. mycode/server/static/assets/vyper-CDx5xZoG.js +1 -0
  386. mycode/server/static/assets/wasm-CG6Dc4jp.js +1 -0
  387. mycode/server/static/assets/wasm-MzD3tlZU.js +1 -0
  388. mycode/server/static/assets/wenyan-BV7otONQ.js +1 -0
  389. mycode/server/static/assets/wgsl-Dx-B1_4e.js +1 -0
  390. mycode/server/static/assets/wikitext-BhOHFoWU.js +1 -0
  391. mycode/server/static/assets/wit-5i3qLPDT.js +1 -0
  392. mycode/server/static/assets/wolfram-lXgVvXCa.js +1 -0
  393. mycode/server/static/assets/xml-sdJ4AIDG.js +1 -0
  394. mycode/server/static/assets/xsl-CtQFsRM5.js +1 -0
  395. mycode/server/static/assets/yaml-Buea-lGh.js +1 -0
  396. mycode/server/static/assets/zenscript-DVFEvuxE.js +1 -0
  397. mycode/server/static/assets/zig-VOosw3JB.js +1 -0
  398. mycode/server/static/favicon_slashes.svg +12 -0
  399. mycode/server/static/index.html +35 -0
  400. mycode_cli-0.1.0.dist-info/METADATA +186 -0
  401. mycode_cli-0.1.0.dist-info/RECORD +404 -0
  402. mycode_cli-0.1.0.dist-info/WHEEL +4 -0
  403. mycode_cli-0.1.0.dist-info/entry_points.txt +2 -0
  404. mycode_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/core/tools.py ADDED
@@ -0,0 +1,898 @@
1
+ """Core tool definitions and execution.
2
+
3
+ The runtime intentionally exposes only four built-in tools: ``read``,
4
+ ``write``, ``edit``, and ``bash``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import queue
12
+ import shlex
13
+ import signal
14
+ import subprocess
15
+ import threading
16
+ import time
17
+ from base64 import b64encode
18
+ from collections import deque
19
+ from collections.abc import Callable, Sequence
20
+ from dataclasses import dataclass
21
+ from difflib import SequenceMatcher
22
+ from mimetypes import guess_type
23
+ from pathlib import Path
24
+ from typing import Any, TextIO, cast
25
+
26
+ from mycode.core.messages import image_block, text_block
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Limits (keep token usage low)
30
+ # ---------------------------------------------------------------------------
31
+
32
+ DEFAULT_MAX_LINES = 2000
33
+ DEFAULT_MAX_BYTES = 50 * 1024
34
+ READ_MAX_LINE_CHARS = 2000
35
+
36
+ BASH_TIMEOUT_SECONDS = 120
37
+ _BASH_MAX_IN_MEMORY_BYTES = 5_000_000
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ToolSpec:
42
+ """Built-in tool metadata and executor binding."""
43
+
44
+ name: str
45
+ description: str
46
+ input_schema: dict[str, Any]
47
+ method_name: str
48
+ streams_output: bool = False
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ToolExecutionResult:
53
+ """Structured tool result used by the runtime.
54
+
55
+ `model_text` is appended to session history for future provider replay.
56
+ `display_text` is shown to the user.
57
+ """
58
+
59
+ model_text: str
60
+ display_text: str
61
+ is_error: bool = False
62
+ content: list[dict[str, Any]] | None = None
63
+
64
+
65
+ DEFAULT_TOOL_SPECS: tuple[ToolSpec, ...] = (
66
+ ToolSpec(
67
+ name="read",
68
+ description=(
69
+ "Read a UTF-8 text file or supported image file. Returns up to 2000 lines for text files. "
70
+ "Use offset/limit for large files. Very long lines are shortened."
71
+ ),
72
+ input_schema={
73
+ "type": "object",
74
+ "properties": {
75
+ "path": {"type": "string", "description": "File path (relative or absolute)."},
76
+ "offset": {"type": "integer", "description": "Line number to start from (1-indexed)."},
77
+ "limit": {"type": "integer", "description": "Maximum number of lines to return."},
78
+ },
79
+ "required": ["path"],
80
+ "additionalProperties": False,
81
+ },
82
+ method_name="read",
83
+ ),
84
+ ToolSpec(
85
+ name="write",
86
+ description="Write a file (create or overwrite).",
87
+ input_schema={
88
+ "type": "object",
89
+ "properties": {
90
+ "path": {"type": "string", "description": "File path (relative or absolute)."},
91
+ "content": {"type": "string", "description": "File content."},
92
+ },
93
+ "required": ["path", "content"],
94
+ "additionalProperties": False,
95
+ },
96
+ method_name="write",
97
+ ),
98
+ ToolSpec(
99
+ name="edit",
100
+ description="Edit a file by replacing an exact oldText snippet with newText. oldText must match exactly.",
101
+ input_schema={
102
+ "type": "object",
103
+ "properties": {
104
+ "path": {"type": "string", "description": "File path (relative or absolute)."},
105
+ "oldText": {"type": "string", "description": "Exact text to replace (must match exactly)."},
106
+ "newText": {"type": "string", "description": "Replacement text."},
107
+ },
108
+ "required": ["path", "oldText", "newText"],
109
+ "additionalProperties": False,
110
+ },
111
+ method_name="edit",
112
+ ),
113
+ ToolSpec(
114
+ name="bash",
115
+ description=(
116
+ "Run a shell command in the session working directory. "
117
+ "Large output returns the tail and saves the full log to a file."
118
+ ),
119
+ input_schema={
120
+ "type": "object",
121
+ "properties": {
122
+ "command": {"type": "string", "description": "Shell command."},
123
+ "timeout": {"type": "integer", "description": "Timeout in seconds (optional)."},
124
+ },
125
+ "required": ["command"],
126
+ "additionalProperties": False,
127
+ },
128
+ method_name="bash",
129
+ streams_output=True,
130
+ ),
131
+ )
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Utilities
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class Truncation:
141
+ truncated: bool
142
+ truncated_by: str | None
143
+ output_lines: int
144
+ output_bytes: int
145
+
146
+
147
+ def truncate_text(
148
+ text: str,
149
+ *,
150
+ max_lines: int = DEFAULT_MAX_LINES,
151
+ max_bytes: int = DEFAULT_MAX_BYTES,
152
+ tail: bool = False,
153
+ ) -> tuple[str, Truncation]:
154
+ """Truncate text by both line and byte limits.
155
+
156
+ Returns (content, truncation).
157
+ """
158
+
159
+ lines = text.splitlines()
160
+ out_lines: list[str] = []
161
+ out_bytes = 0
162
+
163
+ source = reversed(lines) if tail else lines
164
+
165
+ for line in source:
166
+ if len(out_lines) >= max_lines:
167
+ break
168
+ # +1 for newline when joined later
169
+ b = len((line + "\n").encode("utf-8"))
170
+ if out_bytes + b > max_bytes:
171
+ break
172
+ out_lines.append(line)
173
+ out_bytes += b
174
+
175
+ if tail:
176
+ out_lines.reverse()
177
+
178
+ content = "\n".join(out_lines)
179
+ truncated = len(out_lines) < len(lines) or out_bytes < len(text.encode("utf-8"))
180
+
181
+ truncated_by: str | None = None
182
+ if truncated:
183
+ if len(out_lines) < len(lines):
184
+ truncated_by = "lines" if len(out_lines) == max_lines else "bytes"
185
+ else:
186
+ truncated_by = "bytes"
187
+
188
+ trunc = Truncation(
189
+ truncated=truncated,
190
+ truncated_by=truncated_by,
191
+ output_lines=len(out_lines),
192
+ output_bytes=len(content.encode("utf-8")),
193
+ )
194
+ return content, trunc
195
+
196
+
197
+ def parse_tool_arguments(raw: str | None) -> dict[str, Any] | str:
198
+ """Parse a JSON tool-arguments payload.
199
+
200
+ Returns either the parsed object or an error string. Keeping this helper here
201
+ makes tool-argument validation consistent across adapters and tests.
202
+ """
203
+
204
+ if raw is None:
205
+ return {}
206
+
207
+ payload = raw.strip()
208
+ if not payload:
209
+ return {}
210
+
211
+ try:
212
+ parsed = json.loads(payload)
213
+ except json.JSONDecodeError:
214
+ return "error: invalid JSON arguments"
215
+
216
+ if not isinstance(parsed, dict):
217
+ return "error: tool arguments must decode to a JSON object"
218
+
219
+ return parsed
220
+
221
+
222
+ def resolve_path(path: str, *, cwd: str) -> str:
223
+ """Resolve path relative to cwd (without changing global process cwd)."""
224
+
225
+ p = Path(path).expanduser()
226
+ if not p.is_absolute():
227
+ p = Path(cwd) / p
228
+ return str(p.resolve(strict=False))
229
+
230
+
231
+ def _atomic_write_text(path: Path, content: str) -> None:
232
+ tmp = path.with_suffix(path.suffix + ".tmp")
233
+ tmp.write_text(content, encoding="utf-8")
234
+ tmp.replace(path)
235
+
236
+
237
+ def detect_image_mime_type(path: Path) -> str | None:
238
+ try:
239
+ with path.open("rb") as file:
240
+ header = file.read(16)
241
+ except OSError:
242
+ return None
243
+
244
+ if header.startswith(b"\x89PNG\r\n\x1a\n"):
245
+ return "image/png"
246
+ if header.startswith(b"\xff\xd8\xff"):
247
+ return "image/jpeg"
248
+ if header.startswith((b"GIF87a", b"GIF89a")):
249
+ return "image/gif"
250
+ if header.startswith(b"RIFF") and header[8:12] == b"WEBP":
251
+ return "image/webp"
252
+ guessed, _ = guess_type(path.name)
253
+ if guessed in {"image/png", "image/jpeg", "image/gif", "image/webp"}:
254
+ return guessed
255
+ return None
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Execution
260
+ # ---------------------------------------------------------------------------
261
+
262
+
263
+ # Track active subprocesses for cancellation.
264
+ _ACTIVE_PROCS: set[subprocess.Popen] = set()
265
+ _ACTIVE_PROCS_LOCK = threading.Lock()
266
+
267
+
268
+ def _kill_proc_tree(proc: subprocess.Popen[Any]) -> None:
269
+ try:
270
+ if os.name == "posix":
271
+ os.killpg(proc.pid, signal.SIGKILL)
272
+ else:
273
+ proc.kill()
274
+ except Exception:
275
+ try:
276
+ proc.kill()
277
+ except Exception:
278
+ pass
279
+
280
+
281
+ def cancel_all_tools() -> None:
282
+ """Terminate all running bash subprocesses."""
283
+
284
+ with _ACTIVE_PROCS_LOCK:
285
+ procs = list(_ACTIVE_PROCS)
286
+ _ACTIVE_PROCS.clear()
287
+
288
+ for proc in procs:
289
+ _kill_proc_tree(proc)
290
+
291
+
292
+ ToolOutputCallback = Callable[[str], None]
293
+
294
+
295
+ class ToolExecutor:
296
+ """Execute tool calls for a single session."""
297
+
298
+ def __init__(
299
+ self,
300
+ *,
301
+ cwd: str,
302
+ session_dir: Path,
303
+ tools: Sequence[ToolSpec] | None = None,
304
+ supports_image_input: bool = False,
305
+ ):
306
+ self.cwd = str(Path(cwd).resolve(strict=False))
307
+ self.session_dir = session_dir
308
+ self.supports_image_input = supports_image_input
309
+ self.session_dir.mkdir(parents=True, exist_ok=True)
310
+ self.tool_output_dir = self.session_dir / "tool-output"
311
+ self.tool_output_dir.mkdir(parents=True, exist_ok=True)
312
+ self._active_procs: set[subprocess.Popen[str]] = set()
313
+ self._active_procs_lock = threading.Lock()
314
+ self.tool_specs = tuple(tools or DEFAULT_TOOL_SPECS)
315
+ self._tools_by_name: dict[str, ToolSpec] = {}
316
+
317
+ for spec in self.tool_specs:
318
+ if spec.name in self._tools_by_name:
319
+ raise ValueError(f"duplicate tool name: {spec.name}")
320
+ if not callable(getattr(self, spec.method_name, None)):
321
+ raise ValueError(f"missing tool method: {spec.method_name}")
322
+ self._tools_by_name[spec.name] = spec
323
+
324
+ @property
325
+ def definitions(self) -> list[dict[str, Any]]:
326
+ """Return provider-facing tool definitions for the configured tools."""
327
+
328
+ return [
329
+ {
330
+ "name": spec.name,
331
+ "description": spec.description,
332
+ "input_schema": spec.input_schema,
333
+ }
334
+ for spec in self.tool_specs
335
+ ]
336
+
337
+ def get_tool(self, name: str) -> ToolSpec | None:
338
+ """Return the configured tool spec for a tool name."""
339
+
340
+ return self._tools_by_name.get(name)
341
+
342
+ def run(self, name: str, *, args: dict[str, Any]) -> ToolExecutionResult:
343
+ """Execute a non-streaming tool call."""
344
+
345
+ spec = self.get_tool(name)
346
+ if spec is None:
347
+ return ToolExecutionResult(
348
+ model_text=f"error: unknown tool: {name}",
349
+ display_text=f"Unknown tool: {name}",
350
+ is_error=True,
351
+ )
352
+ if spec.streams_output:
353
+ raise ValueError(f"tool requires streaming execution: {name}")
354
+
355
+ handler = cast(Callable[..., ToolExecutionResult], getattr(self, spec.method_name))
356
+ return handler(**args)
357
+
358
+ def run_streaming(
359
+ self,
360
+ name: str,
361
+ *,
362
+ tool_call_id: str,
363
+ args: dict[str, Any],
364
+ on_output: ToolOutputCallback,
365
+ ) -> ToolExecutionResult:
366
+ """Execute a tool call that emits incremental output."""
367
+
368
+ spec = self.get_tool(name)
369
+ if spec is None:
370
+ return ToolExecutionResult(
371
+ model_text=f"error: unknown tool: {name}",
372
+ display_text=f"Unknown tool: {name}",
373
+ is_error=True,
374
+ )
375
+ if not spec.streams_output:
376
+ raise ValueError(f"tool does not support streaming execution: {name}")
377
+
378
+ handler = cast(Callable[..., ToolExecutionResult], getattr(self, spec.method_name))
379
+ return handler(tool_call_id=tool_call_id, on_output=on_output, **args)
380
+
381
+ def _track_proc(self, proc: subprocess.Popen[str]) -> None:
382
+ with self._active_procs_lock:
383
+ self._active_procs.add(proc)
384
+ with _ACTIVE_PROCS_LOCK:
385
+ _ACTIVE_PROCS.add(proc)
386
+
387
+ def _untrack_proc(self, proc: subprocess.Popen[str]) -> None:
388
+ with self._active_procs_lock:
389
+ self._active_procs.discard(proc)
390
+ with _ACTIVE_PROCS_LOCK:
391
+ _ACTIVE_PROCS.discard(proc)
392
+
393
+ def cancel_active(self) -> None:
394
+ """Terminate only bash subprocesses started by this executor."""
395
+
396
+ with self._active_procs_lock:
397
+ procs = list(self._active_procs)
398
+ self._active_procs.clear()
399
+
400
+ for proc in procs:
401
+ with _ACTIVE_PROCS_LOCK:
402
+ _ACTIVE_PROCS.discard(proc)
403
+ _kill_proc_tree(proc)
404
+
405
+ # ---- read -----------------------------------------------------------------
406
+
407
+ def read(self, *, path: str, offset: int | None = None, limit: int | None = None) -> ToolExecutionResult:
408
+ """Read a text file or supported image file.
409
+
410
+ offset is 1-indexed. limit is number of lines.
411
+ """
412
+
413
+ file_path = Path(resolve_path(path, cwd=self.cwd))
414
+ if not file_path.exists():
415
+ return ToolExecutionResult(
416
+ model_text=f"error: file not found: {path}",
417
+ display_text=f"File not found: {path}",
418
+ is_error=True,
419
+ )
420
+ if not file_path.is_file():
421
+ return ToolExecutionResult(
422
+ model_text=f"error: not a file: {path}",
423
+ display_text=f"Not a file: {path}",
424
+ is_error=True,
425
+ )
426
+
427
+ image_mime_type = detect_image_mime_type(file_path)
428
+ if image_mime_type:
429
+ if not self.supports_image_input:
430
+ return ToolExecutionResult(
431
+ model_text="error: image input is not supported by the current model",
432
+ display_text="Current model does not support image input",
433
+ is_error=True,
434
+ )
435
+ summary = f"Read image file [{image_mime_type}]"
436
+ image_data = b64encode(file_path.read_bytes()).decode("utf-8")
437
+ return ToolExecutionResult(
438
+ model_text=summary,
439
+ display_text=summary,
440
+ content=[
441
+ text_block(summary),
442
+ image_block(image_data, mime_type=image_mime_type, name=file_path.name),
443
+ ],
444
+ )
445
+
446
+ start_line = offset if offset and offset > 0 else 1
447
+ line_limit = limit if limit and limit > 0 else DEFAULT_MAX_LINES
448
+ lines: list[str] = []
449
+ total_lines = 0
450
+ next_offset: int | None = None
451
+ first_shortened_line: int | None = None
452
+ shortened_lines = 0
453
+
454
+ try:
455
+ with file_path.open("r", encoding="utf-8") as f:
456
+ for total_lines, raw_line in enumerate(f, start=1):
457
+ if total_lines < start_line:
458
+ continue
459
+ if len(lines) >= line_limit:
460
+ next_offset = total_lines
461
+ break
462
+
463
+ # Keep reads predictable: page by lines and only shorten pathological lines.
464
+ line = raw_line.rstrip("\r\n")
465
+ if len(line) > READ_MAX_LINE_CHARS:
466
+ if first_shortened_line is None:
467
+ first_shortened_line = total_lines
468
+ shortened_lines += 1
469
+ line = line[:READ_MAX_LINE_CHARS] + " ... [line truncated]"
470
+ lines.append(line)
471
+ except UnicodeDecodeError:
472
+ return ToolExecutionResult(
473
+ model_text=f"error: file is not valid utf-8 text: {path}",
474
+ display_text=f"File is not valid UTF-8 text: {path}",
475
+ is_error=True,
476
+ )
477
+ except Exception as exc:
478
+ return ToolExecutionResult(
479
+ model_text=f"error: failed to read file: {exc}",
480
+ display_text=f"Failed to read file: {path}",
481
+ is_error=True,
482
+ )
483
+
484
+ if total_lines < start_line and not (total_lines == 0 and start_line == 1):
485
+ return ToolExecutionResult(
486
+ model_text=f"error: offset {offset} beyond end of file ({total_lines} lines)",
487
+ display_text=f"Offset {offset} beyond end of file: {path}",
488
+ is_error=True,
489
+ )
490
+
491
+ parts: list[str] = []
492
+ content = "\n".join(lines)
493
+ if content:
494
+ parts.append(content)
495
+
496
+ if next_offset is not None:
497
+ parts.append(f"[Showing lines {start_line}-{next_offset - 1}. Use offset={next_offset} to continue.]")
498
+
499
+ if first_shortened_line is not None:
500
+ quoted = shlex.quote(str(file_path))
501
+ prefix = f"[Line {first_shortened_line} was shortened to {READ_MAX_LINE_CHARS} chars."
502
+ if shortened_lines > 1:
503
+ prefix = (
504
+ f"[{shortened_lines} lines were shortened to {READ_MAX_LINE_CHARS} chars. "
505
+ f"First shortened line: {first_shortened_line}."
506
+ )
507
+ parts.append(
508
+ f"{prefix}\n"
509
+ "Use bash to inspect it in bytes:\n"
510
+ f"sed -n '{first_shortened_line}p' {quoted} | head -c 2000\n"
511
+ f"sed -n '{first_shortened_line}p' {quoted} | tail -c +2001 | head -c 2000]"
512
+ )
513
+
514
+ content = "\n\n".join(parts) if parts else ""
515
+ return ToolExecutionResult(model_text=content, display_text=content)
516
+
517
+ # ---- write ----------------------------------------------------------------
518
+
519
+ def write(self, *, path: str, content: str) -> ToolExecutionResult:
520
+ file_path = Path(resolve_path(path, cwd=self.cwd))
521
+ try:
522
+ file_path.parent.mkdir(parents=True, exist_ok=True)
523
+ _atomic_write_text(file_path, content)
524
+ except Exception as exc:
525
+ return ToolExecutionResult(
526
+ model_text=f"error: failed to write file: {exc}",
527
+ display_text=f"Failed to write file: {path}",
528
+ is_error=True,
529
+ )
530
+ return ToolExecutionResult(model_text="ok", display_text=f"Wrote {path}")
531
+
532
+ # ---- edit -----------------------------------------------------------------
533
+
534
+ def edit(self, *, path: str, oldText: str, newText: str) -> ToolExecutionResult: # noqa: N803 (pi-compatible)
535
+ """Replace one exact snippet, with a narrow fuzzy fallback.
536
+
537
+ The fallback only tolerates line-ending and trailing-whitespace changes.
538
+ It still requires a unique match so the edit stays deterministic.
539
+ """
540
+
541
+ file_path = Path(resolve_path(path, cwd=self.cwd))
542
+ if not file_path.exists():
543
+ return ToolExecutionResult(
544
+ model_text=f"error: file not found: {path}",
545
+ display_text=f"File not found: {path}",
546
+ is_error=True,
547
+ )
548
+ if not file_path.is_file():
549
+ return ToolExecutionResult(
550
+ model_text=f"error: not a file: {path}",
551
+ display_text=f"Not a file: {path}",
552
+ is_error=True,
553
+ )
554
+
555
+ try:
556
+ text = file_path.read_text(encoding="utf-8")
557
+ except Exception as exc:
558
+ return ToolExecutionResult(
559
+ model_text=f"error: failed to read file: {exc}",
560
+ display_text=f"Failed to read file: {path}",
561
+ is_error=True,
562
+ )
563
+
564
+ # Exact match first (deterministic and preferred)
565
+ exact_count = text.count(oldText)
566
+ match_pos: int | None = None
567
+ if exact_count == 1:
568
+ match_pos = text.index(oldText)
569
+ updated = text.replace(oldText, newText, 1)
570
+ elif exact_count > 1:
571
+ return ToolExecutionResult(
572
+ model_text=f"error: oldText occurs {exact_count} times; provide a more specific oldText",
573
+ display_text="Edit target is ambiguous; provide a more specific oldText",
574
+ is_error=True,
575
+ )
576
+ else:
577
+ # Conservative fuzzy fallback:
578
+ # tolerate line-ending and trailing-whitespace differences only.
579
+ fuzzy_span, fuzzy_count = _find_fuzzy_edit_span(text, oldText)
580
+ if fuzzy_span is None:
581
+ if fuzzy_count > 1:
582
+ return ToolExecutionResult(
583
+ model_text=(
584
+ f"error: oldText occurs {fuzzy_count} times after normalization; "
585
+ "provide a more specific oldText"
586
+ ),
587
+ display_text="Edit target is ambiguous after normalization",
588
+ is_error=True,
589
+ )
590
+ hint = _closest_line_hint(text, oldText)
591
+ if hint:
592
+ return ToolExecutionResult(
593
+ model_text=f"error: oldText not found. closest line: {hint}",
594
+ display_text="Edit target not found",
595
+ is_error=True,
596
+ )
597
+ return ToolExecutionResult(
598
+ model_text="error: oldText not found",
599
+ display_text="Edit target not found",
600
+ is_error=True,
601
+ )
602
+
603
+ start, end = fuzzy_span
604
+ match_pos = start
605
+ updated = text[:start] + newText + text[end:]
606
+
607
+ try:
608
+ _atomic_write_text(file_path, updated)
609
+ except Exception as exc:
610
+ return ToolExecutionResult(
611
+ model_text=f"error: failed to write file: {exc}",
612
+ display_text=f"Failed to write file: {path}",
613
+ is_error=True,
614
+ )
615
+
616
+ # Return a compact JSON payload so the frontend can render a focused diff
617
+ # around the edited range without re-reading the whole file.
618
+ if match_pos is None:
619
+ match_pos = text.index(oldText)
620
+
621
+ start_line = text[:match_pos].count("\n") + 1
622
+ old_line_count = len(oldText.splitlines()) or 1
623
+ new_line_count = len(newText.splitlines()) or 1
624
+ context_lines = 3
625
+ lines = updated.splitlines()
626
+ edit_start = start_line - 1
627
+ before = lines[max(0, edit_start - context_lines) : edit_start]
628
+ after = lines[edit_start + new_line_count : edit_start + new_line_count + context_lines]
629
+
630
+ return ToolExecutionResult(
631
+ model_text=json.dumps(
632
+ {
633
+ "status": "ok",
634
+ "start_line": start_line,
635
+ "old_line_count": old_line_count,
636
+ "new_line_count": new_line_count,
637
+ "context_before": before,
638
+ "context_after": after,
639
+ }
640
+ ),
641
+ display_text=f"Updated {path}",
642
+ )
643
+
644
+ # ---- bash -----------------------------------------------------------------
645
+
646
+ def bash(
647
+ self,
648
+ *,
649
+ tool_call_id: str,
650
+ command: str,
651
+ timeout: int | None = None,
652
+ on_output: ToolOutputCallback | None = None,
653
+ ) -> ToolExecutionResult:
654
+ """Run a shell command and return combined stdout/stderr text.
655
+
656
+ Output is streamed line-by-line through ``on_output`` when provided. If
657
+ the output grows too large for memory or needs truncation, the full log
658
+ is written under the session's ``tool-output/`` directory.
659
+ """
660
+
661
+ timeout_seconds = int(timeout or BASH_TIMEOUT_SECONDS)
662
+ if timeout_seconds <= 0:
663
+ timeout_seconds = BASH_TIMEOUT_SECONDS
664
+
665
+ proc: subprocess.Popen[str] | None = None
666
+ log_path = self.tool_output_dir / f"bash-{tool_call_id}.log"
667
+ kept_lines: list[str] = []
668
+ kept_bytes = 0
669
+ tail_lines: deque[str] = deque(maxlen=DEFAULT_MAX_LINES)
670
+ log_file: TextIO | None = None
671
+ saved_output_path: Path | None = None
672
+
673
+ try:
674
+ proc = subprocess.Popen(
675
+ command,
676
+ shell=True,
677
+ cwd=self.cwd,
678
+ stdout=subprocess.PIPE,
679
+ stderr=subprocess.STDOUT,
680
+ text=True,
681
+ bufsize=1,
682
+ universal_newlines=True,
683
+ start_new_session=os.name == "posix",
684
+ )
685
+ self._track_proc(proc)
686
+
687
+ stdout = cast(TextIO, proc.stdout)
688
+ output_queue: queue.Queue[str | None] = queue.Queue()
689
+ reader_errors: list[Exception] = []
690
+
691
+ def read_stdout() -> None:
692
+ try:
693
+ for line in stdout:
694
+ output_queue.put(line)
695
+ except Exception as exc: # pragma: no cover - defensive
696
+ reader_errors.append(exc)
697
+ finally:
698
+ output_queue.put(None)
699
+
700
+ reader = threading.Thread(target=read_stdout, daemon=True)
701
+ reader.start()
702
+ deadline = time.monotonic() + timeout_seconds
703
+
704
+ while True:
705
+ remaining = deadline - time.monotonic()
706
+ if remaining <= 0:
707
+ _kill_proc_tree(proc)
708
+ return ToolExecutionResult(
709
+ model_text=f"error: timeout after {timeout_seconds}s",
710
+ display_text=f"Command timed out after {timeout_seconds}s",
711
+ is_error=True,
712
+ )
713
+
714
+ try:
715
+ line = output_queue.get(timeout=min(0.1, remaining))
716
+ except queue.Empty:
717
+ continue
718
+
719
+ if line is None:
720
+ break
721
+
722
+ line = line.rstrip("\n")
723
+ kept_bytes += len((line + "\n").encode("utf-8"))
724
+
725
+ if log_file is None:
726
+ kept_lines.append(line)
727
+ if kept_bytes > _BASH_MAX_IN_MEMORY_BYTES:
728
+ log_file = log_path.open("w", encoding="utf-8")
729
+ saved_output_path = log_path
730
+ if kept_lines:
731
+ log_file.write("\n".join(kept_lines))
732
+ log_file.write("\n")
733
+ tail_lines.extend(kept_lines)
734
+ kept_lines = []
735
+ else:
736
+ tail_lines.append(line)
737
+ log_file.write(line)
738
+ log_file.write("\n")
739
+
740
+ if on_output:
741
+ on_output(line)
742
+
743
+ if reader_errors:
744
+ message = str(reader_errors[0])
745
+ return ToolExecutionResult(
746
+ model_text=f"error: {message}",
747
+ display_text=message,
748
+ is_error=True,
749
+ )
750
+
751
+ try:
752
+ remaining = max(0.1, deadline - time.monotonic())
753
+ proc.wait(timeout=remaining)
754
+ except subprocess.TimeoutExpired:
755
+ _kill_proc_tree(proc)
756
+ return ToolExecutionResult(
757
+ model_text=f"error: timeout after {timeout_seconds}s",
758
+ display_text=f"Command timed out after {timeout_seconds}s",
759
+ is_error=True,
760
+ )
761
+
762
+ raw_output = "\n".join(list(tail_lines) if log_file is not None else kept_lines)
763
+ output = raw_output.strip() or "(empty)"
764
+ content, trunc = truncate_text(output, tail=True)
765
+
766
+ if log_file is None and trunc.truncated:
767
+ try:
768
+ log_path.write_text(raw_output, encoding="utf-8")
769
+ saved_output_path = log_path
770
+ except Exception:
771
+ saved_output_path = None
772
+
773
+ if log_file is not None or trunc.truncated:
774
+ result = content
775
+ if log_file is not None:
776
+ result += "\n\n[Output truncated in memory.]"
777
+ else:
778
+ result += "\n\n[Output truncated.]"
779
+
780
+ if saved_output_path is not None:
781
+ result += f" Full output saved to: {saved_output_path}. Use read with offset/limit."
782
+ if not content:
783
+ quoted = shlex.quote(str(saved_output_path))
784
+ result += (
785
+ "\nUse bash to inspect bytes:\n"
786
+ f"head -c 2000 {quoted}\n"
787
+ f"tail -c +2001 {quoted} | head -c 2000"
788
+ )
789
+ return ToolExecutionResult(model_text=result, display_text=result)
790
+
791
+ return ToolExecutionResult(model_text=content, display_text=content)
792
+
793
+ except Exception as exc:
794
+ message = str(exc)
795
+ return ToolExecutionResult(
796
+ model_text=f"error: {message}",
797
+ display_text=message,
798
+ is_error=True,
799
+ )
800
+ finally:
801
+ if log_file is not None:
802
+ try:
803
+ log_file.close()
804
+ except Exception:
805
+ pass
806
+ if proc:
807
+ self._untrack_proc(proc)
808
+ if proc.poll() is None:
809
+ _kill_proc_tree(proc)
810
+
811
+
812
+ def _closest_line_hint(text: str, needle: str) -> str | None:
813
+ needle_clean = needle.strip()
814
+ if not needle_clean:
815
+ return None
816
+
817
+ best_ratio = 0.0
818
+ best_line = ""
819
+ for line in text.splitlines():
820
+ candidate = line.strip()
821
+ if not candidate:
822
+ continue
823
+ ratio = SequenceMatcher(None, needle_clean, candidate).ratio()
824
+ if ratio > best_ratio:
825
+ best_ratio = ratio
826
+ best_line = candidate
827
+
828
+ if best_ratio < 0.6 or not best_line:
829
+ return None
830
+
831
+ if len(best_line) > 120:
832
+ return best_line[:117] + "..."
833
+ return best_line
834
+
835
+
836
+ def _find_fuzzy_edit_span(text: str, old_text: str) -> tuple[tuple[int, int] | None, int]:
837
+ """Find unique match span with conservative normalization.
838
+
839
+ Normalization is intentionally limited to:
840
+ - line ending normalization (CRLF/CR -> LF)
841
+ - trailing space/tab removal per line
842
+ """
843
+
844
+ normalized_text, text_map = _normalize_for_fuzzy_edit(text)
845
+ normalized_old, _ = _normalize_for_fuzzy_edit(old_text)
846
+
847
+ first = normalized_text.find(normalized_old)
848
+ if first == -1:
849
+ return None, 0
850
+
851
+ count = normalized_text.count(normalized_old)
852
+ if count != 1:
853
+ return None, count
854
+
855
+ end_normalized = first + len(normalized_old)
856
+ start_original = text_map[first]
857
+ end_original = text_map[end_normalized] if end_normalized < len(text_map) else len(text)
858
+ return (start_original, end_original), 1
859
+
860
+
861
+ def _normalize_for_fuzzy_edit(text: str) -> tuple[str, list[int]]:
862
+ """Normalize text for conservative fuzzy edit matching.
863
+
864
+ Returns normalized text plus a map from normalized index -> original index.
865
+ """
866
+
867
+ chars: list[str] = []
868
+ index_map: list[int] = []
869
+
870
+ i = 0
871
+ n = len(text)
872
+ while i < n:
873
+ line_start = i
874
+ while i < n and text[i] not in ("\n", "\r"):
875
+ i += 1
876
+
877
+ line_end = i
878
+ trimmed_end = line_end
879
+ while trimmed_end > line_start and text[trimmed_end - 1] in (" ", "\t"):
880
+ trimmed_end -= 1
881
+
882
+ for pos in range(line_start, trimmed_end):
883
+ chars.append(text[pos])
884
+ index_map.append(pos)
885
+
886
+ if i >= n:
887
+ continue
888
+
889
+ # Normalize any line ending to LF and map it to the original EOL start index.
890
+ eol_start = i
891
+ if text[i] == "\r" and i + 1 < n and text[i + 1] == "\n":
892
+ i += 2
893
+ else:
894
+ i += 1
895
+ chars.append("\n")
896
+ index_map.append(eol_start)
897
+
898
+ return "".join(chars), index_map