wizolt 0.44.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 (86) hide show
  1. wizolt-0.44.0/LICENSE +28 -0
  2. wizolt-0.44.0/MANIFEST.in +4 -0
  3. wizolt-0.44.0/PKG-INFO +126 -0
  4. wizolt-0.44.0/README.md +79 -0
  5. wizolt-0.44.0/pyproject.toml +105 -0
  6. wizolt-0.44.0/setup.cfg +4 -0
  7. wizolt-0.44.0/wizolt/__init__.py +5 -0
  8. wizolt-0.44.0/wizolt/__main__.py +228 -0
  9. wizolt-0.44.0/wizolt/base.py +637 -0
  10. wizolt-0.44.0/wizolt/builtin_skills/__init__.py +1 -0
  11. wizolt-0.44.0/wizolt/builtin_skills/wizolt-help/SKILL.md +264 -0
  12. wizolt-0.44.0/wizolt/cli/__init__.py +32 -0
  13. wizolt-0.44.0/wizolt/cli/commands.py +1084 -0
  14. wizolt-0.44.0/wizolt/cli/hints.py +102 -0
  15. wizolt-0.44.0/wizolt/cli/loop.py +1354 -0
  16. wizolt-0.44.0/wizolt/cli/modals.py +921 -0
  17. wizolt-0.44.0/wizolt/cli/runtime.py +741 -0
  18. wizolt-0.44.0/wizolt/cli/update.py +136 -0
  19. wizolt-0.44.0/wizolt/cli/view.py +619 -0
  20. wizolt-0.44.0/wizolt/cli/worker.py +319 -0
  21. wizolt-0.44.0/wizolt/compaction.py +444 -0
  22. wizolt-0.44.0/wizolt/config.py +674 -0
  23. wizolt-0.44.0/wizolt/context.py +584 -0
  24. wizolt-0.44.0/wizolt/engine.py +756 -0
  25. wizolt-0.44.0/wizolt/image.py +628 -0
  26. wizolt-0.44.0/wizolt/mcp/__init__.py +8 -0
  27. wizolt-0.44.0/wizolt/mcp/config.py +74 -0
  28. wizolt-0.44.0/wizolt/mcp/manager.py +1008 -0
  29. wizolt-0.44.0/wizolt/mcp/rendering.py +378 -0
  30. wizolt-0.44.0/wizolt/mcp/tokens.py +127 -0
  31. wizolt-0.44.0/wizolt/mentions.py +671 -0
  32. wizolt-0.44.0/wizolt/model/__init__.py +10 -0
  33. wizolt-0.44.0/wizolt/model/anthropic.py +426 -0
  34. wizolt-0.44.0/wizolt/model/chat.py +232 -0
  35. wizolt-0.44.0/wizolt/model/client.py +778 -0
  36. wizolt-0.44.0/wizolt/model/history.py +14 -0
  37. wizolt-0.44.0/wizolt/model/protocol.py +396 -0
  38. wizolt-0.44.0/wizolt/model/resilience.py +252 -0
  39. wizolt-0.44.0/wizolt/model/responses.py +355 -0
  40. wizolt-0.44.0/wizolt/paste.py +42 -0
  41. wizolt-0.44.0/wizolt/prompts.py +184 -0
  42. wizolt-0.44.0/wizolt/providers/__init__.py +1 -0
  43. wizolt-0.44.0/wizolt/providers/catalog.json +2045 -0
  44. wizolt-0.44.0/wizolt/providers/catalog.py +728 -0
  45. wizolt-0.44.0/wizolt/providers/compat.py +691 -0
  46. wizolt-0.44.0/wizolt/providers/schema.py +334 -0
  47. wizolt-0.44.0/wizolt/providers/sync.py +412 -0
  48. wizolt-0.44.0/wizolt/render.py +1853 -0
  49. wizolt-0.44.0/wizolt/runner.py +1016 -0
  50. wizolt-0.44.0/wizolt/session/__init__.py +772 -0
  51. wizolt-0.44.0/wizolt/session/codec.py +590 -0
  52. wizolt-0.44.0/wizolt/session/diffs.py +225 -0
  53. wizolt-0.44.0/wizolt/session/images.py +73 -0
  54. wizolt-0.44.0/wizolt/session/jobs.py +146 -0
  55. wizolt-0.44.0/wizolt/session/queue.py +54 -0
  56. wizolt-0.44.0/wizolt/session/store.py +706 -0
  57. wizolt-0.44.0/wizolt/skill.py +123 -0
  58. wizolt-0.44.0/wizolt/source/__init__.py +57 -0
  59. wizolt-0.44.0/wizolt/source/output.py +301 -0
  60. wizolt-0.44.0/wizolt/source/relocate.py +57 -0
  61. wizolt-0.44.0/wizolt/source/view.py +216 -0
  62. wizolt-0.44.0/wizolt/tools/__init__.py +73 -0
  63. wizolt-0.44.0/wizolt/tools/ask.py +97 -0
  64. wizolt-0.44.0/wizolt/tools/base.py +283 -0
  65. wizolt-0.44.0/wizolt/tools/delegate.py +526 -0
  66. wizolt-0.44.0/wizolt/tools/editplan.py +345 -0
  67. wizolt-0.44.0/wizolt/tools/files.py +1093 -0
  68. wizolt-0.44.0/wizolt/tools/mcp.py +103 -0
  69. wizolt-0.44.0/wizolt/tools/memory.py +446 -0
  70. wizolt-0.44.0/wizolt/tools/search.py +780 -0
  71. wizolt-0.44.0/wizolt/tools/shell.py +643 -0
  72. wizolt-0.44.0/wizolt/tools/skill.py +34 -0
  73. wizolt-0.44.0/wizolt/tools/toolblocks.py +378 -0
  74. wizolt-0.44.0/wizolt/tools/tooloutput.py +253 -0
  75. wizolt-0.44.0/wizolt/tools/toolscript.py +545 -0
  76. wizolt-0.44.0/wizolt/tui/__init__.py +30 -0
  77. wizolt-0.44.0/wizolt/tui/app.py +1797 -0
  78. wizolt-0.44.0/wizolt/tui/scrollback.py +266 -0
  79. wizolt-0.44.0/wizolt/tui/views.py +604 -0
  80. wizolt-0.44.0/wizolt/vision.py +56 -0
  81. wizolt-0.44.0/wizolt.egg-info/PKG-INFO +126 -0
  82. wizolt-0.44.0/wizolt.egg-info/SOURCES.txt +84 -0
  83. wizolt-0.44.0/wizolt.egg-info/dependency_links.txt +1 -0
  84. wizolt-0.44.0/wizolt.egg-info/entry_points.txt +2 -0
  85. wizolt-0.44.0/wizolt.egg-info/requires.txt +19 -0
  86. wizolt-0.44.0/wizolt.egg-info/top_level.txt +1 -0
