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.
@@ -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)