lcode-cli 0.16.0__tar.gz → 0.18.0__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 (142) hide show
  1. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/CHANGELOG.md +39 -1
  2. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/PKG-INFO +9 -3
  3. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/README.md +8 -2
  4. lcode_cli-0.18.0/action.yml +79 -0
  5. lcode_cli-0.18.0/docs/automation.md +193 -0
  6. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/commands.md +2 -1
  7. lcode_cli-0.18.0/docs/git.md +98 -0
  8. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/quickstart.md +1 -1
  9. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/usage.md +7 -0
  10. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/examples/README.md +1 -0
  11. lcode_cli-0.18.0/examples/api/fix_tests.py +37 -0
  12. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/mkdocs.yml +2 -0
  13. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/pyproject.toml +2 -0
  14. lcode_cli-0.18.0/src/lcode/__init__.py +16 -0
  15. lcode_cli-0.18.0/src/lcode/action.py +358 -0
  16. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/agent.py +56 -5
  17. lcode_cli-0.18.0/src/lcode/api.py +383 -0
  18. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/cli.py +92 -107
  19. lcode_cli-0.18.0/src/lcode/gitflow.py +640 -0
  20. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/permissions.py +13 -0
  21. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/repl.py +38 -3
  22. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/subagents.py +2 -1
  23. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/tools.py +6 -0
  24. lcode_cli-0.18.0/tests/test_action.py +234 -0
  25. lcode_cli-0.18.0/tests/test_api.py +136 -0
  26. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_backends.py +6 -1
  27. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_cli.py +1 -1
  28. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_extensions.py +3 -3
  29. lcode_cli-0.18.0/tests/test_gitflow.py +260 -0
  30. lcode_cli-0.16.0/src/lcode/__init__.py +0 -3
  31. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.editorconfig +0 -0
  32. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/CODEOWNERS +0 -0
  33. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  34. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  35. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  36. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  37. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  38. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/dependabot.yml +0 -0
  39. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/workflows/ci.yml +0 -0
  40. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/workflows/docs.yml +0 -0
  41. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.github/workflows/release.yml +0 -0
  42. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.gitignore +0 -0
  43. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/.pre-commit-config.yaml +0 -0
  44. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/AGENTS.md +0 -0
  45. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/CODE_OF_CONDUCT.md +0 -0
  46. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/CONTRIBUTING.md +0 -0
  47. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/LICENSE +0 -0
  48. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/SECURITY.md +0 -0
  49. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/agents.md +0 -0
  50. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/assets/demo.svg +0 -0
  51. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/assets/extra.css +0 -0
  52. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/assets/logo.svg +0 -0
  53. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/assets/social-preview.png +0 -0
  54. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/bench.md +0 -0
  55. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/changelog.md +0 -0
  56. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/configuration.md +0 -0
  57. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/contributing.md +0 -0
  58. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/hooks.md +0 -0
  59. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/how-it-works.md +0 -0
  60. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/index.md +0 -0
  61. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/installation.md +0 -0
  62. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/lsp.md +0 -0
  63. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/mcp.md +0 -0
  64. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/memory.md +0 -0
  65. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/models.md +0 -0
  66. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/sandbox.md +0 -0
  67. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/search.md +0 -0
  68. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/servers.md +0 -0
  69. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/docs/troubleshooting.md +0 -0
  70. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/examples/commands/review.md +0 -0
  71. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/examples/skills/commit-message/SKILL.md +0 -0
  72. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  73. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/install.sh +0 -0
  74. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/overrides/main.html +0 -0
  75. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/scripts/record_demo.py +0 -0
  76. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/scripts/social-preview.html +0 -0
  77. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/__main__.py +0 -0
  78. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/backends.py +0 -0
  79. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/bench.py +0 -0
  80. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/bench_backend.py +0 -0
  81. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/catalog.py +0 -0
  82. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/checkpoints.py +0 -0
  83. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/codesearch.py +0 -0
  84. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/config.py +0 -0
  85. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/context.py +0 -0
  86. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/extensions.py +0 -0
  87. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/frontmatter.py +0 -0
  88. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/hardware.py +0 -0
  89. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/hooks.py +0 -0
  90. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/limits.py +0 -0
  91. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/lsp.py +0 -0
  92. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/__init__.py +0 -0
  93. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/auth.py +0 -0
  94. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/catalog.py +0 -0
  95. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/catalog.toml +0 -0
  96. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/client.py +0 -0
  97. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/commands.py +0 -0
  98. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/config.py +0 -0
  99. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/http.py +0 -0
  100. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/manager.py +0 -0
  101. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/protocol.py +0 -0
  102. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/server.py +0 -0
  103. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/mcp/stdio.py +0 -0
  104. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/memory.py +0 -0
  105. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/models.toml +0 -0
  106. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/ollama.py +0 -0
  107. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/planning.py +0 -0
  108. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/render.py +0 -0
  109. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/repomap.py +0 -0
  110. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/sandbox.py +0 -0
  111. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/servers/__init__.py +0 -0
  112. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/servers/encord.py +0 -0
  113. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/servers/valohai.py +0 -0
  114. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/sessions.py +0 -0
  115. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/vision.py +0 -0
  116. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/src/lcode/web.py +0 -0
  117. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/conftest.py +0 -0
  118. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/fake_lsp_server.py +0 -0
  119. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/fake_mcp_server.py +0 -0
  120. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_agent.py +0 -0
  121. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_bench.py +0 -0
  122. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_catalog.py +0 -0
  123. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_checkpoints.py +0 -0
  124. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_codesearch.py +0 -0
  125. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_config.py +0 -0
  126. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_context.py +0 -0
  127. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_context_picker.py +0 -0
  128. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_hooks.py +0 -0
  129. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_lsp.py +0 -0
  130. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_mcp.py +0 -0
  131. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_memory.py +0 -0
  132. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_permissions.py +0 -0
  133. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_plan_mode.py +0 -0
  134. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_repomap.py +0 -0
  135. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_sandbox.py +0 -0
  136. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_servers.py +0 -0
  137. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_sessions.py +0 -0
  138. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_subagents.py +0 -0
  139. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_tools.py +0 -0
  140. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_vision.py +0 -0
  141. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/tests/test_web.py +0 -0
  142. {lcode_cli-0.16.0 → lcode_cli-0.18.0}/uv.lock +0 -0