wizolt-0.44.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, hit9
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT INCLUDING NEGLIGENCE OR OTHERWISE ARISING IN ANY WAY OUT OF THE USE OF
28
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ recursive-include wizolt/builtin_skills *.md
3
+ prune demo*
4
+ prune tests
wizolt-0.44.0/PKG-INFO ADDED
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: wizolt
3
+ Version: 0.44.0
4
+ Summary: A terminal coding agent written in Python
5
+ Author-email: hit9 <hit9@icloud.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/hit9/wizolt
8
+ Project-URL: Repository, https://github.com/hit9/wizolt
9
+ Project-URL: Issues, https://github.com/hit9/wizolt/issues
10
+ Project-URL: Documentation, https://wizolt.readthedocs.io
11
+ Keywords: ai,coding-agent,cli,terminal
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Topic :: Software Development
24
+ Classifier: Topic :: Terminals
25
+ Requires-Python: >=3.11
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: anthropic>=1.0.0
29
+ Requires-Dist: code-symbol-index>=0.5.1
30
+ Requires-Dist: json-repair
31
+ Requires-Dist: fastmcp-slim[client]<4,>=3
32
+ Requires-Dist: httpx2>=2.12.0
33
+ Requires-Dist: openai>=3.0.0
34
+ Requires-Dist: pathspec>=1.0
35
+ Requires-Dist: pillow>=11.0
36
+ Requires-Dist: prompt-toolkit>=3.0.53
37
+ Requires-Dist: rich>=13.0
38
+ Requires-Dist: socksio>=1.0.0
39
+ Requires-Dist: websockets>=15.0.1
40
+ Provides-Extra: dev
41
+ Requires-Dist: pyright>=1.1.411; extra == "dev"
42
+ Requires-Dist: pytest>=8.0; extra == "dev"
43
+ Requires-Dist: pytest-asyncio>=1.0; extra == "dev"
44
+ Requires-Dist: pytest-xdist>=3.8; extra == "dev"
45
+ Requires-Dist: ruff==0.16.0; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ <h1 align="center">
49
+ <img src="https://raw.githubusercontent.com/hit9/wizolt/master/docs/_static/wizolt-logo.png" alt="Wizolt logo: a blue hooded terminal mage holding a lightning staff" width="112"><br>
50
+ wizolt
51
+ </h1>
52
+
53
+ <p align="center">
54
+ <img src="https://raw.githubusercontent.com/hit9/wizolt/master/snapshots/wizolt1.gif" alt="wizolt editing code and running tools" width="600">
55
+ </p>
56
+
57
+ <p align="center">
58
+ A terminal coding agent I use, maintain, and customize, shipped as a self-contained Python package.
59
+ </p>
60
+
61
+ ## Safety
62
+
63
+ **Use at your own risk.** wizolt can edit files and run shell commands in the environment where it starts. It does not provide sandbox isolation; use a container or VM when needed.
64
+
65
+ ## What it is
66
+
67
+ wizolt does not introduce a new kind of coding agent. It combines familiar features — reading and editing files, running commands, follow-ups, sessions, diffs, MCP, and skills — into a tool I use personally.
68
+
69
+ It works on real repositories, including its own: I use wizolt to build and maintain wizolt. Everything ships in one self-contained Python package, so I can change the behavior directly whenever I want the workflow to work differently.
70
+
71
+ Wizolt is the former minacode, which began as the single-file nanocode. The implementation outgrew both earlier names; the project history remains continuous.
72
+
73
+ <p align="center">
74
+ <img src="https://raw.githubusercontent.com/hit9/wizolt/master/snapshots/wizolt2.gif" alt="wizolt resuming a saved session" width="600">
75
+ </p>
76
+ <p align="center"><sub>Resuming a saved session with its conversation and tool history.</sub></p>
77
+
78
+ ## Highlights
79
+
80
+ - **Worker delegation:** hand a bounded task to a second in-process session on its own provider with `/worker`; the `Delegate` tool keeps worker context across delegations until reset.
81
+ - **Forced reply language:** `/language` or `[runtime] language` pins the reply language for the session.
82
+ - **Smarter retries:** exponential backoff with jitter and provider `Retry-After`, shown as a live `retrying` phase with a countdown.
83
+ - **Prompt-cache aware:** stable request prefixes let supported providers reuse work and can reach 90–99% cache hit rates; `/status` shows the reported result.
84
+ - **Code navigation:** jump to definitions, callers, and implementations with a searchable code index.
85
+ - **Live follow-ups:** type while the agent works; `Enter` queues a message for the next model step, while `Ctrl-C` discards a draft or interrupts the task once the input is empty.
86
+ - **Evidence-checked edits:** patch from a numbered source view or exact unique text; stale and ambiguous targets are refused.
87
+ - **Resumable sessions:** conversation, tool calls, diffs, and working memory survive `-c` or `--resume`.
88
+ - **Built-in diff viewer:** `/diff` shows the latest round and the net session result.
89
+ - **MCP and skills:** connect Model Context Protocol servers and load Markdown instruction packs on demand.
90
+ - **Provider-side web search:** opt in to a provider's own search tool (OpenAI, Qwen, Anthropic, Z.AI) and see each search and its sources in the transcript.
91
+ - **Provider compatibility:** OpenAI-compatible APIs and Anthropic.
92
+
93
+ ## Install
94
+
95
+ Requires macOS or Linux, Python 3.11+, and [uv](https://docs.astral.sh/uv/).
96
+
97
+ ```sh
98
+ uv tool install wizolt
99
+ wizolt --init-config
100
+ ```
101
+
102
+ Add your provider to `~/.wizolt/config.toml`:
103
+
104
+ ```toml
105
+ [provider]
106
+ active = "default"
107
+
108
+ [provider.default]
109
+ url = "https://api.deepseek.com"
110
+ key = "sk-..."
111
+ model = "deepseek-v4-flash"
112
+ ```
113
+
114
+ Then run:
115
+
116
+ ```sh
117
+ wizolt
118
+ ```
119
+
120
+ Upgrade with `uv tool upgrade wizolt`.
121
+
122
+ ## Links
123
+
124
+ - [Documentation](https://wizolt.readthedocs.io/en/latest/) — full usage guide and reference.
125
+ - [Blog post](https://hit9.dev/post/nanocode) — why and how it was built.
126
+ - [code-symbol-index](https://github.com/hit9/code-symbol-index) — the code index library wizolt uses.
@@ -0,0 +1,79 @@
1
+ <h1 align="center">
2
+ <img src="https://raw.githubusercontent.com/hit9/wizolt/master/docs/_static/wizolt-logo.png" alt="Wizolt logo: a blue hooded terminal mage holding a lightning staff" width="112"><br>
3
+ wizolt
4
+ </h1>
5
+
6
+ <p align="center">
7
+ <img src="https://raw.githubusercontent.com/hit9/wizolt/master/snapshots/wizolt1.gif" alt="wizolt editing code and running tools" width="600">
8
+ </p>
9
+
10
+ <p align="center">
11
+ A terminal coding agent I use, maintain, and customize, shipped as a self-contained Python package.
12
+ </p>
13
+
14
+ ## Safety
15
+
16
+ **Use at your own risk.** wizolt can edit files and run shell commands in the environment where it starts. It does not provide sandbox isolation; use a container or VM when needed.
17
+
18
+ ## What it is
19
+
20
+ wizolt does not introduce a new kind of coding agent. It combines familiar features — reading and editing files, running commands, follow-ups, sessions, diffs, MCP, and skills — into a tool I use personally.
21
+
22
+ It works on real repositories, including its own: I use wizolt to build and maintain wizolt. Everything ships in one self-contained Python package, so I can change the behavior directly whenever I want the workflow to work differently.
23
+
24
+ Wizolt is the former minacode, which began as the single-file nanocode. The implementation outgrew both earlier names; the project history remains continuous.
25
+
26
+ <p align="center">
27
+ <img src="https://raw.githubusercontent.com/hit9/wizolt/master/snapshots/wizolt2.gif" alt="wizolt resuming a saved session" width="600">
28
+ </p>
29
+ <p align="center"><sub>Resuming a saved session with its conversation and tool history.</sub></p>
30
+
31
+ ## Highlights
32
+
33
+ - **Worker delegation:** hand a bounded task to a second in-process session on its own provider with `/worker`; the `Delegate` tool keeps worker context across delegations until reset.
34
+ - **Forced reply language:** `/language` or `[runtime] language` pins the reply language for the session.
35
+ - **Smarter retries:** exponential backoff with jitter and provider `Retry-After`, shown as a live `retrying` phase with a countdown.
36
+ - **Prompt-cache aware:** stable request prefixes let supported providers reuse work and can reach 90–99% cache hit rates; `/status` shows the reported result.
37
+ - **Code navigation:** jump to definitions, callers, and implementations with a searchable code index.
38
+ - **Live follow-ups:** type while the agent works; `Enter` queues a message for the next model step, while `Ctrl-C` discards a draft or interrupts the task once the input is empty.
39
+ - **Evidence-checked edits:** patch from a numbered source view or exact unique text; stale and ambiguous targets are refused.
40
+ - **Resumable sessions:** conversation, tool calls, diffs, and working memory survive `-c` or `--resume`.
41
+ - **Built-in diff viewer:** `/diff` shows the latest round and the net session result.
42
+ - **MCP and skills:** connect Model Context Protocol servers and load Markdown instruction packs on demand.
43
+ - **Provider-side web search:** opt in to a provider's own search tool (OpenAI, Qwen, Anthropic, Z.AI) and see each search and its sources in the transcript.
44
+ - **Provider compatibility:** OpenAI-compatible APIs and Anthropic.
45
+
46
+ ## Install
47
+
48
+ Requires macOS or Linux, Python 3.11+, and [uv](https://docs.astral.sh/uv/).
49
+
50
+ ```sh
51
+ uv tool install wizolt
52
+ wizolt --init-config
53
+ ```
54
+
55
+ Add your provider to `~/.wizolt/config.toml`:
56
+
57
+ ```toml
58
+ [provider]
59
+ active = "default"
60
+
61
+ [provider.default]
62
+ url = "https://api.deepseek.com"
63
+ key = "sk-..."
64
+ model = "deepseek-v4-flash"
65
+ ```
66
+
67
+ Then run:
68
+
69
+ ```sh
70
+ wizolt
71
+ ```
72
+
73
+ Upgrade with `uv tool upgrade wizolt`.
74
+
75
+ ## Links
76
+
77
+ - [Documentation](https://wizolt.readthedocs.io/en/latest/) — full usage guide and reference.
78
+ - [Blog post](https://hit9.dev/post/nanocode) — why and how it was built.
79
+ - [code-symbol-index](https://github.com/hit9/code-symbol-index) — the code index library wizolt uses.
@@ -0,0 +1,105 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wizolt"
7
+ version = "0.44.0"
8
+ description = "A terminal coding agent written in Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "BSD-3-Clause"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "hit9", email = "hit9@icloud.com" },
15
+ ]
16
+ keywords = ["ai", "coding-agent", "cli", "terminal"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: MacOS",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
29
+ "Topic :: Software Development",
30
+ "Topic :: Terminals",
31
+ ]
32
+
33
+ dependencies = [
34
+ # anthropic 1.x and openai 3.x both moved their HTTP client to httpx2, which stops installing
35
+ # certifi and verifies TLS against the OS trust store instead (SSL_CERT_FILE/SSL_CERT_DIR
36
+ # override it). The floors pin that migration rather than leaving it to the resolver: an older
37
+ # pair would land on plain httpx, a combination retryable_error's transport-error matching is
38
+ # tested against but the rest of the tree no longer is.
39
+ "anthropic>=1.0.0",
40
+ "code-symbol-index>=0.5.1",
41
+ "json-repair",
42
+ "fastmcp-slim[client]>=3,<4",
43
+ # Direct, not transitive: the update check imports httpx2 itself. The provider SDKs bring it
44
+ # in too, but a dependency wizolt imports is wizolt's to declare.
45
+ "httpx2>=2.12.0",
46
+ "openai>=3.0.0",
47
+ "pathspec>=1.0",
48
+ "pillow>=11.0",
49
+ # 3.0.53 fixed Application.invalidate() so it is safe to call from any thread; older versions
50
+ # crash with "no running event loop" when the agent thread asks for a redraw.
51
+ "prompt-toolkit>=3.0.53",
52
+ "rich>=13.0",
53
+ "socksio>=1.0.0",
54
+ "websockets>=15.0.1",
55
+ ]
56
+
57
+ [project.urls]
58
+ Homepage = "https://github.com/hit9/wizolt"
59
+ Repository = "https://github.com/hit9/wizolt"
60
+ Issues = "https://github.com/hit9/wizolt/issues"
61
+ Documentation = "https://wizolt.readthedocs.io"
62
+
63
+ [project.scripts]
64
+ wizolt = "wizolt.__main__:main"
65
+
66
+ [project.optional-dependencies]
67
+ dev = [
68
+ "pyright>=1.1.411",
69
+ "pytest>=8.0",
70
+ "pytest-asyncio>=1.0",
71
+ "pytest-xdist>=3.8",
72
+ "ruff==0.16.0",
73
+ ]
74
+
75
+ [tool.setuptools]
76
+ packages = ["wizolt", "wizolt.builtin_skills", "wizolt.cli", "wizolt.model", "wizolt.mcp", "wizolt.providers", "wizolt.session", "wizolt.source", "wizolt.tools", "wizolt.tui"]
77
+
78
+ [tool.setuptools.package-data]
79
+ wizolt = ["builtin_skills/*/SKILL.md", "providers/catalog.json"]
80
+
81
+ [tool.pytest.ini_options]
82
+ testpaths = ["tests"]
83
+ addopts = ["-n", "auto"]
84
+ # A test that awaits the code under test reads better than the same test wrapped in an
85
+ # asyncio.run() driver, so `async def` tests run without a per-test marker. Each gets its own
86
+ # loop, which is also what the production entry points do.
87
+ asyncio_mode = "auto"
88
+
89
+ [tool.ruff]
90
+ line-length = 160
91
+ target-version = "py311"
92
+
93
+ [tool.ruff.lint]
94
+ extend-select = ["F403"]
95
+ per-file-ignores = { "wizolt/__init__.py" = ["F401"] }
96
+
97
+ [tool.pyright]
98
+ include = ["wizolt"]
99
+ # `uv build` leaves a full copy of the package under build/lib. An editor that analyzes any file in
100
+ # the workspace type-checks that copy as a second definition of every class, so an assignment there
101
+ # reports a mismatch between two same-named types (`ToolRunner* is not assignable to ToolRunner`).
102
+ # `include` alone does not stop it, because the file is analyzed when opened.
103
+ exclude = ["build", "dist", "**/__pycache__", "**/.venv"]
104
+ pythonVersion = "3.11"
105
+ typeCheckingMode = "basic"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """A terminal coding agent."""
2
+
3
+ from wizolt.base import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,228 @@
1
+ """wizolt entry point: command-line argument parsing and dispatch.
2
+
3
+ Invoked through the ``wizolt`` console script or ``python -m wizolt``.
4
+
5
+ Deliberately imports nothing from the wizolt package at module level: argparse, help, version, and
6
+ config initialization should answer before the interactive CLI — prompt_toolkit, the tools, the
7
+ TUI, the session machinery — is imported. The interactive-CLI names `main` needs are reached
8
+ through the lazy `_cli` namespace below: reading `_cli.Session` imports it on first use and caches
9
+ it on this module, so tests can keep substituting fakes here without importing wizolt at module
10
+ load time.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import contextlib
17
+ import importlib
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ import threading
22
+
23
+ # Every interactive-CLI name `main` needs, as (module, attribute). Loaded on first use and cached
24
+ # on this module, so tests can keep substituting fakes here without `import wizolt.__main__`
25
+ # paying for the interactive CLI.
26
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
27
+ "__version__": ("wizolt.base", "__version__"),
28
+ "Agent": ("wizolt.engine", "Agent"),
29
+ "CatalogError": ("wizolt.providers.schema", "CatalogError"),
30
+ "CatalogRuntime": ("wizolt.providers.sync", "CatalogRuntime"),
31
+ "CommandLoop": ("wizolt.cli", "CommandLoop"),
32
+ "Config": ("wizolt.config", "Config"),
33
+ "ConfigError": ("wizolt.base", "ConfigError"),
34
+ "ConfigFile": ("wizolt.config", "ConfigFile"),
35
+ "RuntimeSettings": ("wizolt.config", "RuntimeSettings"),
36
+ "Session": ("wizolt.session", "Session"),
37
+ "Theme": ("wizolt.render", "Theme"),
38
+ "UpdateChecker": ("wizolt.cli.update", "UpdateChecker"),
39
+ "UpdateStatus": ("wizolt.base", "UpdateStatus"),
40
+ "WizoltError": ("wizolt.base", "WizoltError"),
41
+ "configure_logging": ("wizolt.base", "configure_logging"),
42
+ }
43
+
44
+
45
+ def _import_lazy(name: str):
46
+ """Import one interactive-CLI name and cache it on this module (PEP 562)."""
47
+ spec = _LAZY_IMPORTS[name]
48
+ value = getattr(importlib.import_module(spec[0]), spec[1])
49
+ globals()[name] = value
50
+ return value
51
+
52
+
53
+ def __getattr__(name: str):
54
+ """Import a lazy interactive-CLI name on first attribute read (PEP 562)."""
55
+ if name not in _LAZY_IMPORTS:
56
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
57
+ return _import_lazy(name)
58
+
59
+
60
+ class _EntryCli:
61
+ """The entry point's handle on the interactive CLI. Reading an attribute (`_cli.Session`)
62
+ imports that name on first use and caches it on this module, so later reads are plain
63
+ attribute lookups; a name a test has already bound here (a fake) is returned as-is. This is
64
+ the seam that keeps `wizolt.__main__` importable without the interactive CLI while `main`
65
+ still resolves the real classes through module attributes the way it always has."""
66
+
67
+ def __getattr__(self, name: str):
68
+ bound = globals().get(name)
69
+ if bound is not None:
70
+ return bound
71
+ if name not in _LAZY_IMPORTS:
72
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
73
+ return _import_lazy(name)
74
+
75
+
76
+ _cli = _EntryCli()
77
+
78
+
79
+ def run_update() -> int:
80
+ """Check PyPI for a newer wizolt and upgrade it via the detected package manager."""
81
+ print(f"wizolt {_cli.__version__}")
82
+ try:
83
+ latest = _cli.UpdateChecker.fetch_latest_sync()
84
+ except Exception as error: # noqa: BLE001 - update failures from any network/backend layer are reported uniformly.
85
+ print(f"Error: could not check the latest version: {error}", file=sys.stderr)
86
+ return 1
87
+ if not _cli.UpdateStatus(latest=latest).newer_than(_cli.__version__):
88
+ print(f"already up to date ({_cli.__version__})")
89
+ return 0
90
+ command = _cli.UpdateChecker.upgrade_command()
91
+ print(f"updating {_cli.__version__} -> {latest}: {' '.join(command)}")
92
+ try:
93
+ return subprocess.call(command)
94
+ except OSError as error:
95
+ print(f"Error: could not run the upgrade command: {error}", file=sys.stderr)
96
+ return 1
97
+
98
+
99
+ def warm_provider_sdks() -> None:
100
+ """Import the provider SDKs off the main thread so the prompt accepts input immediately.
101
+
102
+ ModelClient imports them lazily because they cost ~0.8s, which was the whole of the delay
103
+ before a fresh prompt echoed keystrokes. Loading them here in the background keeps the prompt
104
+ instant without moving that cost onto the first request: the user's first message takes far
105
+ longer to type than the import takes to finish.
106
+
107
+ Racing this thread against the request path is safe, and deliberately so:
108
+
109
+ - CPython locks imports per module (`importlib._bootstrap._ModuleLock`), so a request-path
110
+ `from openai import OpenAI` that lands mid-warm-up blocks on that module's lock and then
111
+ reads the finished module from `sys.modules`. It cannot observe a half-initialized module,
112
+ and both threads therefore bind the same class object.
113
+ - Per-module locks can deadlock only on an import cycle entered from two threads at once.
114
+ `anthropic` and `openai` do not import each other, and their shared dependencies form a DAG,
115
+ so the lock-wait graph has no cycle. `_DeadlockError` detection is the backstop if that ever
116
+ stops being true.
117
+ - The thread is a daemon because warming must never delay exit. CPython freezes daemon threads
118
+ at finalization rather than letting them run against a torn-down import system, so quitting
119
+ mid-import is silent.
120
+
121
+ Verified by stress test: a barrier-synchronized four-way race and repeated immediate-exit runs
122
+ produce no deadlock, no exception, and no stderr noise.
123
+ """
124
+
125
+ def load() -> None:
126
+ # Warming is only an optimization, and an uncaught failure here would print a thread
127
+ # traceback over the live prompt. Any real problem resurfaces on the request path, which
128
+ # imports the same modules and reports the failure to the user.
129
+ with contextlib.suppress(Exception):
130
+ import anthropic # noqa: F401 - imported for its side effect of populating sys.modules
131
+ import openai # noqa: F401
132
+
133
+ threading.Thread(target=load, name="sdk-warmup", daemon=True).start()
134
+
135
+
136
+ def main(argv: list[str] | None = None) -> int:
137
+ parser = argparse.ArgumentParser(prog="wizolt", epilog="Documentation: https://wizolt.readthedocs.io")
138
+ parser.add_argument("--config", default=None, help="Path to config TOML")
139
+ parser.add_argument("--init-config", action="store_true", help="Create a default config file")
140
+ parser.add_argument("--yolo", action="store_true", help="Skip confirmations for mutating tools")
141
+ parser.add_argument(
142
+ "--theme", choices=["auto", "light", "dark"], default="", help="Color theme (defaults to runtime.theme, then auto-detect via COLORFGBG)"
143
+ )
144
+ resume = parser.add_mutually_exclusive_group()
145
+ resume.add_argument(
146
+ "--resume",
147
+ default="",
148
+ nargs="?",
149
+ const="latest",
150
+ help='Resume a session by UID, uid prefix, or name, or "latest"/"last" for this project\'s most recent',
151
+ )
152
+ resume.add_argument("-c", "--last", "--latest", dest="continue_project", action="store_true", help="Resume the latest session in the current project")
153
+ parser.add_argument("-v", "--version", action="store_true", help="Show version")
154
+ parser.add_argument(
155
+ "command", nargs="?", choices=["update", "upgrade"], default=None, help="Maintenance command: update/upgrade wizolt to the latest version"
156
+ )
157
+ args = parser.parse_args(argv)
158
+ if sys.platform == "win32":
159
+ print("Error: wizolt does not support native Windows; use WSL instead.", file=sys.stderr)
160
+ return 1
161
+ # Cheap exits answer before the interactive CLI is loaded. The explicit update
162
+ # command loads its HTTP/update implementation here, but still never starts a session.
163
+ if args.version:
164
+ print(_cli.__version__)
165
+ return 0
166
+ if args.command in {"update", "upgrade"}:
167
+ return run_update()
168
+ if args.init_config:
169
+ path, created = _cli.ConfigFile.init(args.config)
170
+ print(("Created" if created else "Exists") + " config: " + path)
171
+ return 0
172
+
173
+ # This line needs only the lightweight version module. Put it on screen before importing the
174
+ # session and rendering stacks; the first CommandLoop consumes the handoff, while a session
175
+ # selected later through /resume prints its own banner normally.
176
+ banner_preprinted = sys.stdin.isatty() and sys.stdout.isatty()
177
+ preprinted_output = ""
178
+ if banner_preprinted:
179
+ preprinted_output = f"wizolt {_cli.__version__}. /help for commands.\n\n"
180
+ print(preprinted_output, end="", flush=True)
181
+
182
+ _cli.configure_logging()
183
+ try:
184
+ # Switching sessions ends one run and starts the next rather than re-pointing a live
185
+ # object graph at another Session: everything below is built around one, and this is the
186
+ # only moment nothing is running. Teardown stays in the `finally` that already does it.
187
+ resume = args.resume or ("latest" if args.continue_project else "")
188
+ while True:
189
+ if resume:
190
+ data = _cli.ConfigFile.load(args.config)
191
+ catalog = _cli.CatalogRuntime(_cli.Config.data_dir_from(data))
192
+ config = _cli.Config.from_dict(data, policy=catalog.policy)
193
+ session = _cli.Session.load_snapshot(
194
+ resume,
195
+ config=config,
196
+ settings=_cli.RuntimeSettings.from_dict(data, yolo=args.yolo, theme=args.theme),
197
+ cwd=os.getcwd(),
198
+ catalog=catalog,
199
+ )
200
+ else:
201
+ session = _cli.Session.from_config_file(path=args.config, yolo=args.yolo, theme=args.theme)
202
+ _cli.Theme.set_mode(_cli.Theme.resolve(session.settings.theme))
203
+ warm_provider_sdks()
204
+ command_loop = _cli.CommandLoop(_cli.Agent(session))
205
+ try:
206
+ if banner_preprinted:
207
+ command_loop.preprinted_output = preprinted_output
208
+ code = command_loop.run(show_banner=False)
209
+ banner_preprinted = False
210
+ else:
211
+ code = command_loop.run()
212
+ finally:
213
+ # The runtime closes what the session opened, on the loop that opened it; all that
214
+ # is left here is the terminal-output gate, in case the runtime never got that far.
215
+ command_loop.close_background_output()
216
+ resume = command_loop.resume_request
217
+ if not resume:
218
+ return code
219
+ except _cli.ConfigError as error:
220
+ print("ConfigError: " + str(error), file=sys.stderr)
221
+ return 2
222
+ except (_cli.WizoltError, _cli.CatalogError) as error:
223
+ print("Error: " + str(error), file=sys.stderr)
224
+ return 1
225
+
226
+
227
+ if __name__ == "__main__":
228
+ raise SystemExit(main())