wizelit-cli 0.1.17__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.
- wizelit_cli-0.1.17/.gitignore +47 -0
- wizelit_cli-0.1.17/LICENSE +21 -0
- wizelit_cli-0.1.17/PKG-INFO +89 -0
- wizelit_cli-0.1.17/README.md +68 -0
- wizelit_cli-0.1.17/pyproject.toml +39 -0
- wizelit_cli-0.1.17/wizelit_cli/__init__.py +35 -0
- wizelit_cli-0.1.17/wizelit_cli/agent_cli.py +267 -0
- wizelit_cli-0.1.17/wizelit_cli/api_config.py +77 -0
- wizelit_cli-0.1.17/wizelit_cli/checkpoint.py +44 -0
- wizelit_cli-0.1.17/wizelit_cli/config_paths.py +56 -0
- wizelit_cli-0.1.17/wizelit_cli/hitl.py +198 -0
- wizelit_cli-0.1.17/wizelit_cli/mcp_loader.py +89 -0
- wizelit_cli-0.1.17/wizelit_cli/mcp_settings_loader.py +175 -0
- wizelit_cli-0.1.17/wizelit_cli/profile_store.py +115 -0
- wizelit_cli-0.1.17/wizelit_cli/profile_sync.py +172 -0
- wizelit_cli-0.1.17/wizelit_cli/runner.py +217 -0
- wizelit_cli-0.1.17/wizelit_cli/secure_io.py +29 -0
- wizelit_cli-0.1.17/wizelit_cli/session.py +113 -0
- wizelit_cli-0.1.17/wizelit_cli/settings_loader.py +300 -0
- wizelit_cli-0.1.17/wizelit_cli/settings_types.py +16 -0
- wizelit_cli-0.1.17/wizelit_cli/shutdown.py +142 -0
- wizelit_cli-0.1.17/wizelit_cli/stream_events.py +81 -0
- wizelit_cli-0.1.17/wizelit_cli/stream_renderer.py +416 -0
- wizelit_cli-0.1.17/wizelit_cli/terminal_ui.py +120 -0
- wizelit_cli-0.1.17/wizelit_cli/util.py +33 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.cli_bundle/
|
|
9
|
+
.agent/
|
|
10
|
+
.venv/
|
|
11
|
+
*.egg
|
|
12
|
+
|
|
13
|
+
# Node
|
|
14
|
+
node_modules/
|
|
15
|
+
dist/
|
|
16
|
+
|
|
17
|
+
# Environment
|
|
18
|
+
.env
|
|
19
|
+
!.env.template
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Data
|
|
32
|
+
data/
|
|
33
|
+
|
|
34
|
+
# Test
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
videos/
|
|
39
|
+
|
|
40
|
+
# Build
|
|
41
|
+
*.whl
|
|
42
|
+
|
|
43
|
+
# AWS CDK
|
|
44
|
+
cdk.out/
|
|
45
|
+
cdk.context.json
|
|
46
|
+
|
|
47
|
+
local_*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wizeline
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wizelit-cli
|
|
3
|
+
Version: 0.1.17
|
|
4
|
+
Summary: Wizelit terminal agent — installable CLI for local repo work
|
|
5
|
+
Author-email: Wizeline <engineering@wizeline.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agents,cli,deepagents,terminal,wizelit
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Terminals
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: wizelit-core==0.1.17
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Wizelit CLI
|
|
23
|
+
|
|
24
|
+
Terminal agent for local repository work. Depends on **`wizelit-core`** only (no FastAPI, SQLAlchemy, or Postgres). Settings and MCP sync from your Wizelit hub; skills and subagents ship inside `wizelit-core`.
|
|
25
|
+
|
|
26
|
+
Install via **`uv tool install`** (Python 3.12+ pulled automatically).
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
1. Install [uv](https://docs.astral.sh/uv/): `brew install uv`
|
|
31
|
+
2. Install the CLI:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install wizelit-cli@latest
|
|
35
|
+
wizelit-agent --help
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Create **`~/.wizelit/.env`** (minimum):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
WIZELIT_API_URL=https://your-wizelit-hub.example.com
|
|
42
|
+
WIZELIT_IDE_TOKEN=wiz_xxxxxxxx
|
|
43
|
+
# LLM credentials (match your web UI provider):
|
|
44
|
+
GOOGLE_API_KEY=...
|
|
45
|
+
# and/or AWS_REGION + keys for Bedrock, ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
4. Sync settings from the hub, then run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
wizelit-agent --sync-profile
|
|
52
|
+
wizelit-agent -i --repo /path/to/your-project
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## One-shot (no install)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uvx --from wizelit-cli wizelit-agent -i --repo /path/to/project
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Config
|
|
62
|
+
|
|
63
|
+
| Path | Purpose |
|
|
64
|
+
|------|---------|
|
|
65
|
+
| `~/.wizelit/.env` | Hub URL, IDE token, LLM credentials |
|
|
66
|
+
| `~/.wizelit/profile.json` | Synced LLM + MCP settings (from `--sync-profile`) |
|
|
67
|
+
| `{repo}/.agent/cli_checkpoints.sqlite` | Conversation checkpoints per repo |
|
|
68
|
+
|
|
69
|
+
Override config directory: `WIZELIT_CONFIG_DIR`.
|
|
70
|
+
|
|
71
|
+
## REPL commands
|
|
72
|
+
|
|
73
|
+
| Input | Action |
|
|
74
|
+
|-------|--------|
|
|
75
|
+
| `/exit`, `/quit` | Leave the REPL |
|
|
76
|
+
| `/reload` | Rebuild agent + reconnect MCP |
|
|
77
|
+
| `/sync` | Refresh profile from API, then reload |
|
|
78
|
+
| `Ctrl+C` | Interrupt current turn |
|
|
79
|
+
|
|
80
|
+
## Troubleshooting
|
|
81
|
+
|
|
82
|
+
| Symptom | What to do |
|
|
83
|
+
|---------|------------|
|
|
84
|
+
| `wizelit-agent: command not found` | New terminal after install, or `uv tool update-shell` |
|
|
85
|
+
| `401` on `--sync-profile` | Refresh `WIZELIT_IDE_TOKEN` in `~/.wizelit/.env` |
|
|
86
|
+
| Hub unreachable | Check `WIZELIT_API_URL`; ensure backend is running |
|
|
87
|
+
| One-shot opens REPL | Drop `-i` when passing a prompt on the command line |
|
|
88
|
+
|
|
89
|
+
Upgrade: `uv tool upgrade wizelit-cli`
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Wizelit CLI
|
|
2
|
+
|
|
3
|
+
Terminal agent for local repository work. Depends on **`wizelit-core`** only (no FastAPI, SQLAlchemy, or Postgres). Settings and MCP sync from your Wizelit hub; skills and subagents ship inside `wizelit-core`.
|
|
4
|
+
|
|
5
|
+
Install via **`uv tool install`** (Python 3.12+ pulled automatically).
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
1. Install [uv](https://docs.astral.sh/uv/): `brew install uv`
|
|
10
|
+
2. Install the CLI:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv tool install wizelit-cli@latest
|
|
14
|
+
wizelit-agent --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
3. Create **`~/.wizelit/.env`** (minimum):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
WIZELIT_API_URL=https://your-wizelit-hub.example.com
|
|
21
|
+
WIZELIT_IDE_TOKEN=wiz_xxxxxxxx
|
|
22
|
+
# LLM credentials (match your web UI provider):
|
|
23
|
+
GOOGLE_API_KEY=...
|
|
24
|
+
# and/or AWS_REGION + keys for Bedrock, ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. Sync settings from the hub, then run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
wizelit-agent --sync-profile
|
|
31
|
+
wizelit-agent -i --repo /path/to/your-project
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## One-shot (no install)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uvx --from wizelit-cli wizelit-agent -i --repo /path/to/project
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Config
|
|
41
|
+
|
|
42
|
+
| Path | Purpose |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| `~/.wizelit/.env` | Hub URL, IDE token, LLM credentials |
|
|
45
|
+
| `~/.wizelit/profile.json` | Synced LLM + MCP settings (from `--sync-profile`) |
|
|
46
|
+
| `{repo}/.agent/cli_checkpoints.sqlite` | Conversation checkpoints per repo |
|
|
47
|
+
|
|
48
|
+
Override config directory: `WIZELIT_CONFIG_DIR`.
|
|
49
|
+
|
|
50
|
+
## REPL commands
|
|
51
|
+
|
|
52
|
+
| Input | Action |
|
|
53
|
+
|-------|--------|
|
|
54
|
+
| `/exit`, `/quit` | Leave the REPL |
|
|
55
|
+
| `/reload` | Rebuild agent + reconnect MCP |
|
|
56
|
+
| `/sync` | Refresh profile from API, then reload |
|
|
57
|
+
| `Ctrl+C` | Interrupt current turn |
|
|
58
|
+
|
|
59
|
+
## Troubleshooting
|
|
60
|
+
|
|
61
|
+
| Symptom | What to do |
|
|
62
|
+
|---------|------------|
|
|
63
|
+
| `wizelit-agent: command not found` | New terminal after install, or `uv tool update-shell` |
|
|
64
|
+
| `401` on `--sync-profile` | Refresh `WIZELIT_IDE_TOKEN` in `~/.wizelit/.env` |
|
|
65
|
+
| Hub unreachable | Check `WIZELIT_API_URL`; ensure backend is running |
|
|
66
|
+
| One-shot opens REPL | Drop `-i` when passing a prompt on the command line |
|
|
67
|
+
|
|
68
|
+
Upgrade: `uv tool upgrade wizelit-cli`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wizelit-cli"
|
|
3
|
+
version = "0.1.17"
|
|
4
|
+
description = "Wizelit terminal agent — installable CLI for local repo work"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [{ name = "Wizeline", email = "engineering@wizeline.com" }]
|
|
9
|
+
keywords = ["wizelit", "cli", "agents", "terminal", "deepagents"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Topic :: Terminals",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"wizelit-core==0.1.17",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
wizelit-agent = "wizelit_cli.agent_cli:main"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["wizelit_cli"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.sdist]
|
|
36
|
+
include = ["LICENSE", "README.md", "wizelit_cli/**/*.py"]
|
|
37
|
+
|
|
38
|
+
[tool.uv.sources]
|
|
39
|
+
wizelit-core = { workspace = true }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Interactive terminal CLI for the Wizelit deep agent (agent_cli-style)."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.17"
|
|
4
|
+
|
|
5
|
+
from wizelit_cli.agent_cli import main
|
|
6
|
+
from wizelit_cli.profile_sync import load_profile_layers, sync_cli_profile_to_disk
|
|
7
|
+
from wizelit_cli.profile_store import profile_path
|
|
8
|
+
from wizelit_cli.runner import run_interactive, run_once
|
|
9
|
+
from wizelit_cli.session import build_cli_agent
|
|
10
|
+
from wizelit_cli.settings_loader import resolve_cli_llm_config
|
|
11
|
+
from wizelit_cli.mcp_settings_loader import resolve_cli_mcp_servers
|
|
12
|
+
from wizelit_cli.settings_types import LlmRunConfig
|
|
13
|
+
from wizelit_cli.util import (
|
|
14
|
+
backend_install_root,
|
|
15
|
+
repo_thread_id,
|
|
16
|
+
resolve_cli_skills_dir,
|
|
17
|
+
resolve_cli_subagents_dir,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"LlmRunConfig",
|
|
22
|
+
"backend_install_root",
|
|
23
|
+
"build_cli_agent",
|
|
24
|
+
"load_profile_layers",
|
|
25
|
+
"main",
|
|
26
|
+
"profile_path",
|
|
27
|
+
"repo_thread_id",
|
|
28
|
+
"resolve_cli_llm_config",
|
|
29
|
+
"resolve_cli_mcp_servers",
|
|
30
|
+
"resolve_cli_skills_dir",
|
|
31
|
+
"resolve_cli_subagents_dir",
|
|
32
|
+
"run_interactive",
|
|
33
|
+
"run_once",
|
|
34
|
+
"sync_cli_profile_to_disk",
|
|
35
|
+
]
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Interactive terminal agent for Wizelit v2.
|
|
3
|
+
|
|
4
|
+
Examples:
|
|
5
|
+
cd packages/backend
|
|
6
|
+
uv run python scripts/agent_cli.py -i
|
|
7
|
+
uv run python scripts/agent_cli.py --repo /path/to/project -i
|
|
8
|
+
uv run python scripts/agent_cli.py "Summarize this repo"
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import signal
|
|
18
|
+
import sys
|
|
19
|
+
import uuid
|
|
20
|
+
from contextlib import AsyncExitStack
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from wizelit_cli.config_paths import load_cli_env
|
|
24
|
+
|
|
25
|
+
load_cli_env()
|
|
26
|
+
|
|
27
|
+
from wizelit_cli.checkpoint import close_checkpointer, open_checkpointer # noqa: E402
|
|
28
|
+
from wizelit_cli.runner import run_interactive, run_once # noqa: E402
|
|
29
|
+
from wizelit_cli.session import build_cli_agent, cli_user_id # noqa: E402
|
|
30
|
+
from wizelit_cli.profile_sync import load_profile_layers, sync_cli_profile_to_disk
|
|
31
|
+
from wizelit_cli.settings_loader import resolve_cli_llm_config # noqa: E402
|
|
32
|
+
from wizelit_cli.shutdown import ( # noqa: E402
|
|
33
|
+
run_cli_loop,
|
|
34
|
+
shutdown_cli_session_bounded,
|
|
35
|
+
)
|
|
36
|
+
from wizelit_cli.mcp_loader import disconnect_mcp_servers # noqa: E402
|
|
37
|
+
from wizelit_core.mcp_manager import mcp_manager # noqa: E402
|
|
38
|
+
from wizelit_cli.terminal_ui import stop_all_spinners # noqa: E402
|
|
39
|
+
from wizelit_cli.util import repo_thread_id # noqa: E402
|
|
40
|
+
from wizelit_core.llm_catalog import get_provider_models # noqa: E402
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_version() -> str:
|
|
44
|
+
import importlib.metadata
|
|
45
|
+
|
|
46
|
+
def _pkg_version(name: str) -> str:
|
|
47
|
+
try:
|
|
48
|
+
return importlib.metadata.version(name)
|
|
49
|
+
except importlib.metadata.PackageNotFoundError:
|
|
50
|
+
return "unknown"
|
|
51
|
+
|
|
52
|
+
cli_ver = _pkg_version("wizelit-cli")
|
|
53
|
+
core_ver = _pkg_version("wizelit-core")
|
|
54
|
+
return f"wizelit-agent {cli_ver} (wizelit-core {core_ver})"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _configure_logging() -> None:
|
|
58
|
+
if logging.getLogger().handlers:
|
|
59
|
+
return
|
|
60
|
+
level = getattr(logging, os.getenv("LOG_LEVEL", "WARNING").upper(), logging.WARNING)
|
|
61
|
+
logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _checkpoint_path(repo_path: Path) -> Path:
|
|
65
|
+
state_dir = repo_path / ".agent"
|
|
66
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
return state_dir / "cli_checkpoints.sqlite"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main(argv: list[str] | None = None) -> None:
|
|
71
|
+
_configure_logging()
|
|
72
|
+
|
|
73
|
+
parser = argparse.ArgumentParser(
|
|
74
|
+
description="Run the Wizelit deep agent in the terminal against a target repo.",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"-V",
|
|
78
|
+
"--version",
|
|
79
|
+
action="version",
|
|
80
|
+
version=_format_version(),
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument("prompt", nargs="*", help="Text to send to the agent.")
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"-i",
|
|
85
|
+
"--interactive",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="Interactive REPL (default when stdin is a TTY and no prompt given).",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--repo",
|
|
91
|
+
default=os.getcwd(),
|
|
92
|
+
help="Target repo path the agent operates on (default: CWD).",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--new-thread",
|
|
96
|
+
action="store_true",
|
|
97
|
+
help="Force a fresh conversation thread for this repo.",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--provider",
|
|
101
|
+
choices=list(get_provider_models().keys()),
|
|
102
|
+
help="LLM provider (bedrock, anthropic, google, openai).",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument("--model", help="Model id for the selected provider.")
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--skills-dir",
|
|
107
|
+
help="Directory of skill modules (overrides bundled skills and WIZELIT_SKILLS_DIR).",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--subagents-dir",
|
|
111
|
+
help="Directory of subagent .md specs (overrides bundled subagents and WIZELIT_SUBAGENTS_DIR).",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--sync-profile",
|
|
115
|
+
action="store_true",
|
|
116
|
+
help="Fetch LLM + MCP settings from the backend and write ~/.wizelit/profile.json, then exit.",
|
|
117
|
+
)
|
|
118
|
+
args = parser.parse_args(argv)
|
|
119
|
+
|
|
120
|
+
if args.skills_dir:
|
|
121
|
+
os.environ["WIZELIT_SKILLS_DIR"] = str(Path(args.skills_dir).expanduser().resolve())
|
|
122
|
+
if args.subagents_dir:
|
|
123
|
+
os.environ["WIZELIT_SUBAGENTS_DIR"] = str(Path(args.subagents_dir).expanduser().resolve())
|
|
124
|
+
|
|
125
|
+
if args.sync_profile:
|
|
126
|
+
async def _sync_only() -> None:
|
|
127
|
+
path = await sync_cli_profile_to_disk()
|
|
128
|
+
print(f"Synced CLI profile to {path}")
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
asyncio.run(_sync_only())
|
|
132
|
+
except Exception as exc: # noqa: BLE001
|
|
133
|
+
print(f"error: profile sync failed: {exc}", file=sys.stderr)
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
sys.exit(0)
|
|
136
|
+
|
|
137
|
+
repo_path = Path(args.repo).resolve()
|
|
138
|
+
if not repo_path.is_dir():
|
|
139
|
+
print(f"error: --repo {repo_path} is not a directory", file=sys.stderr)
|
|
140
|
+
sys.exit(2)
|
|
141
|
+
|
|
142
|
+
interactive = args.interactive or (not args.prompt and sys.stdin.isatty())
|
|
143
|
+
interactive_tty = interactive and sys.stdin.isatty()
|
|
144
|
+
|
|
145
|
+
thread_id = repo_thread_id(repo_path)
|
|
146
|
+
if args.new_thread:
|
|
147
|
+
thread_id = f"{thread_id}-{uuid.uuid4().hex[:8]}"
|
|
148
|
+
checkpoint_db = _checkpoint_path(repo_path)
|
|
149
|
+
clean_exit = False
|
|
150
|
+
|
|
151
|
+
async def run_cli() -> None:
|
|
152
|
+
nonlocal clean_exit
|
|
153
|
+
conn, checkpointer = await open_checkpointer(checkpoint_db)
|
|
154
|
+
active_user_id: str | None = None
|
|
155
|
+
exit_process = False
|
|
156
|
+
prefer_local_profile = False
|
|
157
|
+
stale_cancel_retries = 0
|
|
158
|
+
run_exit_code = 0
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
try:
|
|
162
|
+
profile_layers = await load_profile_layers(
|
|
163
|
+
prefer_local=prefer_local_profile
|
|
164
|
+
)
|
|
165
|
+
prefer_local_profile = False
|
|
166
|
+
try:
|
|
167
|
+
llm_config = await resolve_cli_llm_config(
|
|
168
|
+
provider_flag=args.provider,
|
|
169
|
+
model_flag=args.model,
|
|
170
|
+
interactive=interactive,
|
|
171
|
+
interactive_tty=interactive_tty,
|
|
172
|
+
profile_layers=profile_layers,
|
|
173
|
+
)
|
|
174
|
+
except ValueError as exc:
|
|
175
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
176
|
+
sys.exit(2)
|
|
177
|
+
|
|
178
|
+
print(
|
|
179
|
+
f"Using provider={llm_config.provider}, model={llm_config.model_id} "
|
|
180
|
+
f"(source={llm_config.settings_source})\n"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
config = {
|
|
184
|
+
"configurable": {"thread_id": thread_id},
|
|
185
|
+
"recursion_limit": llm_config.recursion_limit,
|
|
186
|
+
}
|
|
187
|
+
user_id = llm_config.user_identifier or cli_user_id()
|
|
188
|
+
active_user_id = user_id
|
|
189
|
+
|
|
190
|
+
async with AsyncExitStack() as stack:
|
|
191
|
+
agent = await asyncio.shield(
|
|
192
|
+
build_cli_agent(
|
|
193
|
+
repo_path=repo_path,
|
|
194
|
+
checkpointer=checkpointer,
|
|
195
|
+
stack=stack,
|
|
196
|
+
llm_config=llm_config,
|
|
197
|
+
thread_id=thread_id,
|
|
198
|
+
profile_layers=profile_layers,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if interactive or not args.prompt:
|
|
203
|
+
result = await run_interactive(agent, config)
|
|
204
|
+
if result == "RELOAD":
|
|
205
|
+
print("Reloading agent and MCP...", flush=True)
|
|
206
|
+
try:
|
|
207
|
+
await disconnect_mcp_servers(user_id)
|
|
208
|
+
except asyncio.CancelledError:
|
|
209
|
+
mcp_manager.drop_user(user_id)
|
|
210
|
+
await asyncio.sleep(0.75)
|
|
211
|
+
prefer_local_profile = True
|
|
212
|
+
stale_cancel_retries = 0
|
|
213
|
+
continue
|
|
214
|
+
else:
|
|
215
|
+
await run_once(agent, config, " ".join(args.prompt))
|
|
216
|
+
stale_cancel_retries = 0
|
|
217
|
+
exit_process = True
|
|
218
|
+
break
|
|
219
|
+
except asyncio.CancelledError:
|
|
220
|
+
# MCP stream teardown can cancel the next loop iteration briefly.
|
|
221
|
+
if stale_cancel_retries < 12:
|
|
222
|
+
stale_cancel_retries += 1
|
|
223
|
+
prefer_local_profile = True
|
|
224
|
+
await asyncio.sleep(0.15)
|
|
225
|
+
continue
|
|
226
|
+
exit_process = True
|
|
227
|
+
raise
|
|
228
|
+
except BaseException:
|
|
229
|
+
exit_process = True
|
|
230
|
+
run_exit_code = 1
|
|
231
|
+
raise
|
|
232
|
+
finally:
|
|
233
|
+
if exit_process:
|
|
234
|
+
clean_exit = True
|
|
235
|
+
await shutdown_cli_session_bounded(
|
|
236
|
+
user_id=active_user_id,
|
|
237
|
+
conn=conn,
|
|
238
|
+
exit_code=run_exit_code,
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
stop_all_spinners()
|
|
242
|
+
|
|
243
|
+
def _sigint_handler(_signum: int, _frame: object) -> None:
|
|
244
|
+
stop_all_spinners()
|
|
245
|
+
raise KeyboardInterrupt
|
|
246
|
+
|
|
247
|
+
previous_sigint = signal.getsignal(signal.SIGINT)
|
|
248
|
+
if sys.stdin.isatty():
|
|
249
|
+
signal.signal(signal.SIGINT, _sigint_handler)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
run_cli_loop(run_cli())
|
|
253
|
+
except KeyboardInterrupt:
|
|
254
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
255
|
+
sys.exit(130)
|
|
256
|
+
except asyncio.CancelledError:
|
|
257
|
+
if not clean_exit:
|
|
258
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
259
|
+
sys.exit(130)
|
|
260
|
+
finally:
|
|
261
|
+
stop_all_spinners()
|
|
262
|
+
if sys.stdin.isatty():
|
|
263
|
+
signal.signal(signal.SIGINT, previous_sigint)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shared backend API configuration for the terminal CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_DEFAULT_API_URL = "http://localhost:8000"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
16
|
+
raw = os.getenv(name)
|
|
17
|
+
if raw is None:
|
|
18
|
+
return default
|
|
19
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def api_url() -> str:
|
|
23
|
+
return (os.getenv("WIZELIT_API_URL") or _DEFAULT_API_URL).rstrip("/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def api_token() -> str | None:
|
|
27
|
+
token = (os.getenv("WIZELIT_IDE_TOKEN") or "").strip()
|
|
28
|
+
return token or None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def use_api_settings() -> bool:
|
|
32
|
+
return _env_bool("WIZELIT_CLI_USE_API_SETTINGS", True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def allow_insecure_http() -> bool:
|
|
36
|
+
"""Opt-in for sending credentials to remote plain-HTTP APIs."""
|
|
37
|
+
return _env_bool("WIZELIT_ALLOW_INSECURE", False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_loopback_host(host: str | None) -> bool:
|
|
41
|
+
if not host:
|
|
42
|
+
return False
|
|
43
|
+
normalized = host.strip("[]").lower()
|
|
44
|
+
if normalized == "localhost":
|
|
45
|
+
return True
|
|
46
|
+
try:
|
|
47
|
+
return ipaddress.ip_address(normalized).is_loopback
|
|
48
|
+
except ValueError:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_insecure_remote_http(base_url: str) -> bool:
|
|
53
|
+
"""True when ``base_url`` is plain HTTP to a non-loopback host."""
|
|
54
|
+
parsed = urlparse(base_url)
|
|
55
|
+
if parsed.scheme != "http":
|
|
56
|
+
return False
|
|
57
|
+
return not _is_loopback_host(parsed.hostname)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def bearer_auth_headers(base_url: str, token: str | None) -> dict[str, str]:
|
|
61
|
+
"""Build Authorization headers, refusing cleartext token to remote hosts."""
|
|
62
|
+
if not token:
|
|
63
|
+
return {}
|
|
64
|
+
if is_insecure_remote_http(base_url):
|
|
65
|
+
if allow_insecure_http():
|
|
66
|
+
logger.warning(
|
|
67
|
+
"Sending IDE bearer token to remote plain HTTP API %s "
|
|
68
|
+
"(WIZELIT_ALLOW_INSECURE=1). Prefer https://.",
|
|
69
|
+
base_url,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Refusing to send IDE token over plain HTTP to non-loopback host "
|
|
74
|
+
f"({base_url}). Use https:// or set WIZELIT_ALLOW_INSECURE=1 to "
|
|
75
|
+
"override (not recommended)."
|
|
76
|
+
)
|
|
77
|
+
return {"Authorization": f"Bearer {token}"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""SQLite checkpoint helpers for the terminal CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def open_checkpointer(db_path: Path) -> tuple[Any, Any]:
|
|
14
|
+
"""Open an AsyncSqliteSaver backed by aiosqlite. Returns (conn, saver)."""
|
|
15
|
+
import aiosqlite
|
|
16
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
17
|
+
|
|
18
|
+
conn = await aiosqlite.connect(str(db_path))
|
|
19
|
+
return conn, AsyncSqliteSaver(conn)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _force_close_sqlite(conn: Any) -> None:
|
|
23
|
+
"""Stop aiosqlite without awaiting close() (avoids wedging the event loop on exit)."""
|
|
24
|
+
try:
|
|
25
|
+
conn.stop()
|
|
26
|
+
except Exception: # noqa: BLE001
|
|
27
|
+
pass
|
|
28
|
+
thread = getattr(conn, "_thread", None)
|
|
29
|
+
if thread is not None and thread.is_alive():
|
|
30
|
+
thread.join(timeout=1.0)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def close_checkpointer(conn: Any, *, force: bool = False) -> None:
|
|
34
|
+
"""Close aiosqlite before the event loop stops."""
|
|
35
|
+
if conn is None:
|
|
36
|
+
return
|
|
37
|
+
if force:
|
|
38
|
+
_force_close_sqlite(conn)
|
|
39
|
+
return
|
|
40
|
+
try:
|
|
41
|
+
await asyncio.wait_for(conn.close(), timeout=2.0)
|
|
42
|
+
except Exception as exc: # noqa: BLE001
|
|
43
|
+
logger.debug("sqlite close: %s", exc)
|
|
44
|
+
_force_close_sqlite(conn)
|