zai-cli 0.1.7__tar.gz → 0.1.9__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.7 → zai_cli-0.1.9}/CHANGELOG.md +42 -0
- {zai_cli-0.1.7/zai_cli.egg-info → zai_cli-0.1.9}/PKG-INFO +89 -7
- {zai_cli-0.1.7 → zai_cli-0.1.9}/README.md +88 -6
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_interactive.py +86 -5
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_settings_cli.py +101 -4
- zai_cli-0.1.9/tests/test_tokens.py +57 -0
- zai_cli-0.1.9/zai/__init__.py +1 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/interactive.py +59 -37
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/settings.py +42 -43
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/setup_wizard.py +18 -2
- zai_cli-0.1.9/zai/cli/status.py +246 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/config.py +3 -0
- zai_cli-0.1.9/zai/core/compaction.py +83 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/context.py +16 -14
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/session.py +24 -0
- zai_cli-0.1.9/zai/core/tokens.py +158 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/main.py +9 -2
- {zai_cli-0.1.7 → zai_cli-0.1.9/zai_cli.egg-info}/PKG-INFO +89 -7
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/SOURCES.txt +4 -0
- zai_cli-0.1.7/zai/__init__.py +0 -1
- {zai_cli-0.1.7 → zai_cli-0.1.9}/LICENSE +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/MANIFEST.in +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/pyproject.toml +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/scripts/release_preflight.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/setup.cfg +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_agent.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_auth.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_browser.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_code_runner.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_config_main.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_core.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_errors.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_hooks_skills_session.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_input.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_integrations_cli.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_mcp.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_plugins.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_process.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_project_memory.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_providers.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_release_preflight.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_search.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_security.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_storage.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_streaming.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_tool_schema.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_tools.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_undo.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_utilities.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_vision.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_watch.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_workflows.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/__main__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/common.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/integrations.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/utilities.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/workflows.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/commit.md +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/explain.md +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/feature.md +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/fix.md +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/review.md +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/agent.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/auth.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/cancellation.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/commands.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/errors.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/fallback.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/hooks.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/memory.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/process.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/project_memory.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/repomap.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/runtime.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/security.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/storage.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/streaming.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/tool_schema.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/undo.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/watch.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/mcp/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/mcp/client.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/mcp/manager.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/plugins/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/plugins/base.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/plugins/loader.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/anthropic.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/base.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/cerebras.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/gemini.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/groq.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/ollama.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/openai.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/openrouter.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/qwen.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/skills/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/skills/registry.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/browser.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/code_runner.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/files.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/git.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/search.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/vision.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/ui/__init__.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/ui/input.py +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/dependency_links.txt +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/entry_points.txt +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/requires.txt +0 -0
- {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,48 @@ 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.9] - 2026-06-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added provider-neutral token estimation helpers and context usage reporting.
|
|
13
|
+
- Added `zai context tokens`, `zai context summary`, interactive
|
|
14
|
+
`/context tokens`, and interactive `/context summary`.
|
|
15
|
+
- Added interactive `/compact` to summarize older session history.
|
|
16
|
+
- Added `zai session compact <name-or-id>` for saved sessions.
|
|
17
|
+
- Added compacted-summary metadata to saved session payloads.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Automatic context compaction now uses configurable thresholds and prints a
|
|
22
|
+
transparent message when older history is summarized.
|
|
23
|
+
- Context status now shows estimated token usage, estimated context limit,
|
|
24
|
+
remaining tokens, and compacted-summary availability.
|
|
25
|
+
- Long-session summaries redact obvious API keys, bearer tokens, passwords, and
|
|
26
|
+
secret values before storage/display.
|
|
27
|
+
|
|
28
|
+
## [0.1.8] - 2026-06-29
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Added clearer provider/model status output for `zai model list` and
|
|
33
|
+
interactive `/model`, including provider, cloud/local status, actual model
|
|
34
|
+
ID, authentication state, and beginner-friendly notes.
|
|
35
|
+
- Added `zai context` and interactive `/context` to show the current folder,
|
|
36
|
+
project-memory status, repo-map/index status, and honest context visibility
|
|
37
|
+
notes.
|
|
38
|
+
- Added interactive `/providers` for provider connection status and next
|
|
39
|
+
actions.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Improved first-run setup copy to explain that `zai` connects to provider
|
|
44
|
+
backends and that the selected provider decides the model.
|
|
45
|
+
- Improved `zai auth status` and interactive `/auth status` with provider
|
|
46
|
+
next actions while continuing to avoid printing secret values.
|
|
47
|
+
- Expanded README provider/model/context guidance and clarified that repo maps
|
|
48
|
+
and project memory are not the same as loading every file into model context.
|
|
49
|
+
|
|
8
50
|
## [0.1.7] - 2026-06-28
|
|
9
51
|
|
|
10
52
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zai-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
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
|
|
@@ -63,6 +63,50 @@ An extensible AI coding assistant for the terminal.
|
|
|
63
63
|
project-specific sessions, switch between model providers, and integrate with
|
|
64
64
|
plugins and MCP servers.
|
|
65
65
|
|
|
66
|
+
## What is zai?
|
|
67
|
+
|
|
68
|
+
`zai` is the terminal CLI. It is not tied to one AI model. It connects to the
|
|
69
|
+
provider you configure, then uses that provider's selected model ID.
|
|
70
|
+
|
|
71
|
+
Cloud providers need their own API key. Ollama runs on your local machine and
|
|
72
|
+
does not need a cloud API key.
|
|
73
|
+
|
|
74
|
+
## Which model does zai use?
|
|
75
|
+
|
|
76
|
+
Run:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
zai model list
|
|
80
|
+
zai auth status
|
|
81
|
+
zai context
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The model list shows the alias, provider, display name, actual model ID, local
|
|
85
|
+
or cloud status, and whether a key is configured.
|
|
86
|
+
|
|
87
|
+
| Provider | Cloud/local | Key needed | Setup |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| Groq | Cloud | Yes | `zai auth login groq` |
|
|
90
|
+
| Gemini | Cloud | Yes | `zai auth login gemini` |
|
|
91
|
+
| OpenAI | Cloud | Yes | `zai auth login openai` |
|
|
92
|
+
| Anthropic/Claude | Cloud | Yes | `zai auth login anthropic` |
|
|
93
|
+
| OpenRouter | Cloud | Yes | `zai auth login openrouter` |
|
|
94
|
+
| Cerebras | Cloud | Yes | `zai auth login cerebras` |
|
|
95
|
+
| Qwen/DashScope | Cloud | Yes | `zai auth login qwen` |
|
|
96
|
+
| Ollama | Local | No cloud key | Start Ollama, then `zai model test ollama` |
|
|
97
|
+
|
|
98
|
+
Quick tester flow:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install --upgrade --no-cache-dir zai-cli
|
|
102
|
+
zai --version
|
|
103
|
+
zai
|
|
104
|
+
/help
|
|
105
|
+
/model
|
|
106
|
+
/auth status
|
|
107
|
+
/context
|
|
108
|
+
```
|
|
109
|
+
|
|
66
110
|
## Requirements
|
|
67
111
|
|
|
68
112
|
- Python 3.10 or newer
|
|
@@ -348,6 +392,7 @@ zai auth status Show API-key and external CLI auth status
|
|
|
348
392
|
zai auth login <provider> Connect a provider API key
|
|
349
393
|
zai auth logout <provider> Remove a provider API key from zai
|
|
350
394
|
zai auth doctor Show auth troubleshooting guidance
|
|
395
|
+
zai context Show loaded project context status
|
|
351
396
|
zai memory show Show last task metadata
|
|
352
397
|
zai memory projects List remembered projects
|
|
353
398
|
zai memory project Show project-scoped memory
|
|
@@ -435,6 +480,8 @@ Inside `zai` interactive mode:
|
|
|
435
480
|
|
|
436
481
|
/model list List models
|
|
437
482
|
/model <name> Change model for this interactive session
|
|
483
|
+
/providers Show provider connection status
|
|
484
|
+
/context Show loaded project context status
|
|
438
485
|
/memory Show last task metadata
|
|
439
486
|
/commands List built-in and plugin slash commands
|
|
440
487
|
```
|
|
@@ -450,22 +497,57 @@ Interactive input also provides:
|
|
|
450
497
|
- `Ctrl+C` to cancel the current input or operation without ending the session;
|
|
451
498
|
- a basic Rich prompt fallback in redirected or unsupported terminals.
|
|
452
499
|
|
|
453
|
-
##
|
|
500
|
+
## How much of my project does zai see?
|
|
501
|
+
|
|
502
|
+
`zai` starts from the folder where you run it. It does not load every file in a
|
|
503
|
+
large repository into every prompt.
|
|
504
|
+
|
|
505
|
+
Use `zai context` or `/context` to inspect the current folder, whether project
|
|
506
|
+
memory exists, whether a repo-map cache is available, and how many conversation
|
|
507
|
+
messages are loaded in the current interactive session.
|
|
508
|
+
|
|
509
|
+
For token/context usage:
|
|
510
|
+
|
|
511
|
+
```text
|
|
512
|
+
zai context tokens
|
|
513
|
+
zai context summary
|
|
514
|
+
/context tokens
|
|
515
|
+
/context summary
|
|
516
|
+
/compact
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
`zai context tokens` and `/context tokens` show an approximate provider-neutral
|
|
520
|
+
token estimate, the configured context limit, remaining tokens, and whether the
|
|
521
|
+
session is approaching the compaction threshold.
|
|
522
|
+
|
|
523
|
+
Project memory is separate from current model context. It is a small
|
|
524
|
+
`.zai/project_memory.json` summary created with `zai memory refresh` or
|
|
525
|
+
`/memory refresh`.
|
|
526
|
+
|
|
527
|
+
Repo maps are summaries/indexes used on demand for repository questions. They
|
|
528
|
+
are not full source files loaded into every model call. If a file has not been
|
|
529
|
+
read or indexed for the current task, treat it as not loaded yet.
|
|
530
|
+
|
|
531
|
+
For long interactive sessions, `/compact` summarizes older conversation and
|
|
532
|
+
tool history so the active session can continue with less context pressure. The
|
|
533
|
+
summary preserves the goal, decisions, recent work, commands/results where
|
|
534
|
+
visible, and pending context in a compact form.
|
|
454
535
|
|
|
455
|
-
All chat, workflow, utility, and agent requests use
|
|
456
|
-
|
|
536
|
+
All chat, workflow, utility, and agent requests use a model-aware context
|
|
537
|
+
pipeline:
|
|
457
538
|
|
|
458
539
|
- the active provider's configured context window sets the request budget;
|
|
459
540
|
- system prompts, tool schemas, and output space are reserved before messages;
|
|
460
541
|
- initial project instructions and explicitly pinned messages are retained;
|
|
461
542
|
- assistant tool calls remain paired with their tool-result messages;
|
|
462
543
|
- oversized tool results keep their beginning and end with an omission marker;
|
|
463
|
-
-
|
|
464
|
-
verbatim;
|
|
544
|
+
- requests are bounded to fit the selected model's context limit;
|
|
465
545
|
- context usage uses a conservative shared token estimate, while provider
|
|
466
546
|
response usage is retained where the SDK reports it.
|
|
467
547
|
|
|
468
|
-
This
|
|
548
|
+
This keeps requests bounded. Compaction is still approximate, so save important
|
|
549
|
+
stable project facts explicitly with `/remember <fact>` or `zai memory remember
|
|
550
|
+
<fact>`.
|
|
469
551
|
|
|
470
552
|
## Providers and fallback
|
|
471
553
|
|
|
@@ -6,6 +6,50 @@ An extensible AI coding assistant for the terminal.
|
|
|
6
6
|
project-specific sessions, switch between model providers, and integrate with
|
|
7
7
|
plugins and MCP servers.
|
|
8
8
|
|
|
9
|
+
## What is zai?
|
|
10
|
+
|
|
11
|
+
`zai` is the terminal CLI. It is not tied to one AI model. It connects to the
|
|
12
|
+
provider you configure, then uses that provider's selected model ID.
|
|
13
|
+
|
|
14
|
+
Cloud providers need their own API key. Ollama runs on your local machine and
|
|
15
|
+
does not need a cloud API key.
|
|
16
|
+
|
|
17
|
+
## Which model does zai use?
|
|
18
|
+
|
|
19
|
+
Run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
zai model list
|
|
23
|
+
zai auth status
|
|
24
|
+
zai context
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The model list shows the alias, provider, display name, actual model ID, local
|
|
28
|
+
or cloud status, and whether a key is configured.
|
|
29
|
+
|
|
30
|
+
| Provider | Cloud/local | Key needed | Setup |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| Groq | Cloud | Yes | `zai auth login groq` |
|
|
33
|
+
| Gemini | Cloud | Yes | `zai auth login gemini` |
|
|
34
|
+
| OpenAI | Cloud | Yes | `zai auth login openai` |
|
|
35
|
+
| Anthropic/Claude | Cloud | Yes | `zai auth login anthropic` |
|
|
36
|
+
| OpenRouter | Cloud | Yes | `zai auth login openrouter` |
|
|
37
|
+
| Cerebras | Cloud | Yes | `zai auth login cerebras` |
|
|
38
|
+
| Qwen/DashScope | Cloud | Yes | `zai auth login qwen` |
|
|
39
|
+
| Ollama | Local | No cloud key | Start Ollama, then `zai model test ollama` |
|
|
40
|
+
|
|
41
|
+
Quick tester flow:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install --upgrade --no-cache-dir zai-cli
|
|
45
|
+
zai --version
|
|
46
|
+
zai
|
|
47
|
+
/help
|
|
48
|
+
/model
|
|
49
|
+
/auth status
|
|
50
|
+
/context
|
|
51
|
+
```
|
|
52
|
+
|
|
9
53
|
## Requirements
|
|
10
54
|
|
|
11
55
|
- Python 3.10 or newer
|
|
@@ -291,6 +335,7 @@ zai auth status Show API-key and external CLI auth status
|
|
|
291
335
|
zai auth login <provider> Connect a provider API key
|
|
292
336
|
zai auth logout <provider> Remove a provider API key from zai
|
|
293
337
|
zai auth doctor Show auth troubleshooting guidance
|
|
338
|
+
zai context Show loaded project context status
|
|
294
339
|
zai memory show Show last task metadata
|
|
295
340
|
zai memory projects List remembered projects
|
|
296
341
|
zai memory project Show project-scoped memory
|
|
@@ -378,6 +423,8 @@ Inside `zai` interactive mode:
|
|
|
378
423
|
|
|
379
424
|
/model list List models
|
|
380
425
|
/model <name> Change model for this interactive session
|
|
426
|
+
/providers Show provider connection status
|
|
427
|
+
/context Show loaded project context status
|
|
381
428
|
/memory Show last task metadata
|
|
382
429
|
/commands List built-in and plugin slash commands
|
|
383
430
|
```
|
|
@@ -393,22 +440,57 @@ Interactive input also provides:
|
|
|
393
440
|
- `Ctrl+C` to cancel the current input or operation without ending the session;
|
|
394
441
|
- a basic Rich prompt fallback in redirected or unsupported terminals.
|
|
395
442
|
|
|
396
|
-
##
|
|
443
|
+
## How much of my project does zai see?
|
|
444
|
+
|
|
445
|
+
`zai` starts from the folder where you run it. It does not load every file in a
|
|
446
|
+
large repository into every prompt.
|
|
447
|
+
|
|
448
|
+
Use `zai context` or `/context` to inspect the current folder, whether project
|
|
449
|
+
memory exists, whether a repo-map cache is available, and how many conversation
|
|
450
|
+
messages are loaded in the current interactive session.
|
|
451
|
+
|
|
452
|
+
For token/context usage:
|
|
453
|
+
|
|
454
|
+
```text
|
|
455
|
+
zai context tokens
|
|
456
|
+
zai context summary
|
|
457
|
+
/context tokens
|
|
458
|
+
/context summary
|
|
459
|
+
/compact
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`zai context tokens` and `/context tokens` show an approximate provider-neutral
|
|
463
|
+
token estimate, the configured context limit, remaining tokens, and whether the
|
|
464
|
+
session is approaching the compaction threshold.
|
|
465
|
+
|
|
466
|
+
Project memory is separate from current model context. It is a small
|
|
467
|
+
`.zai/project_memory.json` summary created with `zai memory refresh` or
|
|
468
|
+
`/memory refresh`.
|
|
469
|
+
|
|
470
|
+
Repo maps are summaries/indexes used on demand for repository questions. They
|
|
471
|
+
are not full source files loaded into every model call. If a file has not been
|
|
472
|
+
read or indexed for the current task, treat it as not loaded yet.
|
|
473
|
+
|
|
474
|
+
For long interactive sessions, `/compact` summarizes older conversation and
|
|
475
|
+
tool history so the active session can continue with less context pressure. The
|
|
476
|
+
summary preserves the goal, decisions, recent work, commands/results where
|
|
477
|
+
visible, and pending context in a compact form.
|
|
397
478
|
|
|
398
|
-
All chat, workflow, utility, and agent requests use
|
|
399
|
-
|
|
479
|
+
All chat, workflow, utility, and agent requests use a model-aware context
|
|
480
|
+
pipeline:
|
|
400
481
|
|
|
401
482
|
- the active provider's configured context window sets the request budget;
|
|
402
483
|
- system prompts, tool schemas, and output space are reserved before messages;
|
|
403
484
|
- initial project instructions and explicitly pinned messages are retained;
|
|
404
485
|
- assistant tool calls remain paired with their tool-result messages;
|
|
405
486
|
- oversized tool results keep their beginning and end with an omission marker;
|
|
406
|
-
-
|
|
407
|
-
verbatim;
|
|
487
|
+
- requests are bounded to fit the selected model's context limit;
|
|
408
488
|
- context usage uses a conservative shared token estimate, while provider
|
|
409
489
|
response usage is retained where the SDK reports it.
|
|
410
490
|
|
|
411
|
-
This
|
|
491
|
+
This keeps requests bounded. Compaction is still approximate, so save important
|
|
492
|
+
stable project facts explicitly with `/remember <fact>` or `zai memory remember
|
|
493
|
+
<fact>`.
|
|
412
494
|
|
|
413
495
|
## Providers and fallback
|
|
414
496
|
|
|
@@ -83,10 +83,15 @@ def test_interactive_key_setup_saves_entered_key(capsys):
|
|
|
83
83
|
|
|
84
84
|
with patch("zai.cli.setup_wizard.get_api_key", return_value=None):
|
|
85
85
|
with patch("zai.cli.setup_wizard.Confirm.ask", return_value=False):
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
with patch("zai.cli.setup_wizard.Prompt.ask", side_effect=lambda *a, **k: next(answers)):
|
|
87
|
+
with patch("zai.cli.setup_wizard.save_provider_key") as save_key:
|
|
88
|
+
with patch("zai.cli.setup_wizard.has_available_provider", side_effect=[False, True, True]):
|
|
89
|
+
with patch(
|
|
90
|
+
"zai.cli.setup_wizard.load_config",
|
|
91
|
+
return_value={"default_model": "gemini"},
|
|
92
|
+
):
|
|
93
|
+
with patch("zai.cli.setup_wizard.save_config"):
|
|
94
|
+
assert _run_key_setup_interactive()
|
|
90
95
|
|
|
91
96
|
output = capsys.readouterr().out
|
|
92
97
|
assert "zai setup" in output
|
|
@@ -227,13 +232,89 @@ def test_interactive_auth_commands_do_not_call_ai(tmp_path, monkeypatch, capsys)
|
|
|
227
232
|
run_interactive("groq")
|
|
228
233
|
|
|
229
234
|
output = capsys.readouterr().out
|
|
230
|
-
assert "
|
|
235
|
+
assert "Provider status" in output
|
|
231
236
|
assert "Saved Gemini API key" in output
|
|
232
237
|
save.assert_called_once_with("gemini", "gemini-key")
|
|
233
238
|
delete.assert_called_once_with("gemini")
|
|
234
239
|
run_agent.assert_not_called()
|
|
235
240
|
|
|
236
241
|
|
|
242
|
+
def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch, capsys):
|
|
243
|
+
monkeypatch.chdir(tmp_path)
|
|
244
|
+
(tmp_path / "app.py").write_text("print('hi')\n")
|
|
245
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
246
|
+
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
247
|
+
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
248
|
+
with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
|
|
249
|
+
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
250
|
+
with patch(
|
|
251
|
+
"zai.cli.interactive.InteractiveInput.prompt",
|
|
252
|
+
side_effect=[
|
|
253
|
+
"/providers",
|
|
254
|
+
"/context",
|
|
255
|
+
"/context tokens",
|
|
256
|
+
"/context summary",
|
|
257
|
+
EOFError(),
|
|
258
|
+
],
|
|
259
|
+
):
|
|
260
|
+
with patch("zai.cli.interactive.run_agent") as run_agent:
|
|
261
|
+
run_interactive("groq")
|
|
262
|
+
|
|
263
|
+
output = capsys.readouterr().out
|
|
264
|
+
assert "Provider status" in output
|
|
265
|
+
assert "Project context" in output
|
|
266
|
+
assert "unread files are not loaded" in output
|
|
267
|
+
assert "Estimated tokens used" in output
|
|
268
|
+
assert "No compacted summary" in output
|
|
269
|
+
assert "app.py is loaded" not in output
|
|
270
|
+
run_agent.assert_not_called()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_interactive_manual_compact_summarizes_history(tmp_path, monkeypatch, capsys):
|
|
274
|
+
monkeypatch.chdir(tmp_path)
|
|
275
|
+
prompts = [
|
|
276
|
+
"task one",
|
|
277
|
+
"task two",
|
|
278
|
+
"task three",
|
|
279
|
+
"task four",
|
|
280
|
+
"task five",
|
|
281
|
+
"/compact",
|
|
282
|
+
"/context summary",
|
|
283
|
+
EOFError(),
|
|
284
|
+
]
|
|
285
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
286
|
+
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
287
|
+
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
288
|
+
with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
|
|
289
|
+
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
290
|
+
with patch(
|
|
291
|
+
"zai.cli.interactive.InteractiveInput.prompt",
|
|
292
|
+
side_effect=prompts,
|
|
293
|
+
):
|
|
294
|
+
with (
|
|
295
|
+
patch(
|
|
296
|
+
"zai.cli.interactive.run_agent",
|
|
297
|
+
side_effect=[
|
|
298
|
+
"answer one",
|
|
299
|
+
"answer two",
|
|
300
|
+
"answer three",
|
|
301
|
+
"answer four",
|
|
302
|
+
"answer five",
|
|
303
|
+
],
|
|
304
|
+
),
|
|
305
|
+
patch(
|
|
306
|
+
"zai.cli.interactive.Confirm.ask",
|
|
307
|
+
return_value=False,
|
|
308
|
+
),
|
|
309
|
+
):
|
|
310
|
+
run_interactive("groq")
|
|
311
|
+
|
|
312
|
+
output = capsys.readouterr().out
|
|
313
|
+
assert "Context compacted" in output
|
|
314
|
+
assert "Current compacted summary" in output
|
|
315
|
+
assert "task one" in output
|
|
316
|
+
|
|
317
|
+
|
|
237
318
|
def test_interactive_offers_project_memory_after_agent_task(tmp_path, monkeypatch, capsys):
|
|
238
319
|
from zai.core.project_memory import load_project_memory
|
|
239
320
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from io import StringIO
|
|
1
2
|
from unittest.mock import MagicMock, patch
|
|
2
3
|
|
|
3
4
|
import typer
|
|
@@ -26,6 +27,7 @@ def test_setup_wizard_saves_entered_key():
|
|
|
26
27
|
from zai.cli.setup_wizard import run_setup_wizard
|
|
27
28
|
|
|
28
29
|
answers = iter(["groq", "groq-key", "groq", "done"])
|
|
30
|
+
console_file = StringIO()
|
|
29
31
|
with (
|
|
30
32
|
patch("zai.cli.setup_wizard.has_available_provider", side_effect=[False, True, True]),
|
|
31
33
|
patch("zai.cli.setup_wizard.get_api_key", return_value=None),
|
|
@@ -38,8 +40,12 @@ def test_setup_wizard_saves_entered_key():
|
|
|
38
40
|
patch("zai.cli.setup_wizard.load_config", return_value={"default_model": "gemini"}),
|
|
39
41
|
patch("zai.cli.setup_wizard.save_config") as save_config,
|
|
40
42
|
):
|
|
41
|
-
assert run_setup_wizard(console=Console(file=
|
|
43
|
+
assert run_setup_wizard(console=Console(file=console_file)) is True
|
|
42
44
|
|
|
45
|
+
output = console_file.getvalue()
|
|
46
|
+
assert "zai is the CLI" in output
|
|
47
|
+
assert "Cloud providers need their own API key" in output
|
|
48
|
+
assert "Active provider/model" in output
|
|
43
49
|
save_key.assert_called_once_with("groq", "groq-key")
|
|
44
50
|
save_config.assert_called()
|
|
45
51
|
|
|
@@ -79,11 +85,13 @@ def test_model_list_set_fallback_and_usage():
|
|
|
79
85
|
):
|
|
80
86
|
selected = runner.invoke(app, ["model", "set", "groq"])
|
|
81
87
|
fallback = runner.invoke(app, ["model", "fallback", "off"])
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
with patch("zai.cli.settings.load_config", return_value=config):
|
|
89
|
+
unknown = runner.invoke(app, ["model", "set", "missing"])
|
|
90
|
+
usage = runner.invoke(app, ["model", "fallback", "maybe"])
|
|
84
91
|
|
|
85
92
|
assert "Available Models" in listed.output
|
|
86
|
-
assert "
|
|
93
|
+
assert "Model ID" in listed.output
|
|
94
|
+
assert "gemini" in listed.output
|
|
87
95
|
assert "Default model set to" in selected.output
|
|
88
96
|
assert "Automatic model fallback" in fallback.output
|
|
89
97
|
assert config["default_model"] == "groq"
|
|
@@ -93,6 +101,74 @@ def test_model_list_set_fallback_and_usage():
|
|
|
93
101
|
assert "Usage: zai model" in usage.output
|
|
94
102
|
|
|
95
103
|
|
|
104
|
+
def test_model_list_explains_provider_model_and_auth_without_secret(monkeypatch):
|
|
105
|
+
app = settings_app()
|
|
106
|
+
config = {
|
|
107
|
+
"default_model": "groq",
|
|
108
|
+
"fallback_order": ["groq"],
|
|
109
|
+
"model_overrides": {},
|
|
110
|
+
"auto_fallback": True,
|
|
111
|
+
}
|
|
112
|
+
monkeypatch.setenv("GROQ_API_KEY", "sk-secret-value")
|
|
113
|
+
|
|
114
|
+
with patch("zai.cli.settings.load_config", return_value=config):
|
|
115
|
+
result = CliRunner().invoke(app, ["model", "list"])
|
|
116
|
+
|
|
117
|
+
assert result.exit_code == 0
|
|
118
|
+
assert "Provider" in result.output
|
|
119
|
+
assert "Model ID" in result.output
|
|
120
|
+
assert "cloud" in result.output
|
|
121
|
+
assert "openai/gpt-oss-120b" in result.output
|
|
122
|
+
assert "key configured" in result.output
|
|
123
|
+
assert "sk-secret-value" not in result.output
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_auth_status_shows_provider_next_actions_without_secret(monkeypatch):
|
|
127
|
+
app = settings_app()
|
|
128
|
+
monkeypatch.setenv("GEMINI_API_KEY", "gemini-secret")
|
|
129
|
+
|
|
130
|
+
result = CliRunner().invoke(app, ["auth", "status"])
|
|
131
|
+
|
|
132
|
+
assert result.exit_code == 0
|
|
133
|
+
assert "Provider status" in result.output
|
|
134
|
+
assert "auth login" in result.output
|
|
135
|
+
assert "groq" in result.output
|
|
136
|
+
assert "key configured" in result.output
|
|
137
|
+
assert "gemini-secret" not in result.output
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_context_command_is_honest_about_unloaded_files(tmp_path, monkeypatch):
|
|
141
|
+
app = settings_app()
|
|
142
|
+
monkeypatch.chdir(tmp_path)
|
|
143
|
+
(tmp_path / "app.py").write_text("print('hi')\n")
|
|
144
|
+
|
|
145
|
+
result = CliRunner().invoke(app, ["context"])
|
|
146
|
+
|
|
147
|
+
assert result.exit_code == 0
|
|
148
|
+
assert f"Folder: {tmp_path}" in result.output
|
|
149
|
+
assert "Project memory: missing" in result.output
|
|
150
|
+
assert "Repo map/index: not loaded yet" in result.output
|
|
151
|
+
assert "unread files are not loaded" in result.output
|
|
152
|
+
assert "Estimated tokens used" in result.output
|
|
153
|
+
assert "app.py is loaded" not in result.output
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_context_tokens_and_summary_commands(tmp_path, monkeypatch):
|
|
157
|
+
app = settings_app()
|
|
158
|
+
monkeypatch.chdir(tmp_path)
|
|
159
|
+
|
|
160
|
+
tokens = CliRunner().invoke(app, ["context", "tokens"])
|
|
161
|
+
summary = CliRunner().invoke(app, ["context", "summary"])
|
|
162
|
+
bad = CliRunner().invoke(app, ["context", "bad"])
|
|
163
|
+
|
|
164
|
+
assert tokens.exit_code == 0
|
|
165
|
+
assert "Estimated tokens used" in tokens.output
|
|
166
|
+
assert "Compacted summary" in tokens.output
|
|
167
|
+
assert summary.exit_code == 0
|
|
168
|
+
assert "No compacted summary" in summary.output
|
|
169
|
+
assert "Usage: zai context" in bad.output
|
|
170
|
+
|
|
171
|
+
|
|
96
172
|
def test_model_check_reports_valid_and_invalid_configuration():
|
|
97
173
|
app = settings_app()
|
|
98
174
|
runner = CliRunner()
|
|
@@ -349,6 +425,27 @@ def test_session_rename_delete_and_export():
|
|
|
349
425
|
assert "Export failed" in export_failed.output
|
|
350
426
|
|
|
351
427
|
|
|
428
|
+
def test_session_compact_routes_history():
|
|
429
|
+
from zai.providers.base import Message
|
|
430
|
+
|
|
431
|
+
app = settings_app()
|
|
432
|
+
history = [
|
|
433
|
+
Message(role="user", content=f"message {index}")
|
|
434
|
+
for index in range(12)
|
|
435
|
+
]
|
|
436
|
+
info = {"name": "auth", "project_path": "project"}
|
|
437
|
+
with (
|
|
438
|
+
patch("zai.cli.settings.load_history", return_value=history),
|
|
439
|
+
patch("zai.cli.settings.get_session_info", return_value=info),
|
|
440
|
+
patch("zai.cli.settings.save_history") as save,
|
|
441
|
+
):
|
|
442
|
+
result = CliRunner().invoke(app, ["session", "compact", "auth"])
|
|
443
|
+
|
|
444
|
+
assert result.exit_code == 0
|
|
445
|
+
assert "Session compacted" in result.output
|
|
446
|
+
save.assert_called_once()
|
|
447
|
+
|
|
448
|
+
|
|
352
449
|
def test_undo_list_empty_and_populated():
|
|
353
450
|
app = settings_app()
|
|
354
451
|
runner = CliRunner()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from zai.core.compaction import manual_compact_messages
|
|
2
|
+
from zai.core.tokens import (
|
|
3
|
+
compacted_summaries,
|
|
4
|
+
context_usage,
|
|
5
|
+
estimate_messages_tokens,
|
|
6
|
+
estimate_tokens,
|
|
7
|
+
redact_secrets,
|
|
8
|
+
)
|
|
9
|
+
from zai.providers.base import Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_estimate_tokens_is_stable_and_nonzero():
|
|
13
|
+
assert estimate_tokens("") == 0
|
|
14
|
+
assert estimate_tokens("hello world") >= 2
|
|
15
|
+
assert estimate_tokens("x" * 120) >= 40
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_context_usage_reports_thresholds():
|
|
19
|
+
messages = [Message(role="user", content="x" * 900)]
|
|
20
|
+
usage = context_usage(messages, context_limit=100)
|
|
21
|
+
|
|
22
|
+
assert usage.tokens_used == estimate_messages_tokens(messages)
|
|
23
|
+
assert usage.tokens_remaining == 0
|
|
24
|
+
assert usage.should_warn is True
|
|
25
|
+
assert usage.should_compact is True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_secret_redaction_covers_common_patterns():
|
|
29
|
+
text = (
|
|
30
|
+
"OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz "
|
|
31
|
+
"Authorization: Bearer abcdefghijklmnopqrstuvwxyz "
|
|
32
|
+
"password=hunter2"
|
|
33
|
+
)
|
|
34
|
+
redacted = redact_secrets(text)
|
|
35
|
+
|
|
36
|
+
assert "sk-abcdefghijklmnopqrstuvwxyz" not in redacted
|
|
37
|
+
assert "abcdefghijklmnopqrstuvwxyz" not in redacted
|
|
38
|
+
assert "hunter2" not in redacted
|
|
39
|
+
assert "[REDACTED_SECRET]" in redacted
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_manual_compaction_creates_summary_and_keeps_recent_messages():
|
|
43
|
+
messages = [
|
|
44
|
+
Message(role="user", content="initial goal"),
|
|
45
|
+
Message(role="assistant", content="decision made"),
|
|
46
|
+
Message(role="tool", content="api_key=sk-secretsecretsecret", tool_name="run"),
|
|
47
|
+
]
|
|
48
|
+
messages.extend(Message(role="user", content=f"step {index}") for index in range(12))
|
|
49
|
+
|
|
50
|
+
compacted, summary = manual_compact_messages(messages, max_tokens=128_000, keep_recent=4)
|
|
51
|
+
|
|
52
|
+
assert summary is not None
|
|
53
|
+
assert "initial goal" in summary.content
|
|
54
|
+
assert "decision made" in summary.content
|
|
55
|
+
assert "sk-secretsecretsecret" not in summary.content
|
|
56
|
+
assert compacted[-1].content == "step 11"
|
|
57
|
+
assert compacted_summaries(compacted)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.9"
|