letscode 0.5.0__tar.gz → 0.6.1__tar.gz

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 (264) hide show
  1. {letscode-0.5.0 → letscode-0.6.1}/PKG-INFO +5 -4
  2. {letscode-0.5.0 → letscode-0.6.1}/README.md +3 -1
  3. letscode-0.6.1/plugins/letscode-mcp/README.md +65 -0
  4. letscode-0.6.1/plugins/letscode-mcp/pyproject.toml +42 -0
  5. letscode-0.6.1/plugins/letscode-mcp/src/letscode_mcp/__init__.py +5 -0
  6. letscode-0.6.1/plugins/letscode-mcp/src/letscode_mcp/plugin.py +304 -0
  7. letscode-0.6.1/plugins/letscode-mcp/tests/test_plugin.py +258 -0
  8. letscode-0.6.1/plugins/letscode-mcp/uv.lock +1036 -0
  9. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/uv.lock +43 -42
  10. {letscode-0.5.0 → letscode-0.6.1}/pyproject.toml +8 -9
  11. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/loop.py +3 -8
  12. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/tools.py +3 -40
  13. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/cli/rpc.py +6 -4
  14. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/registries.py +54 -83
  15. letscode-0.5.0/.builds/alpine.yml +0 -25
  16. letscode-0.5.0/.builds/ubuntu-2404.yml +0 -38
  17. letscode-0.5.0/.cache/.gitignore +0 -1
  18. letscode-0.5.0/.cache/10318616000990552681 +0 -4
  19. letscode-0.5.0/.cache/10493891611479424259 +0 -4
  20. letscode-0.5.0/.cache/10702343394967673498 +0 -4
  21. letscode-0.5.0/.cache/11010100411556762066 +0 -4
  22. letscode-0.5.0/.cache/11154141664771110464 +0 -4
  23. letscode-0.5.0/.cache/11168941369259313220 +0 -80
  24. letscode-0.5.0/.cache/11992942182152933888 +0 -144
  25. letscode-0.5.0/.cache/12059922036673638020 +0 -4
  26. letscode-0.5.0/.cache/1249039435107563575 +0 -161
  27. letscode-0.5.0/.cache/12576105843982642077 +0 -4
  28. letscode-0.5.0/.cache/14203930344178090297 +0 -4
  29. letscode-0.5.0/.cache/1563633362711557789 +0 -4
  30. letscode-0.5.0/.cache/15661711846603463071 +0 -207
  31. letscode-0.5.0/.cache/15908712471909210796 +0 -178
  32. letscode-0.5.0/.cache/16912669318574488615 +0 -80
  33. letscode-0.5.0/.cache/16988395898165525092 +0 -419
  34. letscode-0.5.0/.cache/17150484726184191142 +0 -128
  35. letscode-0.5.0/.cache/17419555459594506489 +0 -4
  36. letscode-0.5.0/.cache/2041198230313398573 +0 -4
  37. letscode-0.5.0/.cache/3072703261793992529 +0 -4
  38. letscode-0.5.0/.cache/3151054395951844290 +0 -4
  39. letscode-0.5.0/.cache/3226154870166803660 +0 -4
  40. letscode-0.5.0/.cache/3476900567878811119 +0 -4
  41. letscode-0.5.0/.cache/3510264687931475889 +0 -160
  42. letscode-0.5.0/.cache/3520474981437140632 +0 -112
  43. letscode-0.5.0/.cache/3541202890535126406 +0 -96
  44. letscode-0.5.0/.cache/3728666290973476153 +0 -80
  45. letscode-0.5.0/.cache/4780842327402266928 +0 -64
  46. letscode-0.5.0/.cache/5187878047780114009 +0 -161
  47. letscode-0.5.0/.cache/6069231736669146243 +0 -4
  48. letscode-0.5.0/.cache/6463951297718265957 +0 -31
  49. letscode-0.5.0/.cache/6479415784420637704 +0 -4
  50. letscode-0.5.0/.cache/6764327636681606151 +0 -4
  51. letscode-0.5.0/.cache/6789241458458765738 +0 -354
  52. letscode-0.5.0/.cache/8444450551902475297 +0 -113
  53. letscode-0.5.0/.cache/8863633364771264685 +0 -112
  54. letscode-0.5.0/.cache/9378231046984033364 +0 -177
  55. letscode-0.5.0/.cache/9810942886417102030 +0 -4
  56. letscode-0.5.0/.github/workflows/ci.yml +0 -36
  57. letscode-0.5.0/.github/workflows/docs.yml +0 -29
  58. letscode-0.5.0/.pre-commit-config.yaml +0 -33
  59. letscode-0.5.0/CHANGES.md +0 -123
  60. letscode-0.5.0/CLAUDE.md +0 -85
  61. letscode-0.5.0/Makefile +0 -50
  62. letscode-0.5.0/docs/about/changelog.md +0 -39
  63. letscode-0.5.0/docs/about/index.md +0 -54
  64. letscode-0.5.0/docs/about/lessons-learned.md +0 -56
  65. letscode-0.5.0/docs/about/related-projects.md +0 -69
  66. letscode-0.5.0/docs/about/roadmap.md +0 -98
  67. letscode-0.5.0/docs/architecture/agent-loop.md +0 -97
  68. letscode-0.5.0/docs/architecture/extension-model.md +0 -194
  69. letscode-0.5.0/docs/architecture/index.md +0 -69
  70. letscode-0.5.0/docs/getting-started.md +0 -136
  71. letscode-0.5.0/docs/index.md +0 -113
  72. letscode-0.5.0/docs/plugins/authoring.md +0 -542
  73. letscode-0.5.0/docs/plugins/index.md +0 -85
  74. letscode-0.5.0/docs/plugins/memory.md +0 -79
  75. letscode-0.5.0/docs/user-guide/configuration.md +0 -108
  76. letscode-0.5.0/docs/user-guide/index.md +0 -36
  77. letscode-0.5.0/docs/user-guide/sessions.md +0 -55
  78. letscode-0.5.0/docs/user-guide/skills.md +0 -130
  79. letscode-0.5.0/docs/user-guide/slash-commands.md +0 -87
  80. letscode-0.5.0/local-notes/playbooks/litestar-dishka/CHECKLISTS.md +0 -131
  81. letscode-0.5.0/local-notes/playbooks/litestar-dishka/COMMON-GOTCHAS.md +0 -296
  82. letscode-0.5.0/local-notes/playbooks/litestar-dishka/INDEX.md +0 -90
  83. letscode-0.5.0/local-notes/playbooks/litestar-dishka/advanced-alchemy.md +0 -1558
  84. letscode-0.5.0/local-notes/playbooks/litestar-dishka/configuration.md +0 -858
  85. letscode-0.5.0/local-notes/playbooks/litestar-dishka/dishka.md +0 -244
  86. letscode-0.5.0/local-notes/playbooks/litestar-dishka/integration.md +0 -135
  87. letscode-0.5.0/local-notes/playbooks/litestar-dishka/litestar-views.md +0 -322
  88. letscode-0.5.0/local-notes/playbooks/litestar-dishka/litestar.md +0 -408
  89. letscode-0.5.0/local-notes/playbooks/litestar-dishka/stack-guide.md +0 -1060
  90. letscode-0.5.0/local-notes/playbooks/litestar-dishka/testing.md +0 -1308
  91. letscode-0.5.0/local-notes/playbooks/litestar-dishka/workflows.md +0 -707
  92. letscode-0.5.0/local-notes/playbooks/python/4-rules-design.md +0 -159
  93. letscode-0.5.0/local-notes/playbooks/python/CHECKLISTS.md +0 -171
  94. letscode-0.5.0/local-notes/playbooks/python/INDEX.md +0 -72
  95. letscode-0.5.0/local-notes/playbooks/python/coding-guidelines.md +0 -301
  96. letscode-0.5.0/local-notes/playbooks/python/design-patterns.md +0 -101
  97. letscode-0.5.0/local-notes/playbooks/python/error-messages.md +0 -197
  98. letscode-0.5.0/local-notes/playbooks/python/monorepo.md +0 -513
  99. letscode-0.5.0/local-notes/playbooks/python/nouns-and-verbs.md +0 -125
  100. letscode-0.5.0/local-notes/playbooks/python/testing.md +0 -434
  101. letscode-0.5.0/local-notes/playbooks/webdev/CHECKLISTS.md +0 -175
  102. letscode-0.5.0/local-notes/playbooks/webdev/INDEX.md +0 -74
  103. letscode-0.5.0/local-notes/playbooks/webdev/api-design.md +0 -405
  104. letscode-0.5.0/local-notes/playbooks/webdev/deployment.md +0 -499
  105. letscode-0.5.0/local-notes/playbooks/webdev/hybrid-ui.md +0 -194
  106. letscode-0.5.0/local-notes/playbooks/webdev/production-patterns.md +0 -577
  107. letscode-0.5.0/local-notes/playbooks/webdev/security.md +0 -480
  108. letscode-0.5.0/notes/01-vision.md +0 -323
  109. letscode-0.5.0/notes/02-design.md +0 -1143
  110. letscode-0.5.0/notes/03-plan.md +0 -383
  111. letscode-0.5.0/notes/04-lessons-learned.md +0 -274
  112. letscode-0.5.0/notes/05-gap-vs-pi.md +0 -284
  113. letscode-0.5.0/notes/06-v0.2-plan.md +0 -183
  114. letscode-0.5.0/notes/07-v0.3-plan.md +0 -204
  115. letscode-0.5.0/notes/08-extension-model.md +0 -228
  116. letscode-0.5.0/notes/09-ergonomics-punchlist.md +0 -96
  117. letscode-0.5.0/notes/10-v0.4-plan.md +0 -250
  118. letscode-0.5.0/notes/11-plugin-contract-v0.4-findings.md +0 -110
  119. letscode-0.5.0/notes/12-textual-frontend.md +0 -339
  120. letscode-0.5.0/notes/13-textual-test-drive.md +0 -105
  121. letscode-0.5.0/notes/14-vs-anthropic-playbook.md +0 -65
  122. letscode-0.5.0/notes/15-prompt-caching.md +0 -57
  123. letscode-0.5.0/notes/16-config-schema.md +0 -96
  124. letscode-0.5.0/notes/17-rpc-protocol.md +0 -135
  125. letscode-0.5.0/notes/18-v0.5-plan.md +0 -147
  126. letscode-0.5.0/notes/19-release-runbook.md +0 -23
  127. letscode-0.5.0/notes/plans/2026-W21.md +0 -46
  128. letscode-0.5.0/notes/plans/textual-tui.md +0 -69
  129. letscode-0.5.0/notes/playbooks/INDEX.md +0 -129
  130. letscode-0.5.0/notes/playbooks/generic-python/4-rules-design.md +0 -159
  131. letscode-0.5.0/notes/playbooks/generic-python/CHECKLISTS.md +0 -173
  132. letscode-0.5.0/notes/playbooks/generic-python/INDEX.md +0 -72
  133. letscode-0.5.0/notes/playbooks/generic-python/coding-guidelines.md +0 -301
  134. letscode-0.5.0/notes/playbooks/generic-python/design-patterns.md +0 -101
  135. letscode-0.5.0/notes/playbooks/generic-python/error-messages.md +0 -197
  136. letscode-0.5.0/notes/playbooks/generic-python/monorepo.md +0 -513
  137. letscode-0.5.0/notes/playbooks/generic-python/nouns-and-verbs.md +0 -125
  138. letscode-0.5.0/notes/playbooks/generic-python/testing.md +0 -434
  139. letscode-0.5.0/notes/playbooks/pluggy/CHECKLISTS.md +0 -126
  140. letscode-0.5.0/notes/playbooks/pluggy/COMMON-GOTCHAS.md +0 -155
  141. letscode-0.5.0/notes/playbooks/pluggy/INDEX.md +0 -88
  142. letscode-0.5.0/notes/playbooks/pluggy/plugin-system.md +0 -870
  143. letscode-0.5.0/notes/plugins-guide.md +0 -554
  144. letscode-0.5.0/notes/test-drive.md +0 -116
  145. letscode-0.5.0/notes/weekly/00readme.md +0 -104
  146. letscode-0.5.0/notes/weekly/2026-W20.md +0 -96
  147. letscode-0.5.0/noxfile.py +0 -27
  148. letscode-0.5.0/ruff.toml +0 -53
  149. letscode-0.5.0/scripts/verify_plugin.py +0 -93
  150. letscode-0.5.0/scripts/verify_wheel.py +0 -143
  151. letscode-0.5.0/tests/__init__.py +0 -0
  152. letscode-0.5.0/tests/_helpers.py +0 -170
  153. letscode-0.5.0/tests/a_unit/__init__.py +0 -0
  154. letscode-0.5.0/tests/a_unit/test_builtin_commands.py +0 -602
  155. letscode-0.5.0/tests/a_unit/test_builtin_plugin.py +0 -164
  156. letscode-0.5.0/tests/a_unit/test_cli.py +0 -845
  157. letscode-0.5.0/tests/a_unit/test_command_dispatch.py +0 -274
  158. letscode-0.5.0/tests/a_unit/test_compaction.py +0 -545
  159. letscode-0.5.0/tests/a_unit/test_config.py +0 -285
  160. letscode-0.5.0/tests/a_unit/test_context_files.py +0 -172
  161. letscode-0.5.0/tests/a_unit/test_execution_env.py +0 -199
  162. letscode-0.5.0/tests/a_unit/test_fake_llm.py +0 -84
  163. letscode-0.5.0/tests/a_unit/test_footer.py +0 -139
  164. letscode-0.5.0/tests/a_unit/test_hooks.py +0 -211
  165. letscode-0.5.0/tests/a_unit/test_hookspecs.py +0 -125
  166. letscode-0.5.0/tests/a_unit/test_llm_client.py +0 -574
  167. letscode-0.5.0/tests/a_unit/test_plugin_manager.py +0 -464
  168. letscode-0.5.0/tests/a_unit/test_session.py +0 -413
  169. letscode-0.5.0/tests/a_unit/test_skill_discovery.py +0 -154
  170. letscode-0.5.0/tests/a_unit/test_skill_loader.py +0 -192
  171. letscode-0.5.0/tests/a_unit/test_skill_synth.py +0 -246
  172. letscode-0.5.0/tests/a_unit/test_smoke.py +0 -30
  173. letscode-0.5.0/tests/a_unit/test_stream.py +0 -138
  174. letscode-0.5.0/tests/a_unit/test_tool_bash.py +0 -183
  175. letscode-0.5.0/tests/a_unit/test_tool_edit.py +0 -231
  176. letscode-0.5.0/tests/a_unit/test_tool_read.py +0 -164
  177. letscode-0.5.0/tests/a_unit/test_tool_write.py +0 -164
  178. letscode-0.5.0/tests/a_unit/test_tools.py +0 -287
  179. letscode-0.5.0/tests/a_unit/test_types.py +0 -235
  180. letscode-0.5.0/tests/b_integration/__init__.py +0 -0
  181. letscode-0.5.0/tests/b_integration/test_agent.py +0 -787
  182. letscode-0.5.0/tests/b_integration/test_agent_loop.py +0 -440
  183. letscode-0.5.0/tests/b_integration/test_agent_loop_tools.py +0 -473
  184. letscode-0.5.0/tests/b_integration/test_basic_frontend.py +0 -2361
  185. letscode-0.5.0/tests/b_integration/test_lifecycle_hooks.py +0 -857
  186. letscode-0.5.0/tests/b_integration/test_rpc_mode.py +0 -351
  187. letscode-0.5.0/tests/b_integration/test_rpc_subprocess.py +0 -219
  188. letscode-0.5.0/tests/c_e2e/__init__.py +0 -0
  189. letscode-0.5.0/tests/c_e2e/test_smoke.py +0 -246
  190. letscode-0.5.0/tests/conftest.py +0 -79
  191. letscode-0.5.0/tests/fixtures/config/sample.toml +0 -11
  192. letscode-0.5.0/tests/fixtures/llm/README.md +0 -47
  193. letscode-0.5.0/tests/fixtures/llm/hello-tool.jsonl +0 -5
  194. letscode-0.5.0/tests/fixtures/llm/hello.jsonl +0 -5
  195. letscode-0.5.0/uv.lock +0 -1326
  196. letscode-0.5.0/zensical.toml +0 -161
  197. {letscode-0.5.0 → letscode-0.6.1}/.gitignore +0 -0
  198. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/README.md +0 -0
  199. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/pyproject.toml +0 -0
  200. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/src/letscode_goal/__init__.py +0 -0
  201. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/src/letscode_goal/plugin.py +0 -0
  202. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/src/letscode_goal/state.py +0 -0
  203. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/tests/test_plugin.py +0 -0
  204. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/uv.lock +0 -0
  205. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/README.md +0 -0
  206. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/pyproject.toml +0 -0
  207. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/src/letscode_memory/__init__.py +0 -0
  208. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/src/letscode_memory/plugin.py +0 -0
  209. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/src/letscode_memory/store.py +0 -0
  210. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/tests/test_plugin.py +0 -0
  211. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/README.md +0 -0
  212. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/pyproject.toml +0 -0
  213. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/__init__.py +0 -0
  214. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/app.py +0 -0
  215. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/frontend.py +0 -0
  216. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/plugin.py +0 -0
  217. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/tests/test_plugin.py +0 -0
  218. {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/uv.lock +0 -0
  219. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/__main__.py +0 -0
  220. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/__init__.py +0 -0
  221. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/agent.py +0 -0
  222. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/compaction.py +0 -0
  223. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/events.py +0 -0
  224. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/execution_env.py +0 -0
  225. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/hooks.py +0 -0
  226. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/hookspecs.py +0 -0
  227. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/messages.py +0 -0
  228. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/state.py +0 -0
  229. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/cli/__init__.py +0 -0
  230. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/cli/app.py +0 -0
  231. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/commands/__init__.py +0 -0
  232. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/commands/builtin.py +0 -0
  233. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/commands/dispatch.py +0 -0
  234. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/config/__init__.py +0 -0
  235. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/config/loader.py +0 -0
  236. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/context/__init__.py +0 -0
  237. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/context/files.py +0 -0
  238. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/__init__.py +0 -0
  239. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/basic/__init__.py +0 -0
  240. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/basic/footer.py +0 -0
  241. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/basic/tui.py +0 -0
  242. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/protocol.py +0 -0
  243. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/__init__.py +0 -0
  244. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/client.py +0 -0
  245. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/errors.py +0 -0
  246. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/models.py +0 -0
  247. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/stream.py +0 -0
  248. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/types.py +0 -0
  249. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/__init__.py +0 -0
  250. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/builtin.py +0 -0
  251. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/manager.py +0 -0
  252. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/session/__init__.py +0 -0
  253. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/session/format.py +0 -0
  254. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/session/store.py +0 -0
  255. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/__init__.py +0 -0
  256. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/discovery.py +0 -0
  257. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/loader.py +0 -0
  258. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/synth.py +0 -0
  259. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/__init__.py +0 -0
  260. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/bash.py +0 -0
  261. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/builtin.py +0 -0
  262. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/edit.py +0 -0
  263. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/read.py +0 -0
  264. {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/write.py +0 -0
@@ -1,9 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: letscode
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: A minimal coding agent for the terminal.
5
- Project-URL: Homepage, https://git.sr.ht/~sfermigier/letscode
6
- Project-URL: Source Code, https://git.sr.ht/~sfermigier/letscode
7
5
  Author-email: Stefane Fermigier <sf@abilian.com>
8
6
  Requires-Python: >=3.12
9
7
  Requires-Dist: aiofiles>=24.0
@@ -13,6 +11,7 @@ Requires-Dist: openai>=1.0
13
11
  Requires-Dist: pluggy>=1.5
14
12
  Requires-Dist: prompt-toolkit>=3.0
15
13
  Requires-Dist: pydantic>=2.0
14
+ Requires-Dist: pyyaml>=6.0
16
15
  Requires-Dist: rich>=13.0
17
16
  Description-Content-Type: text/markdown
18
17
 
@@ -20,10 +19,12 @@ Description-Content-Type: text/markdown
20
19
 
21
20
  A minimal, OpenAI-compatible coding agent for the terminal — written in Python. Point it at any OpenAI-API-compatible endpoint (Ollama, Fireworks, OpenRouter, vLLM, llama.cpp's `llama-server`, …) and get a streaming agent loop with the four tools that cover 95% of coding sessions — `read`, `write`, `edit`, `bash` — plus skills, slash commands, and a plugin system you can extend.
22
21
 
23
- **Status:** v0.5, alpha.
22
+ **Status:** v0.6, alpha.
24
23
 
25
24
  The full docs live in [`docs/`](docs/) and build via [Zensical](https://zensical.org) — run `make docs-serve` for a live preview, or `make docs` for a static build.
26
25
 
26
+ Or go to <https://letscode.hop3.abilian.com> for the online doc.
27
+
27
28
  ## What it does
28
29
 
29
30
  - **Streaming agent loop** with parallel tool execution.
@@ -2,10 +2,12 @@
2
2
 
3
3
  A minimal, OpenAI-compatible coding agent for the terminal — written in Python. Point it at any OpenAI-API-compatible endpoint (Ollama, Fireworks, OpenRouter, vLLM, llama.cpp's `llama-server`, …) and get a streaming agent loop with the four tools that cover 95% of coding sessions — `read`, `write`, `edit`, `bash` — plus skills, slash commands, and a plugin system you can extend.
4
4
 
5
- **Status:** v0.5, alpha.
5
+ **Status:** v0.6, alpha.
6
6
 
7
7
  The full docs live in [`docs/`](docs/) and build via [Zensical](https://zensical.org) — run `make docs-serve` for a live preview, or `make docs` for a static build.
8
8
 
9
+ Or go to <https://letscode.hop3.abilian.com> for the online doc.
10
+
9
11
  ## What it does
10
12
 
11
13
  - **Streaming agent loop** with parallel tool execution.
@@ -0,0 +1,65 @@
1
+ # letscode-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io/) client plugin for [letscode](https://github.com/abilian/letscode). Configure an MCP server in TOML; restart `letscode`; the server's tools show up in `/tools` and the model can call them — same shape as any other letscode tool.
4
+
5
+ **Scope (v0.6):** stdio transport, tools only (resources and prompts deferred). Design: `notes/21-mcp-integration.md` in the letscode repo.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ uv tool install letscode-mcp # or: pipx install letscode-mcp
11
+ ```
12
+
13
+ Once installed alongside `letscode`, the plugin is auto-discovered via the `letscode` entry-point group — no extra wiring.
14
+
15
+ ## Configure
16
+
17
+ Edit `~/.letscode/config.toml` (or `.letscode/config.toml` in a project — project overrides user per key):
18
+
19
+ ```toml
20
+ [mcp.servers.filesystem]
21
+ command = "uvx"
22
+ args = ["mcp-server-filesystem", "/path/to/project"]
23
+
24
+ [mcp.servers.github]
25
+ command = "npx"
26
+ args = ["-y", "@modelcontextprotocol/server-github"]
27
+ env = { GITHUB_TOKEN = "$GITHUB_TOKEN" } # $VAR / ${VAR} expand from your env
28
+ prefix = "gh_" # optional; default = "mcp_<server>_"
29
+ timeout = 10 # seconds for initialize + list_tools
30
+ ```
31
+
32
+ Per-server keys:
33
+
34
+ | key | required | meaning |
35
+ |---|---|---|
36
+ | `command` | yes | executable to spawn (resolved on `PATH`) |
37
+ | `args` | no | command-line args (default `[]`) |
38
+ | `env` | no | extra env vars; values support `$VAR` / `${VAR}` expansion; unset → empty string |
39
+ | `prefix` | no | tool-name prefix override; default `mcp_<server>_` |
40
+ | `timeout` | no | seconds for the startup handshake (default `10`) |
41
+
42
+ ## Tool naming
43
+
44
+ Discovered tools are namespaced to avoid colliding with letscode built-ins (`read`, `write`, `edit`, `bash`) and with each other. A server keyed `filesystem` exposing `read_file` registers as `mcp_filesystem_read_file`. Use `prefix = "fs_"` to shorten if the token cost matters more than the safety margin.
45
+
46
+ ## Failure model
47
+
48
+ Every failure surfaces as a one-line stderr warning at registration or a `ToolResult(is_error=True)` at call time. Never a traceback into the agent loop.
49
+
50
+ | When | Failure | You see |
51
+ |---|---|---|
52
+ | register | `command` not on `PATH` | warning, server skipped, other servers continue |
53
+ | register | startup exceeds `timeout` | warning, server skipped |
54
+ | call | server crashed | `is_error=True` carrying the error message |
55
+ | call | server returns `isError=True` | `is_error=True` carrying the server's content |
56
+
57
+ No auto-restart in v0.6 — restart `letscode` to recover.
58
+
59
+ ## Trust model
60
+
61
+ A configured MCP server runs with your full shell power, same model as letscode's built-in `bash` tool. Only configure servers you trust to read your code, write your files, and hit network endpoints on your behalf.
62
+
63
+ ## License
64
+
65
+ MIT.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "letscode-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP (Model Context Protocol) client plugin for letscode. Surface tools from any MCP server as native letscode tools."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "letscode contributors" },
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "letscode>=0.6",
12
+ "pluggy>=1.5",
13
+ "pydantic>=2.0",
14
+ "mcp>=1.0",
15
+ ]
16
+
17
+ [project.entry-points.letscode]
18
+ mcp = "letscode_mcp.plugin"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/letscode_mcp"]
26
+
27
+ [tool.hatch.build.targets.wheel.sources]
28
+ "src" = ""
29
+
30
+ # Dev only: resolve letscode to the local checkout when this plugin's own
31
+ # venv is built. uv ignores [tool.uv.sources] in published wheels.
32
+ [tool.uv.sources]
33
+ letscode = { path = "../..", editable = true }
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=9.0.3",
38
+ "pytest-asyncio>=0.24",
39
+ ]
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "auto"
@@ -0,0 +1,5 @@
1
+ """letscode-mcp: client for the Model Context Protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,304 @@
1
+ """letscode-mcp: discover MCP servers from TOML config, expose their tools.
2
+
3
+ Design: ``notes/21-mcp-integration.md``. One file does it all — config
4
+ read, background asyncio loop, MCP discovery, tool wrapping. The plugin
5
+ contract (``letscode_register_tools``) is sync, the MCP SDK is async; a
6
+ single daemon thread owning a dedicated event loop bridges the two
7
+ (``asyncio.run_coroutine_threadsafe``).
8
+
9
+ Failure model is uniform: bad config, missing binary, hung server,
10
+ malformed schema, mid-session crash — every one becomes a one-line
11
+ stderr warning at registration *or* a ``ToolResult(is_error=True)`` at
12
+ call time. Never a traceback into the agent loop.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import atexit
19
+ import logging
20
+ import os
21
+ import re
22
+ import sys
23
+ import threading
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ import pluggy
27
+ from pydantic import BaseModel, ConfigDict
28
+
29
+ from letscode.agent.tools import ToolResult
30
+ from letscode.config import load_config
31
+ from letscode.llm.types import TextPart
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Coroutine
35
+ from concurrent.futures import Future as _CFuture
36
+
37
+ from letscode.agent.tools import ToolContext, ToolRegistry
38
+
39
+ _logger = logging.getLogger(__name__)
40
+ hookimpl = pluggy.HookimplMarker("letscode")
41
+
42
+ _DEFAULT_TIMEOUT = 10.0
43
+ # $VAR or ${VAR}. Empty string when unset — keeps configs portable
44
+ # instead of erroring on a host that doesn't have an optional secret.
45
+ _ENV_RE = re.compile(r"\$(?:\{([^}]+)\}|([A-Za-z_][A-Za-z0-9_]*))")
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Background asyncio loop (one daemon thread for all MCP sessions)
50
+ # ---------------------------------------------------------------------------
51
+
52
+ _bg_loop: asyncio.AbstractEventLoop | None = None
53
+ _bg_thread: threading.Thread | None = None
54
+ _bg_lock = threading.Lock()
55
+ # Stop-events for every live session task. atexit flips them to drain
56
+ # the bg loop cleanly on process exit.
57
+ _stop_events: list[asyncio.Event] = []
58
+
59
+
60
+ def _ensure_bg_loop() -> asyncio.AbstractEventLoop:
61
+ """Start the daemon thread + event loop on first use; idempotent."""
62
+ global _bg_loop, _bg_thread # noqa: PLW0603 — process-wide singleton, locked init
63
+ with _bg_lock:
64
+ if _bg_loop is not None:
65
+ return _bg_loop
66
+ loop = asyncio.new_event_loop()
67
+ thread = threading.Thread(
68
+ target=loop.run_forever, name="letscode-mcp", daemon=True
69
+ )
70
+ thread.start()
71
+ _bg_loop = loop
72
+ _bg_thread = thread
73
+ atexit.register(_shutdown)
74
+ return loop
75
+
76
+
77
+ def _run_in_bg(coro: Coroutine[Any, Any, Any], *, timeout: float | None = None) -> Any: # noqa: ANN401
78
+ """Run a coroutine on the bg loop from a sync caller; block for result."""
79
+ loop = _ensure_bg_loop()
80
+ fut: _CFuture[Any] = asyncio.run_coroutine_threadsafe(coro, loop)
81
+ return fut.result(timeout=timeout)
82
+
83
+
84
+ def _shutdown() -> None:
85
+ """Best-effort cleanup. Subprocesses die with the parent regardless."""
86
+ loop = _bg_loop
87
+ if loop is None:
88
+ return
89
+ for stop in _stop_events:
90
+ loop.call_soon_threadsafe(stop.set)
91
+ # ponytail: no join — daemon thread, OS reaps the subprocess children
92
+ loop.call_soon_threadsafe(loop.stop)
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Config helpers
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
+ def _expand_env(env_in: dict[str, str]) -> dict[str, str]:
101
+ """Expand ``$VAR`` / ``${VAR}`` in env values against ``os.environ``."""
102
+ return {
103
+ k: _ENV_RE.sub(lambda m: os.environ.get(m.group(1) or m.group(2), ""), v)
104
+ for k, v in env_in.items()
105
+ }
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Pydantic params model synthesised from an MCP tool's inputSchema
110
+ # ---------------------------------------------------------------------------
111
+
112
+
113
+ def _params_model(class_name: str, schema: dict[str, Any]) -> type[BaseModel]:
114
+ """A permissive ``BaseModel`` whose ``model_json_schema`` is the MCP schema.
115
+
116
+ The LLM sees the real schema (so it knows required fields and types);
117
+ incoming arg dicts are accepted verbatim via ``extra="allow"``. The
118
+ MCP server itself validates on receive — re-validating here would
119
+ just be a second source of truth that could disagree.
120
+ """
121
+ fixed = schema or {"type": "object", "properties": {}}
122
+
123
+ class _Params(BaseModel):
124
+ model_config = ConfigDict(extra="allow")
125
+
126
+ @classmethod
127
+ def model_json_schema(cls, **_kw: Any) -> dict[str, Any]: # type: ignore[override] # noqa: ANN401
128
+ return fixed
129
+
130
+ _Params.__name__ = class_name
131
+ return _Params
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # letscode Tool wrapping one discovered MCP tool
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ class _McpTool:
140
+ """A letscode Tool whose ``execute`` marshals a call to the bg loop."""
141
+
142
+ execution_mode = "parallel"
143
+
144
+ def __init__( # noqa: PLR0913 — six small kwargs read better than a config object
145
+ self,
146
+ *,
147
+ name: str,
148
+ description: str,
149
+ parameters: type[BaseModel],
150
+ session: Any, # noqa: ANN401 — opaque mcp ClientSession
151
+ remote_name: str,
152
+ timeout: float,
153
+ ) -> None:
154
+ self.name = name
155
+ self.description = description
156
+ self.parameters = parameters
157
+ self._session = session
158
+ self._remote_name = remote_name
159
+ self._timeout = timeout
160
+
161
+ async def execute(self, params: BaseModel, ctx: ToolContext) -> ToolResult:
162
+ del ctx
163
+ args = params.model_dump()
164
+ try:
165
+ cfut = asyncio.run_coroutine_threadsafe(self._call(args), _ensure_bg_loop())
166
+ return await asyncio.wrap_future(cfut)
167
+ except Exception as exc: # noqa: BLE001 — deliberate fail-soft; design §5
168
+ return ToolResult(
169
+ content=[TextPart(text=f"MCP call failed: {exc}")],
170
+ is_error=True,
171
+ )
172
+
173
+ async def _call(self, args: dict[str, Any]) -> ToolResult:
174
+ result = await asyncio.wait_for(
175
+ self._session.call_tool(self._remote_name, args), timeout=self._timeout
176
+ )
177
+ parts = [TextPart(text=_part_text(p)) for p in (result.content or [])]
178
+ if not parts:
179
+ parts = [TextPart(text="")]
180
+ return ToolResult(
181
+ content=parts, is_error=bool(getattr(result, "isError", False))
182
+ )
183
+
184
+
185
+ def _part_text(part: Any) -> str: # noqa: ANN401 — opaque mcp content union
186
+ """MCP TextContent has ``.text``; image/embedded-resource falls back to repr."""
187
+ return getattr(part, "text", None) or repr(part)
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # Server bring-up (runs on the bg loop)
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ async def _spawn_server(
196
+ server_name: str,
197
+ command: str,
198
+ args: list[str],
199
+ env: dict[str, str],
200
+ timeout: float,
201
+ ) -> tuple[Any, list[Any], asyncio.Event]:
202
+ """Open one MCP server in a long-running task.
203
+
204
+ Returns ``(session, tools, stop_event)``. The task holds the
205
+ ``stdio_client`` + ``ClientSession`` context managers open until
206
+ ``stop_event`` is set — the right pattern for anyio task scopes
207
+ (enter and exit on the same task).
208
+ """
209
+ # ponytail: imports inside the function so the plugin module loads
210
+ # even when the mcp SDK is absent (rare; matters during plugin discovery).
211
+ from mcp import ClientSession, StdioServerParameters # noqa: PLC0415
212
+ from mcp.client.stdio import stdio_client # noqa: PLC0415
213
+
214
+ params = StdioServerParameters(command=command, args=args, env=env or None)
215
+ loop = asyncio.get_running_loop()
216
+ ready: asyncio.Future[tuple[Any, list[Any]]] = loop.create_future()
217
+ stop = asyncio.Event()
218
+
219
+ async def _serve() -> None:
220
+ try: # noqa: PLW0717 — one long-lived async-with is the whole body
221
+ async with (
222
+ stdio_client(params) as (read, write),
223
+ ClientSession(read, write) as session,
224
+ ):
225
+ await asyncio.wait_for(session.initialize(), timeout=timeout)
226
+ listed = await asyncio.wait_for(session.list_tools(), timeout=timeout)
227
+ if not ready.done():
228
+ ready.set_result((session, list(listed.tools)))
229
+ await stop.wait()
230
+ except Exception as exc: # noqa: BLE001 — surfaces to caller via ready.set_exception
231
+ if not ready.done():
232
+ ready.set_exception(exc)
233
+
234
+ # The bg loop holds the running task; the linter's "store the handle"
235
+ # warning doesn't apply — the task lives until `stop` is set.
236
+ asyncio.create_task(_serve(), name=f"mcp-{server_name}") # noqa: RUF006
237
+ session, tools = await ready
238
+ return session, tools, stop
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # The hook
243
+ # ---------------------------------------------------------------------------
244
+
245
+
246
+ @hookimpl
247
+ def letscode_register_tools(registry: ToolRegistry) -> None:
248
+ """Discover every configured MCP server, register its tools."""
249
+ try:
250
+ cfg = load_config()
251
+ servers = cfg.sections.get("mcp", {}).get("servers", {})
252
+ except Exception:
253
+ _logger.warning("mcp: could not read config; skipping", exc_info=True)
254
+ return
255
+ if not servers:
256
+ return
257
+ for server_name, server_cfg in servers.items():
258
+ _register_one_server(registry, server_name, server_cfg or {})
259
+
260
+
261
+ def _register_one_server(
262
+ registry: ToolRegistry, server_name: str, server_cfg: dict[str, Any]
263
+ ) -> None:
264
+ command = server_cfg.get("command")
265
+ if not command:
266
+ _warn(f"server {server_name!r} missing 'command'; skipped")
267
+ return
268
+ args = list(server_cfg.get("args", []))
269
+ env = _expand_env(dict(server_cfg.get("env", {})))
270
+ prefix = server_cfg.get("prefix") or f"mcp_{server_name}_"
271
+ timeout = float(server_cfg.get("timeout", _DEFAULT_TIMEOUT))
272
+
273
+ try:
274
+ session, tools, stop = _run_in_bg(
275
+ _spawn_server(server_name, command, args, env, timeout),
276
+ timeout=timeout + 5,
277
+ )
278
+ except Exception as exc: # noqa: BLE001 — fail-soft; design §5
279
+ _warn(f"server {server_name!r} ({command}) failed: {exc}")
280
+ return
281
+ _stop_events.append(stop)
282
+
283
+ for t in tools:
284
+ try:
285
+ tool_name = f"{prefix}{t.name}"
286
+ registry.add(
287
+ _McpTool(
288
+ name=tool_name,
289
+ description=t.description or "",
290
+ parameters=_params_model(
291
+ f"_McpParams_{server_name}_{t.name}",
292
+ dict(t.inputSchema or {}),
293
+ ),
294
+ session=session,
295
+ remote_name=t.name,
296
+ timeout=timeout,
297
+ )
298
+ )
299
+ except Exception as exc: # noqa: BLE001 — fail-soft per tool
300
+ _warn(f"server {server_name!r} tool {t.name!r} skipped: {exc}")
301
+
302
+
303
+ def _warn(msg: str) -> None:
304
+ print(f"letscode-mcp: {msg}", file=sys.stderr)