lcode-cli 0.20.0__tar.gz → 0.22.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.
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/CHANGELOG.md +27 -1
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/PKG-INFO +3 -2
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/README.md +2 -1
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/installation.md +28 -3
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/mcp.md +31 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/usage.md +1 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/pyproject.toml +3 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/cli.py +17 -1
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/catalog.toml +21 -0
- lcode_cli-0.22.0/src/lcode/servers/xai.py +289 -0
- lcode_cli-0.22.0/src/lcode/update.py +557 -0
- lcode_cli-0.22.0/tests/test_update.py +303 -0
- lcode_cli-0.22.0/tests/test_xai.py +163 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.editorconfig +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.gitignore +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/AGENTS.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/LICENSE +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/SECURITY.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/action.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/agents.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/assets/logo.svg +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/assets/social-preview.png +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/automation.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/bench.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/changelog.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/commands.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/configuration.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/contributing.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/editors.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/git.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/hooks.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/index.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/lsp.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/memory.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/models.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/search.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/servers.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/examples/README.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/install.sh +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/mkdocs.yml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/overrides/main.html +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/agent.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/api.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/backends.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/config.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/context.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/repl.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/tools.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/conftest.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_action.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_api.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_backends.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_config.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_context.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/tests/test_web.py +0 -0
- {lcode_cli-0.20.0 → lcode_cli-0.22.0}/uv.lock +0 -0
|
@@ -6,6 +6,30 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.22.0] - 2026-10-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `lcode mcp add xai`: images, image edits, videos and speech with xAI's Grok Imagine and text to
|
|
14
|
+
speech APIs, in the cloud and billed to your xAI account. It asks for your xAI API key, and runs
|
|
15
|
+
`lcode-mcp-xai`, a new MCP server that ships with lcode, with the tools `generate_image`,
|
|
16
|
+
`edit_image`, `generate_video` (waits for the video and downloads it), `video_status` and
|
|
17
|
+
`text_to_speech`. Files are saved in `generated/` in the project or where you ask. See
|
|
18
|
+
[Images, videos and speech with xAI](https://nasser1941.github.io/lcode/mcp/#images-videos-and-speech-with-xai).
|
|
19
|
+
|
|
20
|
+
## [0.21.0] - 2026-10-06
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `lcode update` updates lcode the way it was installed (uv tool, pipx, Homebrew, pip, or
|
|
25
|
+
`git pull` for a checkout), and Ollama when it runs in Docker. The newest Ollama release comes
|
|
26
|
+
from Docker Hub, not a local `latest` tag, and is pulled by its version tag (such as
|
|
27
|
+
`ollama/ollama:0.35.1`) so `docker ps` shows the version. A container from Docker Compose gets
|
|
28
|
+
the new tag in its compose file and is recreated with `docker compose up`; any other container is
|
|
29
|
+
recreated with the same settings, and the old one comes back if the new one doesn't start.
|
|
30
|
+
`lcode update --check` only shows which versions are out. See
|
|
31
|
+
[Updating](https://nasser1941.github.io/lcode/installation/#updating).
|
|
32
|
+
|
|
9
33
|
## [0.20.0] - 2026-10-06
|
|
10
34
|
|
|
11
35
|
### Added
|
|
@@ -467,7 +491,9 @@ First public release.
|
|
|
467
491
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
468
492
|
- One-line installer for Linux and macOS.
|
|
469
493
|
|
|
470
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
494
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.22.0...HEAD
|
|
495
|
+
[0.22.0]: https://github.com/nasser1941/lcode/compare/v0.21.0...v0.22.0
|
|
496
|
+
[0.21.0]: https://github.com/nasser1941/lcode/compare/v0.20.0...v0.21.0
|
|
471
497
|
[0.20.0]: https://github.com/nasser1941/lcode/compare/v0.19.0...v0.20.0
|
|
472
498
|
[0.19.0]: https://github.com/nasser1941/lcode/compare/v0.18.0...v0.19.0
|
|
473
499
|
[0.18.0]: https://github.com/nasser1941/lcode/compare/v0.17.0...v0.18.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.22.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/
|
|
@@ -116,7 +116,8 @@ through [Ollama](https://ollama.com), and when it needs current information it c
|
|
|
116
116
|
(`lcode mcp add atlassian`), image generation with ComfyUI, or any other MCP server. Browser sign-in (OAuth) is built in.
|
|
117
117
|
- **Sees images.** Attach a screenshot or mockup with `@path` and lcode looks at it, using the
|
|
118
118
|
vision part of your model. With ComfyUI (`lcode mcp add comfyui`) it can also generate and edit
|
|
119
|
-
images with local models such as FLUX, SDXL and Qwen-Image
|
|
119
|
+
images with local models such as FLUX, SDXL and Qwen-Image, or, in the cloud, images, videos
|
|
120
|
+
and speech with xAI's Grok Imagine (`lcode mcp add xai`).
|
|
120
121
|
- **Web search when needed.** Looks up the latest versions, docs and error messages with Ollama web
|
|
121
122
|
search, Brave, Tavily or your own SearXNG, and reads pages as clean text.
|
|
122
123
|
- **Private.** The model runs on your machine, lcode never uploads your files and has no telemetry.
|
|
@@ -80,7 +80,8 @@ through [Ollama](https://ollama.com), and when it needs current information it c
|
|
|
80
80
|
(`lcode mcp add atlassian`), image generation with ComfyUI, or any other MCP server. Browser sign-in (OAuth) is built in.
|
|
81
81
|
- **Sees images.** Attach a screenshot or mockup with `@path` and lcode looks at it, using the
|
|
82
82
|
vision part of your model. With ComfyUI (`lcode mcp add comfyui`) it can also generate and edit
|
|
83
|
-
images with local models such as FLUX, SDXL and Qwen-Image
|
|
83
|
+
images with local models such as FLUX, SDXL and Qwen-Image, or, in the cloud, images, videos
|
|
84
|
+
and speech with xAI's Grok Imagine (`lcode mcp add xai`).
|
|
84
85
|
- **Web search when needed.** Looks up the latest versions, docs and error messages with Ollama web
|
|
85
86
|
search, Brave, Tavily or your own SearXNG, and reads pages as clean text.
|
|
86
87
|
- **Private.** The model runs on your machine, lcode never uploads your files and has no telemetry.
|
|
@@ -161,11 +161,36 @@ lmstudio` (or `llama.cpp`, `vllm`, `mlx`). See [Other model servers](servers.md)
|
|
|
161
161
|
Use [WSL2](https://learn.microsoft.com/windows/wsl/install) with Ubuntu and follow the Ubuntu
|
|
162
162
|
steps. NVIDIA GPUs work inside WSL2 with the regular Windows driver.
|
|
163
163
|
|
|
164
|
-
##
|
|
164
|
+
## Updating
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
lcode update # update lcode, and Ollama if it runs in Docker
|
|
168
|
+
lcode update --check # only show which versions are out
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- **lcode** updates the way you installed it: `uv tool upgrade`, `pipx upgrade`, `brew upgrade`
|
|
172
|
+
or `pip`. If it runs from a git checkout, `git pull` (on `main`, without local changes).
|
|
173
|
+
- **Ollama in a Docker container** is updated to the newest release on Docker Hub. lcode looks
|
|
174
|
+
that up instead of trusting a local `latest` tag, which can be months old, and skips release
|
|
175
|
+
candidates. It pulls the version's own tag, such as `ollama/ollama:0.35.1`, so `docker ps` shows
|
|
176
|
+
which version runs. A ROCm image stays ROCm.
|
|
177
|
+
- **A container from Docker Compose:** lcode changes the `image:` line in its compose file and
|
|
178
|
+
runs `docker compose up -d` for that service, so Compose keeps managing it.
|
|
179
|
+
- **Any other container** is recreated with the same name, ports, volumes, environment, GPUs,
|
|
180
|
+
network and restart policy. The old one is kept until the new one answers with the new
|
|
181
|
+
version, and is put back if it doesn't.
|
|
182
|
+
- Your models stay: they're in the container's volume.
|
|
183
|
+
- **Ollama installed without Docker** gets the command that updates it: on Linux,
|
|
184
|
+
`curl -fsSL https://ollama.com/install.sh | sh`; on macOS, `brew upgrade ollama` or the app
|
|
185
|
+
itself.
|
|
186
|
+
|
|
187
|
+
lcode shows what it will do and asks first. `lcode update lcode` or `lcode update ollama` updates
|
|
188
|
+
only one; `-y` doesn't ask.
|
|
189
|
+
|
|
190
|
+
## Uninstall
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
uv tool uninstall lcode-cli # remove lcode (pipx: pipx uninstall lcode-cli; Homebrew: brew uninstall lcode)
|
|
169
194
|
rm -rf ~/.config/lcode ~/.local/state/lcode # remove settings and saved sessions
|
|
170
195
|
ollama rm lcode-qwen3.6-35b qwen3.6:35b-a3b-coding # remove downloaded models
|
|
171
196
|
```
|
|
@@ -29,6 +29,7 @@ and find the code for the top one."*
|
|
|
29
29
|
| `github` | Repositories, issues, pull requests, Actions | a GitHub token (or the GitHub CLI) |
|
|
30
30
|
| `playwright` | A real (headless) browser: open pages, click, fill forms | `npx` |
|
|
31
31
|
| `comfyui` | Generate and edit images with local models (FLUX, SDXL, SD 1.5, Qwen-Image) | `uvx` and [ComfyUI](#images-with-comfyui) running locally |
|
|
32
|
+
| `xai` | Generate and edit images, and generate videos and speech, with xAI's Grok Imagine (paid, in the cloud) | `uvx`, an xAI API key ([below](#images-videos-and-speech-with-xai)) |
|
|
32
33
|
| `context7` | Up-to-date docs and examples for thousands of libraries | nothing (an API key is optional) |
|
|
33
34
|
| `sentry` | Errors, issues, traces and releases | a Sentry account (browser sign-in) |
|
|
34
35
|
| `postgres` | Schemas, read-only queries, query performance | `uvx`, a connection URL |
|
|
@@ -137,6 +138,36 @@ size, prompt), which can take several minutes of trial and error. Once an image
|
|
|
137
138
|
lcode to save that workflow in your project (for example `assets/icon.workflow.json`) and reuse it:
|
|
138
139
|
later images are a single `run_workflow` call.
|
|
139
140
|
|
|
141
|
+
### Images, videos and speech with xAI
|
|
142
|
+
|
|
143
|
+
xAI's [Grok Imagine](https://docs.x.ai/developers/model-capabilities/imagine) and
|
|
144
|
+
[text to speech](https://docs.x.ai/developers/models/text-to-speech) APIs make images, videos and
|
|
145
|
+
speech in the cloud: no GPU needed, but each call is billed to your xAI account. lcode ships an MCP
|
|
146
|
+
server for them, `lcode-mcp-xai`.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
lcode mcp add xai # asks for your xAI API key (create one at https://console.x.ai)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The key is typed hidden and kept in `~/.config/lcode/mcp.json`, readable only by you. If
|
|
153
|
+
`XAI_API_KEY` is already set in your environment, lcode uses that and doesn't store the key.
|
|
154
|
+
|
|
155
|
+
| Tool | | Costs |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `generate_image` | Images from a description: 1–10 at a time, aspect ratio, 1k or 2k | $0.02–0.05 an image |
|
|
158
|
+
| `edit_image` | Change a local image as described | the same |
|
|
159
|
+
| `generate_video` | A 1–15 second MP4 from a description, optionally starting from a local image, up to 1080p. Waits for it (a minute or more) | $0.02–0.08 a second |
|
|
160
|
+
| `video_status` | Fetch a video that wasn't ready within 10 minutes | |
|
|
161
|
+
| `text_to_speech` | Speech in one of 26 voices and 20 languages, as MP3 or WAV | per character |
|
|
162
|
+
|
|
163
|
+
Files are saved in `generated/` in the project, or where you ask (*"generate an app icon and save
|
|
164
|
+
it as assets/icon.png"*). xAI has no music generation API.
|
|
165
|
+
|
|
166
|
+
Your prompts, and the images you edit or animate, go to xAI. lcode asks before each call, since each
|
|
167
|
+
one costs money. To stop asking for some tools, list them under `"allow"` for the server in
|
|
168
|
+
`mcp.json`, for example `"allow": ["generate_image", "text_to_speech"]`. xAI applies its own usage
|
|
169
|
+
policies to what it generates; when it refuses a prompt, lcode passes its message to the model.
|
|
170
|
+
|
|
140
171
|
### Metabase
|
|
141
172
|
|
|
142
173
|
Metabase has its own MCP server inside every instance (Metabase 60 and later), at
|
|
@@ -11,6 +11,7 @@ lcode config [set|unset] # show or change settings
|
|
|
11
11
|
lcode bench [models] # score models on small coding tasks on this machine
|
|
12
12
|
lcode mcp [catalog|add|list] # connect MCP servers: Jira, GitHub, AWS, databases… (see MCP servers)
|
|
13
13
|
lcode acp # run inside an editor over the Agent Client Protocol (see Editors)
|
|
14
|
+
lcode update [--check] # update lcode, and Ollama when it runs in Docker (see Installation)
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
| Option | Meaning |
|
|
@@ -49,6 +49,7 @@ Changelog = "https://github.com/nasser1941/lcode/blob/main/CHANGELOG.md"
|
|
|
49
49
|
lcode = "lcode.cli:main"
|
|
50
50
|
lcode-mcp-valohai = "lcode.servers.valohai:main"
|
|
51
51
|
lcode-mcp-encord = "lcode.servers.encord:main"
|
|
52
|
+
lcode-mcp-xai = "lcode.servers.xai:main"
|
|
52
53
|
|
|
53
54
|
[dependency-groups]
|
|
54
55
|
dev = ["pytest>=8", "ruff>=0.6"]
|
|
@@ -87,6 +88,8 @@ ignore = [
|
|
|
87
88
|
"src/lcode/gitflow.py" = ["E501"]
|
|
88
89
|
"src/lcode/action.py" = ["E501"]
|
|
89
90
|
"src/lcode/acp.py" = ["E501"]
|
|
91
|
+
"src/lcode/update.py" = ["E501"]
|
|
92
|
+
"src/lcode/servers/xai.py" = ["E501"]
|
|
90
93
|
|
|
91
94
|
[tool.pytest.ini_options]
|
|
92
95
|
testpaths = ["tests"]
|
|
@@ -678,7 +678,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
678
678
|
parser = argparse.ArgumentParser(
|
|
679
679
|
prog="lcode",
|
|
680
680
|
description="A local-first terminal coding agent powered by open-weight models via Ollama.",
|
|
681
|
-
epilog="Subcommands: lcode setup | models | doctor | config | bench | mcp | index "
|
|
681
|
+
epilog="Subcommands: lcode setup | models | doctor | config | bench | mcp | index | update "
|
|
682
682
|
"(lcode <subcommand> --help). "
|
|
683
683
|
"Docs: https://nasser1941.github.io/lcode/",
|
|
684
684
|
)
|
|
@@ -774,6 +774,15 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
|
|
|
774
774
|
from lcode.mcp.commands import build_parser as mcp_parser
|
|
775
775
|
|
|
776
776
|
subs["mcp"] = mcp_parser()
|
|
777
|
+
p = argparse.ArgumentParser(
|
|
778
|
+
prog="lcode update",
|
|
779
|
+
description="Update lcode, and Ollama when it runs in Docker (to the newest version on Docker Hub, pinned "
|
|
780
|
+
"to its version tag).",
|
|
781
|
+
)
|
|
782
|
+
p.add_argument("what", nargs="?", choices=["lcode", "ollama"], help="only this one (default: both)")
|
|
783
|
+
p.add_argument("--check", action="store_true", help="only show which versions are out")
|
|
784
|
+
p.add_argument("-y", "--yes", action="store_true", help="don't ask before updating")
|
|
785
|
+
subs["update"] = p
|
|
777
786
|
subs["acp"] = argparse.ArgumentParser(
|
|
778
787
|
prog="lcode acp",
|
|
779
788
|
description="Run as the agent inside an editor, over the Agent Client Protocol on stdin and stdout "
|
|
@@ -787,6 +796,12 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
|
|
|
787
796
|
return subs
|
|
788
797
|
|
|
789
798
|
|
|
799
|
+
def cmd_update(args) -> None:
|
|
800
|
+
from lcode import update
|
|
801
|
+
|
|
802
|
+
sys.exit(update.command(console, args.what or "", check=args.check, yes=args.yes))
|
|
803
|
+
|
|
804
|
+
|
|
790
805
|
def cmd_acp(args) -> None:
|
|
791
806
|
from lcode import acp
|
|
792
807
|
|
|
@@ -863,6 +878,7 @@ COMMANDS = {
|
|
|
863
878
|
"index": cmd_index,
|
|
864
879
|
"action": cmd_action,
|
|
865
880
|
"acp": cmd_acp,
|
|
881
|
+
"update": cmd_update,
|
|
866
882
|
}
|
|
867
883
|
|
|
868
884
|
|
|
@@ -281,3 +281,24 @@ secret = true
|
|
|
281
281
|
var = "VALOHAI_HOST"
|
|
282
282
|
prompt = "Valohai address (Enter for app.valohai.com; for a self-hosted installation, its URL)"
|
|
283
283
|
optional = true
|
|
284
|
+
|
|
285
|
+
[xai]
|
|
286
|
+
name = "xAI Grok Imagine"
|
|
287
|
+
description = "Generate and edit images, and generate videos and speech, with xAI's Grok Imagine and voice API (paid, in the cloud)"
|
|
288
|
+
homepage = "https://nasser1941.github.io/lcode/mcp/#xai"
|
|
289
|
+
requires = ["uvx"]
|
|
290
|
+
setup = """Uses xAI's API: your prompts, and images you edit or animate, go to xAI, and every call is billed to your xAI
|
|
291
|
+
account (an image $0.02-0.05, video $0.02-0.08 a second, speech per character). Create an API key at https://console.x.ai.
|
|
292
|
+
Files are saved in generated/ in the project. lcode asks before each call; to stop asking for some tools, list them
|
|
293
|
+
under "allow" for this server in mcp.json. xAI has no music generation."""
|
|
294
|
+
|
|
295
|
+
[xai.server]
|
|
296
|
+
command = "uvx"
|
|
297
|
+
args = ["--isolated", "--python", "3.12", "--from", "lcode-cli", "lcode-mcp-xai"]
|
|
298
|
+
env = { XAI_API_KEY = "${XAI_API_KEY}" }
|
|
299
|
+
timeout = 900 # a video takes minutes
|
|
300
|
+
|
|
301
|
+
[[xai.inputs]]
|
|
302
|
+
var = "XAI_API_KEY"
|
|
303
|
+
prompt = "xAI API key (create one at https://console.x.ai)"
|
|
304
|
+
secret = true
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""An MCP server for xAI's Grok Imagine and voice APIs: images, image edits, videos and speech.
|
|
2
|
+
|
|
3
|
+
xAI has an HTTP API but no MCP server, so lcode ships this one. It needs an API key (XAI_API_KEY,
|
|
4
|
+
from https://console.x.ai) and saves what it makes in the project, in `generated/` unless a path is
|
|
5
|
+
given. The prompts, and any image it edits or animates, go to xAI, which bills per image, per
|
|
6
|
+
second of video and per character of speech, and applies its own usage policies.
|
|
7
|
+
|
|
8
|
+
Run: lcode-mcp-xai (lcode mcp add xai sets it up)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import base64
|
|
15
|
+
import mimetypes
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
from lcode import __version__
|
|
24
|
+
from lcode.mcp.server import Server, ToolFailure
|
|
25
|
+
|
|
26
|
+
API = "https://api.x.ai/v1"
|
|
27
|
+
TIMEOUT = 120
|
|
28
|
+
IMAGE_MODELS = ["grok-imagine-image-2.0", "grok-imagine-image-quality", "grok-imagine-image"]
|
|
29
|
+
VIDEO_MODELS = ["grok-imagine-video-1.5", "grok-imagine-video-1.5-lite", "grok-imagine-video"]
|
|
30
|
+
ASPECT_RATIOS = ["auto", "1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "2:1", "1:2", "21:9"]
|
|
31
|
+
VOICES = ["eve", "ara", "leo", "rex", "sal", "luna", "iris", "orion", "helix", "zagan", "carina", "altair", "zenith",
|
|
32
|
+
"perseus", "helios", "lux", "kepler", "rigel", "cosmo", "celeste", "ursa", "sirius", "lumen", "castor",
|
|
33
|
+
"naksh", "atlas"] # fmt: skip
|
|
34
|
+
AUDIO_FORMATS = {"mp3": ".mp3", "wav": ".wav"}
|
|
35
|
+
VIDEO_WAIT = 600 # seconds generate_video waits before handing back the request id
|
|
36
|
+
POLL = 5
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class XAI:
|
|
40
|
+
def __init__(self, key: str, base: str = API, output: Path | None = None):
|
|
41
|
+
self.base = base.rstrip("/")
|
|
42
|
+
self.output = output or Path("generated")
|
|
43
|
+
self.session = requests.Session()
|
|
44
|
+
self.session.headers.update({"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
|
|
45
|
+
|
|
46
|
+
def request(self, method: str, path: str, **kwargs) -> requests.Response:
|
|
47
|
+
try:
|
|
48
|
+
r = self.session.request(method, f"{self.base}{path}", timeout=TIMEOUT, **kwargs)
|
|
49
|
+
except requests.RequestException as e:
|
|
50
|
+
raise ToolFailure(f"can't reach xAI: {e}") from e
|
|
51
|
+
if r.status_code in (401, 403):
|
|
52
|
+
raise ToolFailure(
|
|
53
|
+
f"xAI refused the request ({r.status_code}): {_error(r)}. Check XAI_API_KEY (console.x.ai)."
|
|
54
|
+
)
|
|
55
|
+
if r.status_code == 429:
|
|
56
|
+
raise ToolFailure(f"xAI's rate limit or credit limit was reached: {_error(r)}")
|
|
57
|
+
if r.status_code >= 400:
|
|
58
|
+
raise ToolFailure(f"xAI error {r.status_code}: {_error(r)}")
|
|
59
|
+
return r
|
|
60
|
+
|
|
61
|
+
def post(self, path: str, body: dict) -> dict:
|
|
62
|
+
return self.request("POST", path, json=body).json()
|
|
63
|
+
|
|
64
|
+
def download(self, url: str) -> bytes:
|
|
65
|
+
try:
|
|
66
|
+
r = requests.get(url, timeout=TIMEOUT)
|
|
67
|
+
r.raise_for_status()
|
|
68
|
+
except requests.RequestException as e:
|
|
69
|
+
raise ToolFailure(f"couldn't download the result from xAI: {e}") from e
|
|
70
|
+
return r.content
|
|
71
|
+
|
|
72
|
+
def target(self, path: str, stem: str, suffix: str) -> Path:
|
|
73
|
+
"""Where to save: the given path, or a new file in the output folder."""
|
|
74
|
+
if path:
|
|
75
|
+
target = Path(path).expanduser()
|
|
76
|
+
if not target.suffix:
|
|
77
|
+
target = target.with_suffix(suffix)
|
|
78
|
+
else:
|
|
79
|
+
name = re.sub(r"[^a-z0-9]+", "-", stem.lower()).strip("-")[:40] or "xai"
|
|
80
|
+
target = self.output / f"{name}-{time.strftime('%Y%m%d-%H%M%S')}{suffix}"
|
|
81
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
n, unique = 2, target
|
|
83
|
+
while unique.exists():
|
|
84
|
+
unique = target.with_name(f"{target.stem}-{n}{target.suffix}")
|
|
85
|
+
n += 1
|
|
86
|
+
return unique
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _error(r: requests.Response) -> str:
|
|
90
|
+
try:
|
|
91
|
+
data = r.json()
|
|
92
|
+
except ValueError:
|
|
93
|
+
return r.text[:300]
|
|
94
|
+
error = data.get("error") if isinstance(data, dict) else None
|
|
95
|
+
if isinstance(error, dict):
|
|
96
|
+
return str(error.get("message") or error)[:300]
|
|
97
|
+
return str(error or data)[:300]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def data_url(path: str) -> str:
|
|
101
|
+
"""A local image as a data: URL, for edits and image-to-video."""
|
|
102
|
+
file = Path(path).expanduser()
|
|
103
|
+
if not file.is_file():
|
|
104
|
+
raise ToolFailure(f"{path} doesn't exist")
|
|
105
|
+
mime = mimetypes.guess_type(file.name)[0] or "image/png"
|
|
106
|
+
if mime not in ("image/png", "image/jpeg", "image/webp"):
|
|
107
|
+
raise ToolFailure(f"{path} isn't a PNG, JPEG or WebP image")
|
|
108
|
+
return f"data:{mime};base64,{base64.b64encode(file.read_bytes()).decode()}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def image_suffix(data: bytes) -> str:
|
|
112
|
+
if data[:3] == b"\xff\xd8\xff":
|
|
113
|
+
return ".jpg"
|
|
114
|
+
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
115
|
+
return ".webp"
|
|
116
|
+
return ".png"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def save_images(api: XAI, reply: dict, prompt: str, path: str) -> list[Path]:
|
|
120
|
+
saved = []
|
|
121
|
+
items = reply.get("data") or []
|
|
122
|
+
if not items:
|
|
123
|
+
raise ToolFailure(f"xAI returned no image: {str(reply)[:300]}")
|
|
124
|
+
for i, item in enumerate(items):
|
|
125
|
+
data = base64.b64decode(item["b64_json"]) if item.get("b64_json") else api.download(item["url"])
|
|
126
|
+
many = path and len(items) > 1
|
|
127
|
+
target = api.target(f"{Path(path).with_suffix('')}-{i + 1}" if many else path, prompt, image_suffix(data))
|
|
128
|
+
target.write_bytes(data)
|
|
129
|
+
saved.append(target)
|
|
130
|
+
return saved
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build(api: XAI) -> Server:
|
|
134
|
+
server = Server(
|
|
135
|
+
"xai",
|
|
136
|
+
__version__,
|
|
137
|
+
"xAI's Grok Imagine and voice: generate and edit images, generate videos (from text or a start image) "
|
|
138
|
+
"and speech. Results are saved as files in the project; each call costs money on the user's xAI account. "
|
|
139
|
+
"xAI has no music generation.",
|
|
140
|
+
)
|
|
141
|
+
path_arg = {"type": "string", "description": "Where to save it (default: a new file in generated/)"}
|
|
142
|
+
|
|
143
|
+
@server.tool(
|
|
144
|
+
"generate_image",
|
|
145
|
+
"Generate images from a description with Grok Imagine and save them. Costs $0.02–0.05 per image.",
|
|
146
|
+
{
|
|
147
|
+
"prompt": {"type": "string", "description": "What the image shows, in detail"},
|
|
148
|
+
"n": {"type": "integer", "description": "How many images, 1–10 (default 1)"},
|
|
149
|
+
"aspect_ratio": {"type": "string", "enum": ASPECT_RATIOS, "description": "Default auto"},
|
|
150
|
+
"resolution": {"type": "string", "enum": ["1k", "2k"], "description": "Default 1k"},
|
|
151
|
+
"model": {"type": "string", "enum": IMAGE_MODELS, "description": f"Default {IMAGE_MODELS[0]}"},
|
|
152
|
+
"path": path_arg,
|
|
153
|
+
},
|
|
154
|
+
["prompt"],
|
|
155
|
+
)
|
|
156
|
+
def generate_image(prompt: str, n: int = 1, aspect_ratio: str = "auto", resolution: str = "1k",
|
|
157
|
+
model: str = IMAGE_MODELS[0], path: str = "") -> str: # fmt: skip
|
|
158
|
+
body = {"model": model, "prompt": prompt, "n": max(1, min(10, int(n))), "response_format": "b64_json"}
|
|
159
|
+
if aspect_ratio and aspect_ratio != "auto":
|
|
160
|
+
body["aspect_ratio"] = aspect_ratio
|
|
161
|
+
if resolution and resolution != "1k":
|
|
162
|
+
body["resolution"] = resolution
|
|
163
|
+
saved = save_images(api, api.post("/images/generations", body), prompt, path)
|
|
164
|
+
return f"Saved {len(saved)} image(s) from {model}:\n" + "\n".join(f"- {p}" for p in saved)
|
|
165
|
+
|
|
166
|
+
@server.tool(
|
|
167
|
+
"edit_image",
|
|
168
|
+
"Change an existing image (a local file) as described, with Grok Imagine, and save the result.",
|
|
169
|
+
{
|
|
170
|
+
"image": {"type": "string", "description": "Path of the image to edit (PNG, JPEG or WebP)"},
|
|
171
|
+
"prompt": {"type": "string", "description": "What to change"},
|
|
172
|
+
"model": {"type": "string", "enum": IMAGE_MODELS, "description": f"Default {IMAGE_MODELS[0]}"},
|
|
173
|
+
"path": path_arg,
|
|
174
|
+
},
|
|
175
|
+
["image", "prompt"],
|
|
176
|
+
)
|
|
177
|
+
def edit_image(image: str, prompt: str, model: str = IMAGE_MODELS[0], path: str = "") -> str:
|
|
178
|
+
body = {"model": model, "prompt": prompt, "image": {"url": data_url(image), "type": "image_url"},
|
|
179
|
+
"response_format": "b64_json"} # fmt: skip
|
|
180
|
+
saved = save_images(api, api.post("/images/edits", body), prompt, path)
|
|
181
|
+
return f"Saved the edited image: {saved[0]}"
|
|
182
|
+
|
|
183
|
+
def finish_video(request_id: str, prompt: str, path: str, wait: float) -> str:
|
|
184
|
+
deadline = time.monotonic() + wait
|
|
185
|
+
while True:
|
|
186
|
+
state = api.request("GET", f"/videos/{request_id}").json()
|
|
187
|
+
status = state.get("status")
|
|
188
|
+
if status == "done":
|
|
189
|
+
video = state.get("video") or {}
|
|
190
|
+
target = api.target(path, prompt or request_id, ".mp4")
|
|
191
|
+
target.write_bytes(api.download(video["url"]))
|
|
192
|
+
return f"Saved the video ({video.get('duration', '?')} s, {state.get('model', '')}): {target}"
|
|
193
|
+
if status in ("failed", "expired"):
|
|
194
|
+
raise ToolFailure(f"xAI couldn't make the video ({status}): {state.get('error') or state}")
|
|
195
|
+
if time.monotonic() > deadline:
|
|
196
|
+
return (
|
|
197
|
+
f"The video isn't ready yet (request {request_id}). Check later with "
|
|
198
|
+
f'video_status(request_id="{request_id}"), which downloads it when it\'s done.'
|
|
199
|
+
)
|
|
200
|
+
time.sleep(POLL)
|
|
201
|
+
|
|
202
|
+
@server.tool(
|
|
203
|
+
"generate_video",
|
|
204
|
+
"Generate a video with Grok Imagine, from a description and optionally a start image, and save it as "
|
|
205
|
+
"MP4. Takes a minute or more. Costs $0.02–0.08 per second of video.",
|
|
206
|
+
{
|
|
207
|
+
"prompt": {"type": "string", "description": "What happens in the video"},
|
|
208
|
+
"image": {"type": "string", "description": "A local image to start from (optional)"},
|
|
209
|
+
"duration": {"type": "integer", "description": "Seconds, 1–15 (default 8)"},
|
|
210
|
+
"aspect_ratio": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"enum": [r for r in ASPECT_RATIOS if r not in ("auto", "2:1", "1:2", "21:9")],
|
|
213
|
+
},
|
|
214
|
+
"resolution": {"type": "string", "enum": ["480p", "720p", "1080p"], "description": "Default 720p"},
|
|
215
|
+
"model": {"type": "string", "enum": VIDEO_MODELS, "description": f"Default {VIDEO_MODELS[0]}"},
|
|
216
|
+
"path": path_arg,
|
|
217
|
+
},
|
|
218
|
+
["prompt"],
|
|
219
|
+
)
|
|
220
|
+
def generate_video(prompt: str, image: str = "", duration: int = 8, aspect_ratio: str = "16:9",
|
|
221
|
+
resolution: str = "720p", model: str = VIDEO_MODELS[0], path: str = "") -> str: # fmt: skip
|
|
222
|
+
body: dict = {"model": model, "prompt": prompt, "duration": max(1, min(15, int(duration))),
|
|
223
|
+
"aspect_ratio": aspect_ratio, "resolution": resolution} # fmt: skip
|
|
224
|
+
if image:
|
|
225
|
+
body["image"] = {"url": data_url(image)}
|
|
226
|
+
request_id = api.post("/videos/generations", body).get("request_id")
|
|
227
|
+
if not request_id:
|
|
228
|
+
raise ToolFailure("xAI didn't start the video")
|
|
229
|
+
return finish_video(str(request_id), prompt, path, VIDEO_WAIT)
|
|
230
|
+
|
|
231
|
+
@server.tool(
|
|
232
|
+
"video_status",
|
|
233
|
+
"Check a video that generate_video started and that wasn't ready yet; saves it when it's done.",
|
|
234
|
+
{"request_id": {"type": "string"}, "path": path_arg},
|
|
235
|
+
["request_id"],
|
|
236
|
+
)
|
|
237
|
+
def video_status(request_id: str, path: str = "") -> str:
|
|
238
|
+
return finish_video(request_id, "", path, 0)
|
|
239
|
+
|
|
240
|
+
@server.tool(
|
|
241
|
+
"text_to_speech",
|
|
242
|
+
"Turn text into speech with an xAI voice and save it as an audio file. Billed per character.",
|
|
243
|
+
{
|
|
244
|
+
"text": {"type": "string", "description": "What to say (up to 60,000 characters)"},
|
|
245
|
+
"voice": {"type": "string", "enum": VOICES, "description": "Default eve"},
|
|
246
|
+
"language": {"type": "string", "description": "BCP-47 code such as en, de, fr, ja, or auto (default)"},
|
|
247
|
+
"format": {"type": "string", "enum": list(AUDIO_FORMATS), "description": "Default mp3"},
|
|
248
|
+
"speed": {"type": "number", "description": "1.0 is normal"},
|
|
249
|
+
"path": path_arg,
|
|
250
|
+
},
|
|
251
|
+
["text"],
|
|
252
|
+
)
|
|
253
|
+
def text_to_speech(text: str, voice: str = "eve", language: str = "auto", format: str = "mp3",
|
|
254
|
+
speed: float = 1.0, path: str = "") -> str: # fmt: skip
|
|
255
|
+
codec = format if format in AUDIO_FORMATS else "mp3"
|
|
256
|
+
body = {"text": text, "voice_id": voice.lower(), "language": language or "auto",
|
|
257
|
+
"output_format": {"codec": codec}, "speed": float(speed or 1.0)} # fmt: skip
|
|
258
|
+
r = api.request("POST", "/tts", json=body)
|
|
259
|
+
duration = ""
|
|
260
|
+
if "json" in r.headers.get("Content-Type", ""):
|
|
261
|
+
data = r.json()
|
|
262
|
+
audio = base64.b64decode(data.get("audio", ""))
|
|
263
|
+
duration = f", {data['duration']:.1f} s" if isinstance(data.get("duration"), (int, float)) else ""
|
|
264
|
+
else:
|
|
265
|
+
audio = r.content
|
|
266
|
+
if not audio:
|
|
267
|
+
raise ToolFailure("xAI returned no audio")
|
|
268
|
+
target = api.target(path, text[:40], AUDIO_FORMATS[codec])
|
|
269
|
+
target.write_bytes(audio)
|
|
270
|
+
return f"Saved the speech ({voice}{duration}): {target}"
|
|
271
|
+
|
|
272
|
+
return server
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def main(argv: list[str] | None = None) -> None:
|
|
276
|
+
parser = argparse.ArgumentParser(
|
|
277
|
+
prog="lcode-mcp-xai", description="MCP server for xAI's image, video and speech APIs (stdio)."
|
|
278
|
+
)
|
|
279
|
+
parser.add_argument("--output", default=os.environ.get("LCODE_XAI_OUTPUT", "generated"),
|
|
280
|
+
help="folder for the files it makes (default: generated/ in the project)") # fmt: skip
|
|
281
|
+
args = parser.parse_args(argv)
|
|
282
|
+
key = os.environ.get("XAI_API_KEY")
|
|
283
|
+
if not key:
|
|
284
|
+
parser.error("set XAI_API_KEY to an xAI API key (https://console.x.ai)")
|
|
285
|
+
build(XAI(key, os.environ.get("XAI_API_BASE", API), Path(args.output))).serve()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
main()
|