@@ -6,6 +6,42 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.18.0] - 2026-10-06
10
+
11
+ ### Added
12
+
13
+ - `lcode -p "…" --output json` prints one result object (status, answer, tool calls, files
14
+ changed, token usage); `--output stream-json` prints an event per model step and tool result,
15
+ then the result. `--max-steps` and `--allowed-tools` limit a run, and the exit code says how it
16
+ ended. Without a person to ask, anything that needs permission is refused.
17
+ - A Python API: `from lcode import Session`, with `run()`, `stream()` and an `approve` callback
18
+ for permission requests. See `examples/api/fix_tests.py`.
19
+ - A GitHub Action for self-hosted runners (`uses: nasser1941/lcode@v0.18.0`). It answers `@lcode`
20
+ in issues and pull requests from the repository's owners, members and collaborators, proposes
21
+ changes as a pull request (or pushes them to the pull request's branch), and can review pull
22
+ requests. See [Automation](https://nasser1941.github.io/lcode/automation/).
23
+
24
+ ## [0.17.0] - 2026-10-06
25
+
26
+ ### Added
27
+
28
+ - `/commit` writes a commit message in the repository's style, from the diff, the conversation
29
+ and recent commits, and commits once you approve it. It commits what you staged, or every
30
+ change except files the model thinks are unrelated and files that may hold secrets.
31
+ - `/review [base]` reviews the uncommitted changes, or the branch against a base, and reports
32
+ findings with `path:line`. The model can read but not change anything during a review.
33
+ - `/pr [base]` pushes the branch and opens a GitHub pull request with `gh` after you approve the
34
+ title and description; commits made on `main` itself go to a new branch.
35
+ - A command of your own called `commit`, `review` or `pr` replaces the built-in one.
36
+ - `lcode --worktree [NAME]` (`-w`) runs the session in a git worktree on its own branch, so
37
+ several sessions can work on one repository at once. It's removed at the end if nothing
38
+ changed. See [Git workflow](https://nasser1941.github.io/lcode/git/).
39
+
40
+ ### Fixed
41
+
42
+ - With another model server, `/ctx` no longer says the model reloads: it changes how much of the
43
+ server's context window lcode uses.
44
+
9
45
  ## [0.16.0] - 2026-10-05
10
46
 
11
47
  ### Added
@@ -398,7 +434,9 @@ First public release.
398
434
  - `AGENTS.md` project instructions and `/init` to generate them.
399
435
  - One-line installer for Linux and macOS.
400
436
 
401
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.16.0...HEAD
437
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.18.0...HEAD
438
+ [0.18.0]: https://github.com/nasser1941/lcode/compare/v0.17.0...v0.18.0
439
+ [0.17.0]: https://github.com/nasser1941/lcode/compare/v0.16.0...v0.17.0
402
440
  [0.16.0]: https://github.com/nasser1941/lcode/compare/v0.15.0...v0.16.0
403
441
  [0.15.0]: https://github.com/nasser1941/lcode/compare/v0.14.0...v0.15.0
404
442
  [0.14.0]: https://github.com/nasser1941/lcode/compare/v0.13.0...v0.14.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.16.0
3
+ Version: 0.18.0
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -101,6 +101,12 @@ through [Ollama](https://ollama.com), and when it needs current information it c
101
101
  - **Remembers what matters.** Corrections, decisions and where things live carry over to the next
102
102
  session as short notes, saved by the model, by you (`/remember`) or by a quick check at the end
103
103
  of a session. Not whole conversations, and never secrets.
104
+ - **Git workflow.** `/commit` writes messages in your repository's style, `/review` checks the
105
+ changes with `file:line` findings, `/pr` opens the pull request, each after you approve it, and
106
+ `lcode --worktree` runs parallel sessions on one repository.
107
+ - **Automation.** `lcode -p --output json` for scripts and CI, a Python API (`from lcode import
108
+ Session`), and a GitHub Action that answers `@lcode` in issues and pull requests on your own
109
+ self-hosted runner.
104
110
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
105
111
  with tool calling. Or use LM Studio, llama.cpp, vLLM or MLX instead of Ollama.
106
112
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
@@ -208,8 +214,8 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
208
214
 
209
215
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
210
216
 
211
- - [Git workflow: /commit, /review, /pr and parallel worktrees](https://github.com/nasser1941/lcode/issues/50)
212
- - [Automation: JSON output, a Python API and a GitHub Action for self-hosted runners](https://github.com/nasser1941/lcode/issues/48)
217
+ - [Editor integration through the Agent Client Protocol](https://github.com/nasser1941/lcode/issues/49)
218
+ - [Long-running work: finish notifications and background commands](https://github.com/nasser1941/lcode/issues/51)
213
219
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
214
220
 
215
221
  ## Contributing
@@ -65,6 +65,12 @@ through [Ollama](https://ollama.com), and when it needs current information it c
65
65
  - **Remembers what matters.** Corrections, decisions and where things live carry over to the next
66
66
  session as short notes, saved by the model, by you (`/remember`) or by a quick check at the end
67
67
  of a session. Not whole conversations, and never secrets.
68
+ - **Git workflow.** `/commit` writes messages in your repository's style, `/review` checks the
69
+ changes with `file:line` findings, `/pr` opens the pull request, each after you approve it, and
70
+ `lcode --worktree` runs parallel sessions on one repository.
71
+ - **Automation.** `lcode -p --output json` for scripts and CI, a Python API (`from lcode import
72
+ Session`), and a GitHub Action that answers `@lcode` in issues and pull requests on your own
73
+ self-hosted runner.
68
74
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
69
75
  with tool calling. Or use LM Studio, llama.cpp, vLLM or MLX instead of Ollama.
70
76
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
@@ -172,8 +178,8 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
172
178
 
173
179
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
174
180
 
175
- - [Git workflow: /commit, /review, /pr and parallel worktrees](https://github.com/nasser1941/lcode/issues/50)
176
- - [Automation: JSON output, a Python API and a GitHub Action for self-hosted runners](https://github.com/nasser1941/lcode/issues/48)
181
+ - [Editor integration through the Agent Client Protocol](https://github.com/nasser1941/lcode/issues/49)
182
+ - [Long-running work: finish notifications and background commands](https://github.com/nasser1941/lcode/issues/51)
177
183
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
178
184
 
179
185
  ## Contributing
@@ -0,0 +1,79 @@
1
+ name: lcode
2
+ description: Answer @lcode in issues and pull requests, propose changes and review pull requests, with an open-weight model on your own self-hosted runner.
3
+ author: nasser1941
4
+ branding:
5
+ icon: terminal
6
+ color: gray-dark
7
+
8
+ inputs:
9
+ model:
10
+ description: The model to use (default, the runner's lcode configuration)
11
+ required: false
12
+ default: ""
13
+ trigger:
14
+ description: The word that calls lcode in a comment
15
+ required: false
16
+ default: "@lcode"
17
+ review:
18
+ description: Review pull requests when they're opened or updated ("true" or "false")
19
+ required: false
20
+ default: "false"
21
+ review-forks:
22
+ description: Also review pull requests from forks; their code runs through lcode's tools on your runner
23
+ required: false
24
+ default: "false"
25
+ author-associations:
26
+ description: Who can call lcode, by their association with the repository
27
+ required: false
28
+ default: "OWNER,MEMBER,COLLABORATOR"
29
+ permission-mode:
30
+ description: auto-edit (file changes, no shell commands except allow-commands) or yolo (anything; only on an isolated runner)
31
+ required: false
32
+ default: "auto-edit"
33
+ allow-commands:
34
+ description: Shell commands the model may run, one per line or comma-separated, with * wildcards (e.g. "pytest*, npm test")
35
+ required: false
36
+ default: ""
37
+ max-steps:
38
+ description: Model steps per request before lcode stops
39
+ required: false
40
+ default: "60"
41
+ lcode-version:
42
+ description: The lcode version to run (default, the one matching this action's tag)
43
+ required: false
44
+ default: ""
45
+ github-token:
46
+ description: Token for comments, pull requests and pushes
47
+ required: false
48
+ default: ${{ github.token }}
49
+
50
+ runs:
51
+ using: composite
52
+ steps:
53
+ - name: Run lcode
54
+ shell: bash
55
+ env:
56
+ GITHUB_TOKEN: ${{ inputs.github-token }}
57
+ LCODE_MODEL: ${{ inputs.model }}
58
+ LCODE_TRIGGER: ${{ inputs.trigger }}
59
+ LCODE_REVIEW: ${{ inputs.review }}
60
+ LCODE_REVIEW_FORKS: ${{ inputs.review-forks }}
61
+ LCODE_ASSOCIATIONS: ${{ inputs.author-associations }}
62
+ LCODE_PERMISSION_MODE: ${{ inputs.permission-mode }}
63
+ LCODE_ALLOW_COMMANDS: ${{ inputs.allow-commands }}
64
+ LCODE_MAX_STEPS: ${{ inputs.max-steps }}
65
+ LCODE_VERSION: ${{ inputs.lcode-version }}
66
+ ACTION_REF: ${{ github.action_ref }}
67
+ run: |
68
+ version="${LCODE_VERSION:-${ACTION_REF#v}}"
69
+ spec="lcode-cli"
70
+ if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then spec="lcode-cli==$version"; fi
71
+ if command -v uvx > /dev/null; then
72
+ uvx --from "$spec" lcode action
73
+ elif command -v lcode > /dev/null; then
74
+ lcode action
75
+ else
76
+ python3 -m venv "$RUNNER_TEMP/lcode-venv"
77
+ "$RUNNER_TEMP/lcode-venv/bin/pip" install --quiet "$spec"
78
+ "$RUNNER_TEMP/lcode-venv/bin/lcode" action
79
+ fi
@@ -0,0 +1,193 @@
1
+ # Automation
2
+
3
+ lcode can also run without a person at the keyboard:
4
+ - **Scripts and CI** get structured results from `lcode -p`.
5
+ - **Programs** use it from Python.
6
+ - **A GitHub Action** answers `@lcode` in issues and pull requests on your own GPU machine.
7
+
8
+ In all three, nothing waits for an answer: anything that would need your permission is refused,
9
+ unless the permission mode, a [permission rule](hooks.md#permission-rules) or your program allows it.
10
+
11
+ ## Structured output
12
+
13
+ ```bash
14
+ lcode -p "Fix the failing test in tests/test_parser.py" --auto-edit --output json
15
+ lcode -p "Explain the retry logic" --output stream-json --max-steps 20
16
+ ```
17
+
18
+ | Option | |
19
+ |---|---|
20
+ | `--output json` | One JSON object when the request is done |
21
+ | `--output stream-json` | One JSON event per line as the request runs, ending with the same result object |
22
+ | `--max-steps N` | Stop after N model steps (status `max_steps`) |
23
+ | `--allowed-tools LIST` | Only these tools, e.g. `"read_file,grep,glob,bash"` |
24
+
25
+ lcode's usual output goes to stderr, so stdout holds only the JSON. The exit code says how it
26
+ went: `0` success, `1` an error, `3` stopped at `--max-steps`, `130` interrupted.
27
+
28
+ ### The result
29
+
30
+ ```json
31
+ {
32
+ "type": "result",
33
+ "schema": 1,
34
+ "lcode": "0.18.0",
35
+ "status": "success",
36
+ "text": "Two bugs fixed in `calc.py`: …",
37
+ "session_id": "20261006-101503-7f3a",
38
+ "model": "lcode-qwen3.6-35b",
39
+ "tool_calls": [
40
+ {"id": "call_ab9fmzq1", "name": "read_file", "arguments": {"path": "calc.py"}, "error": false, "output": "…"},
41
+ {"id": "call_lhhyt2jn", "name": "bash", "arguments": {"command": "pytest -q"}, "error": true, "output": "This needs the user's permission, …"}
42
+ ],
43
+ "files_changed": [{"status": "M", "path": "calc.py"}],
44
+ "usage": {"requests": 6, "prompt_tokens": 35092, "output_tokens": 942},
45
+ "seconds": 30.67,
46
+ "error": ""
47
+ }
48
+ ```
49
+
50
+ | Field | |
51
+ |---|---|
52
+ | `status` | `success`, `error`, `max_steps` or `interrupted` |
53
+ | `text` | The model's final answer |
54
+ | `tool_calls` | Every tool call, with its arguments and output (the first 4,000 characters). `error` is true when the call failed or wasn't allowed |
55
+ | `files_changed` | Files the request added (`A`), modified (`M`) or deleted (`D`), shell commands included |
56
+ | `usage` | Model requests and tokens; `prompt_tokens` includes tokens served from the prompt cache |
57
+ | `error` | What went wrong, when `status` is `error` |
58
+ | `session_id` | Continue the conversation later with `lcode --resume <id>` |
59
+
60
+ A failure before the request starts (no model server, an unknown model) prints
61
+ `{"type": "result", "status": "error", "error": "…"}` and exits with `1`.
62
+
63
+ ### The events (stream-json)
64
+
65
+ ```json
66
+ {"type": "start", "schema": 1, "lcode": "0.18.0", "session_id": "…", "model": "lcode-qwen3.6-35b", "cwd": "/src/app"}
67
+ {"type": "assistant", "text": "", "tool_calls": [{"id": "call_1", "name": "read_file", "arguments": {"path": "calc.py"}}]}
68
+ {"type": "tool_result", "id": "call_1", "name": "read_file", "error": false, "output": "…"}
69
+ {"type": "assistant", "text": "The bug is in clamp(): …", "tool_calls": []}
70
+ {"type": "result", "status": "success", …}
71
+ ```
72
+
73
+ An `assistant` event comes after each model step; a `tool_result` follows each of its calls, with
74
+ the same `id`.
75
+
76
+ ## Python
77
+
78
+ ```python
79
+ from lcode import Session
80
+
81
+ with Session("path/to/repo", permission_mode="auto-edit") as session:
82
+ result = session.run("Fix the failing test in tests/test_parser.py")
83
+ print(result.status, result.text)
84
+ for change in result.files_changed:
85
+ print(change["status"], change["path"])
86
+ ```
87
+
88
+ - **A session is one conversation:** each `run` continues it.
89
+ - **Events while it runs:** `session.stream(prompt)` yields the events above, ending with the result.
90
+ - **Permissions:** pass `approve` to decide permission requests. It gets
91
+ `{"kind": "bash", "title": "…", "target": "pytest -q"}`; without it, every request is refused.
92
+ - **Options:** `model`, `context`, `permission_mode`, `allowed_tools`, `max_steps`, `think`,
93
+ `web`, `memory`, `mcp` and `sandbox`. Leave one out to use your
94
+ [configuration](configuration.md).
95
+ - **lcode's own output:** `verbose=True` shows it on stderr.
96
+ - **When lcode can't start** (no model server, a missing model): `lcode.SetupError`.
97
+
98
+ [`examples/api/fix_tests.py`](https://github.com/nasser1941/lcode/blob/main/examples/api/fix_tests.py)
99
+ runs a repository's tests, lets the model fix what fails and approves only the test command:
100
+
101
+ ```text
102
+ $ python examples/api/fix_tests.py ~/src/calc
103
+ ✓ read_file: 1 def average(values):
104
+ ✓ bash: ============================= test session starts ==============================
105
+ ✓ edit_file: Edited calc.py (1 replacement(s)). Result:
106
+ ✓ edit_file: Edited calc.py (1 replacement(s)). Result:
107
+ ✓ bash: ============================= test session starts ==============================
108
+
109
+ success in 22s, 5 model requests
110
+ M calc.py
111
+ ```
112
+
113
+ ## GitHub Action
114
+
115
+ The action runs lcode on a **self-hosted runner**, a machine of yours with a GPU and Ollama (or
116
+ [another model server](servers.md)). The code and the model stay on that machine; only the
117
+ replies and the commits go to GitHub.
118
+
119
+ - **Someone comments `@lcode …`** on an issue or a pull request. lcode reacts with 👀, does what
120
+ they ask and replies.
121
+ - When it changed files on an **issue**, it commits them to a new branch and opens a pull
122
+ request that closes the issue.
123
+ - On a **pull request**, it pushes the commit to that pull request's branch.
124
+ - **With `review: true`**, every new or updated pull request gets a review as a comment.
125
+
126
+ ```yaml
127
+ # .github/workflows/lcode.yml
128
+ name: lcode
129
+ on:
130
+ issue_comment:
131
+ types: [created]
132
+ pull_request:
133
+ types: [opened, synchronize]
134
+
135
+ permissions:
136
+ contents: write
137
+ issues: write
138
+ pull-requests: write
139
+
140
+ jobs:
141
+ lcode:
142
+ if: github.event_name == 'pull_request' || contains(github.event.comment.body, '@lcode')
143
+ runs-on: [self-hosted, gpu]
144
+ steps:
145
+ - uses: actions/checkout@v5
146
+ with:
147
+ fetch-depth: 0
148
+ - uses: nasser1941/lcode@v0.18.0
149
+ with:
150
+ review: true
151
+ allow-commands: "pytest*, npm test"
152
+ ```
153
+
154
+ | Input | Default | |
155
+ |---|---|---|
156
+ | `model` | the runner's config | The model to use |
157
+ | `trigger` | `@lcode` | The word that calls lcode in a comment |
158
+ | `review` | `false` | Review pull requests when they're opened or updated |
159
+ | `review-forks` | `false` | Also review pull requests from forks |
160
+ | `author-associations` | `OWNER,MEMBER,COLLABORATOR` | Who can call lcode |
161
+ | `permission-mode` | `auto-edit` | `auto-edit`: file changes, and only the shell commands in `allow-commands`. `yolo`: anything |
162
+ | `allow-commands` | | Shell commands the model may run, with `*` wildcards |
163
+ | `max-steps` | `60` | Model steps per request |
164
+ | `lcode-version` | the action's | The lcode version to run |
165
+
166
+ ### Setting up the runner
167
+
168
+ 1. [Add a self-hosted runner](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners)
169
+ to the repository, and give it a label such as `gpu`.
170
+ 2. On that machine, as the runner's user, install lcode and a model:
171
+ `curl -fsSL https://nasser1941.github.io/lcode/install.sh | bash`. The action runs lcode with
172
+ [uv](https://docs.astral.sh/uv/) when it's there, otherwise with the `lcode` it finds, otherwise
173
+ it installs lcode with pip.
174
+ 3. Settings and [permission rules](hooks.md#permission-rules) for the action go in that user's
175
+ `~/.config/lcode/config.toml`. A repository's own `.lcode/settings.toml` isn't used, because
176
+ nobody approved it on the runner.
177
+
178
+ ### Safety
179
+
180
+ - **Who can trigger it.** Only people with an owner, member or collaborator association, and
181
+ never bots.
182
+ - **Other people's text.** The issue's description and the code may have been written by anyone.
183
+ lcode tells the model to treat instructions in them as information, but a model can still be
184
+ talked into things. Keep `permission-mode: auto-edit` with a short `allow-commands` list,
185
+ unless the runner is isolated and disposable.
186
+ - **Forks.** Pull requests from forks aren't reviewed unless you set `review-forks`: their code
187
+ would run through lcode's tools on your machine.
188
+ - **Git.** lcode does the git steps itself: the model can't commit, push or switch branches, and
189
+ commits are made as `lcode`.
190
+ - **Public repositories.** GitHub
191
+ [advises against](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners)
192
+ self-hosted runners for public repositories, since anyone can open a pull request. Use the
193
+ action on private repositories, or on a runner you can afford to lose.
@@ -46,7 +46,8 @@ Report bugs first, with `path:line`, then missing tests. Don't change any files.
46
46
  | `~/.config/lcode/commands/<name>.md` | For every repository |
47
47
 
48
48
  The file name is the command's name. A repository's command overrides one of yours with the same
49
- name; built-in commands such as `/help` can't be replaced.
49
+ name. A command called `review`, `commit` or `pr` replaces lcode's [built-in one](git.md), so a team
50
+ can use its own checklist; other built-in commands such as `/help` can't be replaced.
50
51
 
51
52
  ## Skills
52
53
 
@@ -0,0 +1,98 @@
1
+ # Git workflow
2
+
3
+ The everyday git steps, without spelling them out each time:
4
+ - **`/commit`** writes a message in your repository's style;
5
+ - **`/review`** checks the changes before you commit or merge;
6
+ - **`/pr`** opens a pull request;
7
+ - **`lcode --worktree`** runs several sessions on one repository without them getting in each other's way.
8
+
9
+ lcode never commits, pushes or opens a pull request until you say yes, also in `yolo` mode.
10
+
11
+ ## /commit
12
+
13
+ ```text
14
+ ❯ /commit
15
+ ╭─ Commit message ───────────────────────────────────────────────╮
16
+ │ feat: add median() │
17
+ │ │
18
+ │ Add a median() function to stats.py and its tests. It raises │
19
+ │ ValueError on empty input, consistent with mean(). │
20
+ ╰────────────────────────────────────────────────────────────────╯
21
+ Files stats.py, test_stats.py
22
+ Left out (unrelated, the model thinks): debug.log
23
+ [y] commit · [e] edit the message · [n] cancel: y
24
+ ✓ Committed fecd803 feat: add median()
25
+ ```
26
+
27
+ - **What goes in.**
28
+ - If you staged files (`git add`), exactly those.
29
+ - Otherwise every changed and new file, except:
30
+ - files the model thinks are unrelated to the work, listed so you can see them;
31
+ - files that may hold secrets (`.env`, `*.pem`, keys, credentials and the like). These only go in when you stage them yourself.
32
+ - **The message.** It's written from the diff, from what the conversation knows about why you made the change, and from the repository's recent commits: their prefixes, tense, length and trailers. `/commit mention ticket ABC-12` adds your own notes.
33
+ - **Editing.** `e` opens the message in your editor (`$EDITOR`).
34
+ - **Hooks.** Your git hooks run as usual: a failing pre-commit hook stops the commit and shows why.
35
+
36
+ ## /review
37
+
38
+ | | Reviews |
39
+ |---|---|
40
+ | `/review` | The uncommitted changes if any tracked file changed; otherwise the branch's changes compared with the default branch (`main`) |
41
+ | `/review develop` | The branch, and any uncommitted changes, compared with `develop` |
42
+
43
+ The model reads the code around the changes where it needs to. During a review it can't change
44
+ files or run commands that change anything. It reports findings with `path:line`, the kind of
45
+ problem (bug, test, security or clarity), what's wrong and how to fix it, most important first:
46
+
47
+ ```text
48
+ stats.py:18 — bug
49
+ The even-length median is broken: for median([1, 3, 2, 4]) the old code returns 2.5, the new
50
+ code returns 3. Revert line 18 to (s[mid - 1] + s[mid]) / 2.
51
+
52
+ test_stats.py:13 — test
53
+ test_median() asserts median([1, 3, 2, 4]) == 2.5, so it fails with this change.
54
+ ```
55
+
56
+ Then ask it to fix what you agree with, in the same conversation.
57
+
58
+ **Your own checklist:** put review guidelines in `AGENTS.md` or in a [skill](commands.md#skills),
59
+ and the review follows them. To replace `/review` entirely, write a
60
+ [command](commands.md#commands) of your own called `review`. The same goes for `/commit` and `/pr`.
61
+
62
+ ## /pr
63
+
64
+ Pushes the branch and opens a GitHub pull request. It needs the [GitHub CLI](https://cli.github.com),
65
+ signed in with `gh auth login`.
66
+
67
+ - **Into which branch.** The default branch, or the one you name: `/pr develop`. Anything else you
68
+ type is a note for the description: `/pr develop link it to #12`.
69
+ - **Before you start.** Commit first: `/pr` stops while there are uncommitted changes.
70
+ - **Commits made on `main` itself** go to a new branch named after the change. Your local `main`
71
+ still has them, so reset it once the pull request is merged.
72
+ - **The title and description** are written from the commits, the diff and the conversation, and
73
+ follow the repository's pull request template if it has one. You see them before anything is
74
+ pushed: `y` to push and open, `e` to edit, `n` to stop.
75
+ - **A branch with an open pull request:** `/pr` offers to push the new commits instead.
76
+
77
+ ## Parallel sessions: lcode --worktree
78
+
79
+ ```bash
80
+ lcode --worktree fix-login # a session on a new branch fix-login, in a folder of its own
81
+ lcode -w # the same, with a generated name
82
+ ```
83
+
84
+ A [git worktree](https://git-scm.com/docs/git-worktree) is a second working folder of the same
85
+ repository, on its own branch. Sessions in different worktrees can edit, test and commit at the
86
+ same time without touching each other's files, or yours.
87
+
88
+ - **Where it starts.** The worktree starts from your current commit. Uncommitted changes in your
89
+ own folder don't come along.
90
+ - **Where it lives.** lcode keeps worktree folders in `~/.local/state/lcode/worktrees/` and prints
91
+ the path. They're outside the repository so the model can't mistake your folder for its own.
92
+ - **When the session ends.**
93
+ - If nothing changed (no commits and no uncommitted changes), the worktree and its branch are
94
+ removed.
95
+ - Otherwise it stays, and lcode tells you how to continue (`lcode --worktree fix-login` again),
96
+ merge it (`git merge fix-login`) or remove it.
97
+ - **What's separate.** Sessions (`/resume`, `lcode -c`) and [checkpoints](usage.md#undo-and-checkpoints)
98
+ belong to each worktree. [Memory notes](memory.md) are shared with the repository.
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.16.0 — local coding agent │
15
+ │ lcode v0.18.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -15,11 +15,15 @@ lcode mcp [catalog|add|list] # connect MCP servers: Jira, GitHub, AWS, databases
15
15
  | Option | Meaning |
16
16
  |---|---|
17
17
  | `-p, --prompt TEXT` | Run one request non-interactively, print the result and exit |
18
+ | `--output json\|stream-json` | With `-p`: print a JSON result, or a JSON event per line ([Automation](automation.md)) |
19
+ | `--max-steps N` | Stop a request after N model steps |
20
+ | `--allowed-tools LIST` | Only these tools, e.g. `"read_file,grep,glob,bash"` |
18
21
  | `-m, --model NAME` | Catalog key (see `lcode models`) or any installed Ollama model |
19
22
  | `--context SIZE` | Context window, e.g. `65536`, `128k`, `1m` |
20
23
  | `-r, --repo DIR` | Work in another directory |
21
24
  | `-c, --continue` | Continue the most recently used session in this directory |
22
25
  | `--resume [SESSION]` | Resume a saved session: pick from a list, or give its number, name or id |
26
+ | `-w, --worktree [NAME]` | Work in a [git worktree](git.md#parallel-sessions-lcode-worktree) of its own, on branch NAME; removed at the end if nothing changed |
23
27
  | `--plan` | Start in [plan mode](#plan-mode): agree on a plan before anything changes |
24
28
  | `--auto-edit` | Apply file edits without asking (commands still ask) |
25
29
  | `--yolo` | Never ask for permission |
@@ -74,6 +78,9 @@ reasoning is on.
74
78
  | `/mode [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
75
79
  | `/cd DIR` | Change the working directory |
76
80
  | `/todos` | Show the model's task list |
81
+ | `/commit [notes]` | [Commit](git.md#commit) the changes with a message in the repository's style, once you approve it |
82
+ | `/review [base]` | [Review](git.md#review) the uncommitted changes, or the branch against a base |
83
+ | `/pr [base] [notes]` | Push the branch and open a GitHub [pull request](git.md#pr), once you approve it |
77
84
  | `/exit` | Quit |
78
85
 
79
86
  ## What the model can do
@@ -7,6 +7,7 @@ them everywhere):
7
7
  |---|---|---|
8
8
  | [`commands/review.md`](commands/review.md) | `.lcode/commands/review.md` | `/review` or `/review the parser` |
9
9
  | [`skills/commit-message/`](skills/commit-message/) | `.lcode/skills/commit-message/` | ask for a commit message, or `/commit-message` |
10
+ | [`api/fix_tests.py`](api/fix_tests.py) | anywhere | `python fix_tests.py path/to/repo`: lcode from Python ([Automation](https://nasser1941.github.io/lcode/automation/)) |
10
11
 
11
12
  ```bash
12
13
  mkdir -p .lcode/commands .lcode/skills
@@ -0,0 +1,37 @@
1
+ """Fix a repository's failing tests with lcode, from Python.
2
+
3
+ python fix_tests.py path/to/repo
4
+
5
+ lcode may edit files, and run the test command only: every other shell command is refused,
6
+ because nobody is there to ask.
7
+ """
8
+
9
+ import sys
10
+
11
+ from lcode import Session
12
+
13
+ TEST_COMMANDS = ("pytest", "python -m pytest", "python3 -m pytest")
14
+
15
+
16
+ def approve(request: dict) -> bool:
17
+ return request["kind"] == "bash" and request["target"].startswith(TEST_COMMANDS)
18
+
19
+
20
+ def main() -> int:
21
+ repo = sys.argv[1] if len(sys.argv) > 1 else "."
22
+ with Session(repo, permission_mode="auto-edit", approve=approve, max_steps=40) as session:
23
+ for event in session.stream("Run the tests, fix the code until they pass, and run them again."):
24
+ if event["type"] == "tool_result":
25
+ mark = "✗" if event["error"] else "✓"
26
+ print(f"{mark} {event['name']}: {event['output'].splitlines()[0][:80] if event['output'] else ''}")
27
+ elif event["type"] == "result":
28
+ print(f"\n{event['status']} in {event['seconds']:.0f}s, {event['usage']['requests']} model requests")
29
+ for change in event["files_changed"]:
30
+ print(f" {change['status']} {change['path']}")
31
+ print(f"\n{event['text']}")
32
+ return 0 if event["status"] == "success" else 1
33
+ return 1
34
+
35
+
36
+ if __name__ == "__main__":
37
+ sys.exit(main())
@@ -95,6 +95,8 @@ nav:
95
95
  - Memory: memory.md
96
96
  - Subagents: agents.md
97
97
  - Commands and skills: commands.md
98
+ - Git workflow: git.md
99
+ - Automation: automation.md
98
100
  - Code intelligence: lsp.md
99
101
  - Repository map and code search: search.md
100
102
  - Models & context windows: models.md
@@ -84,6 +84,8 @@ ignore = [
84
84
  "src/lcode/codesearch.py" = ["E501"]
85
85
  "src/lcode/bench_backend.py" = ["E501"]
86
86
  "src/lcode/backends.py" = ["E501"]
87
+ "src/lcode/gitflow.py" = ["E501"]
88
+ "src/lcode/action.py" = ["E501"]
87
89
 
88
90
  [tool.pytest.ini_options]
89
91
  testpaths = ["tests"]
@@ -0,0 +1,16 @@
1
+ """lcode — a local-first terminal coding agent powered by open-weight models via Ollama.
2
+
3
+ From a program: `from lcode import Session` (see lcode.api).
4
+ """
5
+
6
+ __version__ = "0.18.0"
7
+
8
+ API = ("Session", "Result", "Options", "SetupError")
9
+
10
+
11
+ def __getattr__(name: str):
12
+ if name in API: # loaded on first use, so `import lcode` stays fast
13
+ from lcode import api
14
+
15
+ return getattr(api, name)
16
+ raise AttributeError(f"module 'lcode' has no attribute {name!r}")