zai-cli 0.1.1__tar.gz → 0.1.3__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.
- {zai_cli-0.1.1 → zai_cli-0.1.3}/CHANGELOG.md +23 -0
- {zai_cli-0.1.1/zai_cli.egg-info → zai_cli-0.1.3}/PKG-INFO +1 -1
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_input.py +1 -1
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_interactive.py +67 -3
- zai_cli-0.1.3/zai/__init__.py +1 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/interactive.py +104 -18
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/ui/input.py +1 -1
- {zai_cli-0.1.1 → zai_cli-0.1.3/zai_cli.egg-info}/PKG-INFO +1 -1
- zai_cli-0.1.1/zai/__init__.py +0 -1
- {zai_cli-0.1.1 → zai_cli-0.1.3}/LICENSE +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/MANIFEST.in +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/README.md +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/pyproject.toml +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/scripts/release_preflight.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/setup.cfg +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_agent.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_browser.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_code_runner.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_config_main.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_core.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_errors.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_hooks_skills_session.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_integrations_cli.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_mcp.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_plugins.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_process.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_providers.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_release_preflight.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_search.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_security.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_settings_cli.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_storage.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_streaming.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_tool_schema.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_tools.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_undo.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_utilities.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_vision.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_watch.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_workflows.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/__main__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/common.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/integrations.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/settings.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/utilities.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/workflows.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/commit.md +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/explain.md +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/feature.md +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/fix.md +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/review.md +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/config.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/agent.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/cancellation.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/commands.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/context.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/errors.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/fallback.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/hooks.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/memory.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/process.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/repomap.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/runtime.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/security.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/session.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/storage.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/streaming.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/tool_schema.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/undo.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/watch.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/main.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/mcp/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/mcp/client.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/mcp/manager.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/plugins/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/plugins/base.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/plugins/loader.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/anthropic.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/base.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/cerebras.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/gemini.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/groq.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/ollama.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/openai.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/openrouter.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/qwen.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/skills/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/skills/registry.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/browser.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/code_runner.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/files.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/git.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/search.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/vision.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/ui/__init__.py +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/SOURCES.txt +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/dependency_links.txt +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/entry_points.txt +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/requires.txt +0 -0
- {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,29 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and the project uses [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.1.3] - 2026-06-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Made Ctrl+C exit predictable in interactive mode: one Ctrl+C cancels the
|
|
13
|
+
current prompt/operation, and a second consecutive Ctrl+C exits the session.
|
|
14
|
+
- Added in-chat key setup: starting `zai` without a provider now offers API-key
|
|
15
|
+
setup immediately, and `/setup` can save keys without leaving interactive mode.
|
|
16
|
+
|
|
17
|
+
## [0.1.2] - 2026-06-24
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Improved interactive slash-command discovery: typing `/` now shows the
|
|
22
|
+
commands menu.
|
|
23
|
+
- Improved interactive model discovery: bare `/model` now shows the model list.
|
|
24
|
+
- Clarified the interactive startup hint to point users at `/` and `/help`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Prevented common help words such as `commands`, `setup`, and `model list`
|
|
29
|
+
from being sent to the AI as ordinary project requests.
|
|
30
|
+
|
|
8
31
|
## [0.1.1] - 2026-06-24
|
|
9
32
|
|
|
10
33
|
### Added
|
|
@@ -19,6 +19,7 @@ def test_slash_command_typo_is_corrected():
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_plain_command_aliases_do_not_go_to_ai():
|
|
22
|
+
assert _normalize_plain_command("/") == "/commands"
|
|
22
23
|
assert _normalize_plain_command("commands") == "/commands"
|
|
23
24
|
assert _normalize_plain_command("setup") == "/setup"
|
|
24
25
|
assert _normalize_plain_command("model list") == "/model list"
|
|
@@ -34,17 +35,61 @@ def test_interactive_plain_commands_show_local_help(tmp_path, monkeypatch, capsy
|
|
|
34
35
|
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
35
36
|
with patch(
|
|
36
37
|
"zai.cli.interactive.InteractiveInput.prompt",
|
|
37
|
-
side_effect=["commands", "setup", EOFError()],
|
|
38
|
+
side_effect=["/", "commands", "setup", "/model", EOFError()],
|
|
38
39
|
):
|
|
39
40
|
with patch("zai.cli.interactive.run_agent") as run_agent:
|
|
40
|
-
|
|
41
|
+
with patch(
|
|
42
|
+
"zai.cli.interactive._run_key_setup_interactive",
|
|
43
|
+
return_value=True,
|
|
44
|
+
) as setup:
|
|
45
|
+
run_interactive("groq")
|
|
41
46
|
|
|
42
47
|
output = capsys.readouterr().out
|
|
43
48
|
assert "Interactive slash commands" in output
|
|
44
|
-
|
|
49
|
+
setup.assert_called_once()
|
|
50
|
+
assert "Available models" in output
|
|
45
51
|
run_agent.assert_not_called()
|
|
46
52
|
|
|
47
53
|
|
|
54
|
+
def test_interactive_offers_setup_when_no_provider(tmp_path, monkeypatch, capsys):
|
|
55
|
+
monkeypatch.chdir(tmp_path)
|
|
56
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=False):
|
|
57
|
+
with patch("zai.cli.interactive.Confirm.ask", return_value=True):
|
|
58
|
+
with patch(
|
|
59
|
+
"zai.cli.interactive._run_key_setup_interactive",
|
|
60
|
+
return_value=True,
|
|
61
|
+
) as setup:
|
|
62
|
+
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
63
|
+
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
64
|
+
with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
|
|
65
|
+
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
66
|
+
with patch(
|
|
67
|
+
"zai.cli.interactive.InteractiveInput.prompt",
|
|
68
|
+
side_effect=EOFError(),
|
|
69
|
+
):
|
|
70
|
+
run_interactive("groq")
|
|
71
|
+
|
|
72
|
+
output = capsys.readouterr().out
|
|
73
|
+
assert "No AI provider available" in output
|
|
74
|
+
setup.assert_called_once()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_interactive_key_setup_saves_entered_key(capsys):
|
|
78
|
+
from zai.cli.interactive import _run_key_setup_interactive
|
|
79
|
+
|
|
80
|
+
answers = iter(["gemini-key", "", "", "", "", "", ""])
|
|
81
|
+
|
|
82
|
+
with patch("zai.cli.interactive.get_api_key", return_value=None):
|
|
83
|
+
with patch("zai.cli.interactive.Prompt.ask", side_effect=lambda *a, **k: next(answers)):
|
|
84
|
+
with patch("zai.cli.interactive.save_api_key") as save_key:
|
|
85
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
86
|
+
assert _run_key_setup_interactive()
|
|
87
|
+
|
|
88
|
+
output = capsys.readouterr().out
|
|
89
|
+
assert "Provider key setup" in output
|
|
90
|
+
save_key.assert_called_once_with("gemini", "gemini-key")
|
|
91
|
+
|
|
92
|
+
|
|
48
93
|
def test_interactive_help_then_exit(tmp_path, monkeypatch, capsys):
|
|
49
94
|
monkeypatch.chdir(tmp_path)
|
|
50
95
|
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
@@ -85,6 +130,25 @@ def test_interactive_fires_session_end(tmp_path, monkeypatch):
|
|
|
85
130
|
assert any(call.args[0] == "SessionEnd" for call in fire.call_args_list)
|
|
86
131
|
|
|
87
132
|
|
|
133
|
+
def test_interactive_exits_after_second_input_cancel(tmp_path, monkeypatch, capsys):
|
|
134
|
+
monkeypatch.chdir(tmp_path)
|
|
135
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
136
|
+
with patch("zai.cli.interactive.fire_hook", return_value=True) as fire:
|
|
137
|
+
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
138
|
+
with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
|
|
139
|
+
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
140
|
+
with patch(
|
|
141
|
+
"zai.cli.interactive.InteractiveInput.prompt",
|
|
142
|
+
side_effect=[KeyboardInterrupt(), KeyboardInterrupt()],
|
|
143
|
+
):
|
|
144
|
+
run_interactive("groq")
|
|
145
|
+
|
|
146
|
+
output = capsys.readouterr().out
|
|
147
|
+
assert "Press Ctrl+C again to exit" in output
|
|
148
|
+
assert "Goodbye" in output
|
|
149
|
+
assert any(call.args[0] == "SessionEnd" for call in fire.call_args_list)
|
|
150
|
+
|
|
151
|
+
|
|
88
152
|
def test_interactive_session_rename_routes_arguments(tmp_path, monkeypatch):
|
|
89
153
|
monkeypatch.chdir(tmp_path)
|
|
90
154
|
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
@@ -7,7 +7,7 @@ from rich.panel import Panel
|
|
|
7
7
|
from rich.prompt import Confirm, Prompt
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
|
-
from ..config import get_api_key, get_models, load_config
|
|
10
|
+
from ..config import ENV_FILE, get_api_key, get_models, load_config, save_api_key, save_config
|
|
11
11
|
from ..core.agent import plan_agent, run_agent, undo_last
|
|
12
12
|
from ..core.commands import get_command_prompt, list_commands, load_commands
|
|
13
13
|
from ..core.hooks import fire as fire_hook
|
|
@@ -73,6 +73,16 @@ PLAIN_COMMAND_ALIASES = {
|
|
|
73
73
|
"undo": "/undo",
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
KEY_SETUP_PROVIDERS = [
|
|
77
|
+
("gemini", "Google Gemini (free, recommended)", "https://aistudio.google.com/app/apikey"),
|
|
78
|
+
("groq", "Groq (free, fast)", "https://console.groq.com/keys"),
|
|
79
|
+
("openai", "OpenAI (paid)", "https://platform.openai.com/api-keys"),
|
|
80
|
+
("cerebras", "Cerebras (free)", "https://cloud.cerebras.ai/platform"),
|
|
81
|
+
("openrouter", "OpenRouter", "https://openrouter.ai/keys"),
|
|
82
|
+
("qwen", "Qwen/DashScope", "https://dashscope.aliyun.com/"),
|
|
83
|
+
("anthropic", "Anthropic Claude (paid)", "https://console.anthropic.com/settings/keys"),
|
|
84
|
+
]
|
|
85
|
+
|
|
76
86
|
|
|
77
87
|
def _show_help() -> None:
|
|
78
88
|
console.print(
|
|
@@ -140,8 +150,73 @@ def _show_setup_hint() -> None:
|
|
|
140
150
|
))
|
|
141
151
|
|
|
142
152
|
|
|
153
|
+
def _show_models() -> None:
|
|
154
|
+
table = Table(title="Available models", border_style="cyan")
|
|
155
|
+
table.add_column("Name", style="cyan")
|
|
156
|
+
table.add_column("Model")
|
|
157
|
+
table.add_column("Key")
|
|
158
|
+
for key, data in get_models().items():
|
|
159
|
+
key_status = "ok" if get_api_key(data["provider"]) else "no key"
|
|
160
|
+
table.add_row(key, data["name"], key_status)
|
|
161
|
+
console.print(table)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _run_key_setup_interactive() -> bool:
|
|
165
|
+
console.print(Panel(
|
|
166
|
+
"[bold cyan]Provider key setup[/bold cyan]\n"
|
|
167
|
+
"Add at least one API key. Gemini and Groq are good free starting points; "
|
|
168
|
+
"OpenAI is paid.\n"
|
|
169
|
+
f"[dim]Keys are saved locally in {ENV_FILE}[/dim]",
|
|
170
|
+
border_style="cyan",
|
|
171
|
+
))
|
|
172
|
+
for provider, label, url in KEY_SETUP_PROVIDERS:
|
|
173
|
+
existing = get_api_key(provider)
|
|
174
|
+
if existing:
|
|
175
|
+
console.print(f"[green]✓[/green] {label} already set")
|
|
176
|
+
value = Prompt.ask(
|
|
177
|
+
f"[cyan]{label}[/cyan] new API key [dim](Enter to keep)[/dim]",
|
|
178
|
+
default="",
|
|
179
|
+
password=True,
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
console.print(f"[dim]{label} key: {url}[/dim]")
|
|
183
|
+
value = Prompt.ask(
|
|
184
|
+
f"[cyan]{label}[/cyan] API key [dim](Enter to skip)[/dim]",
|
|
185
|
+
default="",
|
|
186
|
+
password=True,
|
|
187
|
+
)
|
|
188
|
+
if value.strip():
|
|
189
|
+
save_api_key(provider, value.strip())
|
|
190
|
+
console.print("[green]✓ Saved[/green]")
|
|
191
|
+
|
|
192
|
+
config = load_config()
|
|
193
|
+
models = get_models(config)
|
|
194
|
+
available_free = [
|
|
195
|
+
name for name, data in models.items()
|
|
196
|
+
if data["free"] and get_api_key(data["provider"])
|
|
197
|
+
]
|
|
198
|
+
if available_free:
|
|
199
|
+
current = config.get("default_model")
|
|
200
|
+
default = current if current in models else available_free[0]
|
|
201
|
+
selected = Prompt.ask(
|
|
202
|
+
"Default model",
|
|
203
|
+
choices=list(models.keys()),
|
|
204
|
+
default=default,
|
|
205
|
+
)
|
|
206
|
+
config["default_model"] = selected
|
|
207
|
+
save_config(config)
|
|
208
|
+
|
|
209
|
+
if has_available_provider():
|
|
210
|
+
console.print("[green]zai is ready.[/green]")
|
|
211
|
+
return True
|
|
212
|
+
console.print("[yellow]No provider key was saved. Run /setup when ready.[/yellow]")
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
143
216
|
def _normalize_plain_command(message: str) -> str:
|
|
144
217
|
stripped = message.strip()
|
|
218
|
+
if stripped == "/":
|
|
219
|
+
return "/commands"
|
|
145
220
|
if stripped.startswith("/") or not stripped:
|
|
146
221
|
return message
|
|
147
222
|
return PLAIN_COMMAND_ALIASES.get(stripped.lower(), message)
|
|
@@ -230,10 +305,13 @@ def run_interactive(model: str = None) -> None:
|
|
|
230
305
|
if not has_available_provider():
|
|
231
306
|
console.print(Panel(
|
|
232
307
|
"[red]No AI provider available![/red]\n"
|
|
233
|
-
"
|
|
308
|
+
"Set up an API key now, run [cyan]zai setup[/cyan], or start Ollama.",
|
|
234
309
|
border_style="red",
|
|
235
310
|
))
|
|
236
|
-
|
|
311
|
+
if not Confirm.ask("Set up an API key now?", default=True):
|
|
312
|
+
return
|
|
313
|
+
if not _run_key_setup_interactive():
|
|
314
|
+
return
|
|
237
315
|
|
|
238
316
|
try:
|
|
239
317
|
visible = [name for name in sorted(os.listdir(cwd)) if not name.startswith(".")]
|
|
@@ -244,7 +322,7 @@ def run_interactive(model: str = None) -> None:
|
|
|
244
322
|
f"[dim]Folder: {cwd}[/dim]\n"
|
|
245
323
|
f"[dim]Files: {', '.join(visible[:12])}"
|
|
246
324
|
f"{'...' if len(visible) > 12 else ''}[/dim]\n"
|
|
247
|
-
"[dim]Ctrl+C to exit | /help for
|
|
325
|
+
"[dim]Ctrl+C to exit | type / for commands | /help for help[/dim]",
|
|
248
326
|
border_style="cyan",
|
|
249
327
|
))
|
|
250
328
|
|
|
@@ -315,15 +393,23 @@ def run_interactive(model: str = None) -> None:
|
|
|
315
393
|
session_context.compress(preferred)
|
|
316
394
|
save_auto_history(history, cwd)
|
|
317
395
|
|
|
396
|
+
cancelled_once = False
|
|
397
|
+
|
|
318
398
|
while True:
|
|
319
399
|
try:
|
|
320
400
|
try:
|
|
321
401
|
message = terminal_input.prompt()
|
|
322
402
|
except KeyboardInterrupt:
|
|
323
|
-
|
|
403
|
+
if cancelled_once:
|
|
404
|
+
fire_hook("SessionEnd", {"cwd": cwd, "messages": len(history)})
|
|
405
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
406
|
+
break
|
|
407
|
+
cancelled_once = True
|
|
408
|
+
console.print("[dim]Input cancelled. Press Ctrl+C again to exit.[/dim]")
|
|
324
409
|
continue
|
|
325
410
|
if not message.strip():
|
|
326
411
|
continue
|
|
412
|
+
cancelled_once = False
|
|
327
413
|
message = _correct_slash_command(message)
|
|
328
414
|
message = _normalize_plain_command(message)
|
|
329
415
|
stripped = message.strip()
|
|
@@ -333,23 +419,15 @@ def run_interactive(model: str = None) -> None:
|
|
|
333
419
|
elif stripped == "/commands":
|
|
334
420
|
_show_commands()
|
|
335
421
|
elif stripped == "/setup":
|
|
336
|
-
|
|
422
|
+
_run_key_setup_interactive()
|
|
337
423
|
elif stripped == "/clear":
|
|
338
424
|
console.clear()
|
|
339
425
|
elif stripped == "/files":
|
|
340
426
|
_show_files(cwd)
|
|
341
|
-
elif stripped.startswith("/model "):
|
|
342
|
-
name = stripped.split(" ", 1)[1].strip()
|
|
427
|
+
elif stripped == "/model" or stripped.startswith("/model "):
|
|
428
|
+
name = stripped.split(" ", 1)[1].strip() if " " in stripped else "list"
|
|
343
429
|
if name == "list":
|
|
344
|
-
|
|
345
|
-
key_status = (
|
|
346
|
-
"[green]ok[/green]"
|
|
347
|
-
if get_api_key(data["provider"])
|
|
348
|
-
else "[red]no key[/red]"
|
|
349
|
-
)
|
|
350
|
-
console.print(
|
|
351
|
-
f" [cyan]{key}[/cyan] — {data['name']} ({key_status})"
|
|
352
|
-
)
|
|
430
|
+
_show_models()
|
|
353
431
|
elif name in get_models():
|
|
354
432
|
preferred = name
|
|
355
433
|
session_context.set_model(name)
|
|
@@ -579,7 +657,15 @@ def run_interactive(model: str = None) -> None:
|
|
|
579
657
|
remember_turn(stripped, result)
|
|
580
658
|
save_session(task=stripped[:80], model=preferred)
|
|
581
659
|
except KeyboardInterrupt:
|
|
582
|
-
|
|
660
|
+
if cancelled_once:
|
|
661
|
+
fire_hook("SessionEnd", {"cwd": cwd, "messages": len(history)})
|
|
662
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
663
|
+
break
|
|
664
|
+
cancelled_once = True
|
|
665
|
+
console.print(
|
|
666
|
+
"\n[yellow]Current operation cancelled. "
|
|
667
|
+
"Press Ctrl+C again to exit.[/yellow]"
|
|
668
|
+
)
|
|
583
669
|
continue
|
|
584
670
|
except EOFError:
|
|
585
671
|
fire_hook("SessionEnd", {"cwd": cwd, "messages": len(history)})
|
|
@@ -59,7 +59,7 @@ def completion_candidates(
|
|
|
59
59
|
command_token = parts[0]
|
|
60
60
|
if len(parts) == 1 and not before.endswith(" "):
|
|
61
61
|
return [
|
|
62
|
-
(f"/{name}", f"/{name}", "command")
|
|
62
|
+
(f"/{name}", f"/{name}", "slash command")
|
|
63
63
|
for name in sorted(set(commands))
|
|
64
64
|
if f"/{name}".startswith(command_token.lower())
|
|
65
65
|
]
|
zai_cli-0.1.1/zai/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|