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.
Files changed (104) hide show
  1. {zai_cli-0.1.1 → zai_cli-0.1.3}/CHANGELOG.md +23 -0
  2. {zai_cli-0.1.1/zai_cli.egg-info → zai_cli-0.1.3}/PKG-INFO +1 -1
  3. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_input.py +1 -1
  4. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_interactive.py +67 -3
  5. zai_cli-0.1.3/zai/__init__.py +1 -0
  6. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/interactive.py +104 -18
  7. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/ui/input.py +1 -1
  8. {zai_cli-0.1.1 → zai_cli-0.1.3/zai_cli.egg-info}/PKG-INFO +1 -1
  9. zai_cli-0.1.1/zai/__init__.py +0 -1
  10. {zai_cli-0.1.1 → zai_cli-0.1.3}/LICENSE +0 -0
  11. {zai_cli-0.1.1 → zai_cli-0.1.3}/MANIFEST.in +0 -0
  12. {zai_cli-0.1.1 → zai_cli-0.1.3}/README.md +0 -0
  13. {zai_cli-0.1.1 → zai_cli-0.1.3}/pyproject.toml +0 -0
  14. {zai_cli-0.1.1 → zai_cli-0.1.3}/scripts/release_preflight.py +0 -0
  15. {zai_cli-0.1.1 → zai_cli-0.1.3}/setup.cfg +0 -0
  16. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_agent.py +0 -0
  17. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_browser.py +0 -0
  18. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_code_runner.py +0 -0
  19. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_config_main.py +0 -0
  20. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_core.py +0 -0
  21. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_errors.py +0 -0
  22. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_hooks_skills_session.py +0 -0
  23. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_integrations_cli.py +0 -0
  24. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_mcp.py +0 -0
  25. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_plugins.py +0 -0
  26. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_process.py +0 -0
  27. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_providers.py +0 -0
  28. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_release_preflight.py +0 -0
  29. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_search.py +0 -0
  30. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_security.py +0 -0
  31. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_settings_cli.py +0 -0
  32. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_storage.py +0 -0
  33. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_streaming.py +0 -0
  34. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_tool_schema.py +0 -0
  35. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_tools.py +0 -0
  36. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_undo.py +0 -0
  37. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_utilities.py +0 -0
  38. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_vision.py +0 -0
  39. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_watch.py +0 -0
  40. {zai_cli-0.1.1 → zai_cli-0.1.3}/tests/test_workflows.py +0 -0
  41. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/__main__.py +0 -0
  42. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/__init__.py +0 -0
  43. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/common.py +0 -0
  44. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/integrations.py +0 -0
  45. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/settings.py +0 -0
  46. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/utilities.py +0 -0
  47. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/cli/workflows.py +0 -0
  48. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/commit.md +0 -0
  49. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/explain.md +0 -0
  50. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/feature.md +0 -0
  51. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/fix.md +0 -0
  52. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/commands/review.md +0 -0
  53. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/config.py +0 -0
  54. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/__init__.py +0 -0
  55. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/agent.py +0 -0
  56. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/cancellation.py +0 -0
  57. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/commands.py +0 -0
  58. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/context.py +0 -0
  59. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/errors.py +0 -0
  60. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/fallback.py +0 -0
  61. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/hooks.py +0 -0
  62. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/memory.py +0 -0
  63. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/process.py +0 -0
  64. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/repomap.py +0 -0
  65. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/runtime.py +0 -0
  66. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/security.py +0 -0
  67. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/session.py +0 -0
  68. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/storage.py +0 -0
  69. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/streaming.py +0 -0
  70. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/tool_schema.py +0 -0
  71. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/undo.py +0 -0
  72. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/core/watch.py +0 -0
  73. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/main.py +0 -0
  74. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/mcp/__init__.py +0 -0
  75. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/mcp/client.py +0 -0
  76. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/mcp/manager.py +0 -0
  77. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/plugins/__init__.py +0 -0
  78. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/plugins/base.py +0 -0
  79. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/plugins/loader.py +0 -0
  80. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/__init__.py +0 -0
  81. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/anthropic.py +0 -0
  82. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/base.py +0 -0
  83. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/cerebras.py +0 -0
  84. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/gemini.py +0 -0
  85. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/groq.py +0 -0
  86. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/ollama.py +0 -0
  87. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/openai.py +0 -0
  88. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/openrouter.py +0 -0
  89. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/providers/qwen.py +0 -0
  90. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/skills/__init__.py +0 -0
  91. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/skills/registry.py +0 -0
  92. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/__init__.py +0 -0
  93. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/browser.py +0 -0
  94. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/code_runner.py +0 -0
  95. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/files.py +0 -0
  96. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/git.py +0 -0
  97. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/search.py +0 -0
  98. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/tools/vision.py +0 -0
  99. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai/ui/__init__.py +0 -0
  100. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/SOURCES.txt +0 -0
  101. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/dependency_links.txt +0 -0
  102. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/entry_points.txt +0 -0
  103. {zai_cli-0.1.1 → zai_cli-0.1.3}/zai_cli.egg-info/requires.txt +0 -0
  104. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zai-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Your personal AI CLI — free, fast, and smart
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/HumaizaNaz/zai_cli
@@ -11,7 +11,7 @@ def test_slash_command_completion():
11
11
  {"resume", "review", "help"},
12
12
  )
13
13
 
14
- assert candidates == [("/resume", "/resume", "command")]
14
+ assert candidates == [("/resume", "/resume", "slash command")]
15
15
 
16
16
 
17
17
  def test_model_completion():
@@ -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
- run_interactive("groq")
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
- assert "API key setup" in output
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
- "Run [cyan]zai setup[/cyan] or start Ollama.",
308
+ "Set up an API key now, run [cyan]zai setup[/cyan], or start Ollama.",
234
309
  border_style="red",
235
310
  ))
236
- return
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 commands[/dim]",
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
- console.print("[dim]Input cancelled.[/dim]")
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
- _show_setup_hint()
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
- for key, data in get_models().items():
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
- console.print("\n[yellow]Current operation cancelled.[/yellow]")
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
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zai-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Your personal AI CLI — free, fast, and smart
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/HumaizaNaz/zai_cli
@@ -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