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.
Files changed (113) hide show
  1. {zai_cli-0.1.7 → zai_cli-0.1.9}/CHANGELOG.md +42 -0
  2. {zai_cli-0.1.7/zai_cli.egg-info → zai_cli-0.1.9}/PKG-INFO +89 -7
  3. {zai_cli-0.1.7 → zai_cli-0.1.9}/README.md +88 -6
  4. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_interactive.py +86 -5
  5. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_settings_cli.py +101 -4
  6. zai_cli-0.1.9/tests/test_tokens.py +57 -0
  7. zai_cli-0.1.9/zai/__init__.py +1 -0
  8. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/interactive.py +59 -37
  9. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/settings.py +42 -43
  10. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/setup_wizard.py +18 -2
  11. zai_cli-0.1.9/zai/cli/status.py +246 -0
  12. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/config.py +3 -0
  13. zai_cli-0.1.9/zai/core/compaction.py +83 -0
  14. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/context.py +16 -14
  15. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/session.py +24 -0
  16. zai_cli-0.1.9/zai/core/tokens.py +158 -0
  17. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/main.py +9 -2
  18. {zai_cli-0.1.7 → zai_cli-0.1.9/zai_cli.egg-info}/PKG-INFO +89 -7
  19. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/SOURCES.txt +4 -0
  20. zai_cli-0.1.7/zai/__init__.py +0 -1
  21. {zai_cli-0.1.7 → zai_cli-0.1.9}/LICENSE +0 -0
  22. {zai_cli-0.1.7 → zai_cli-0.1.9}/MANIFEST.in +0 -0
  23. {zai_cli-0.1.7 → zai_cli-0.1.9}/pyproject.toml +0 -0
  24. {zai_cli-0.1.7 → zai_cli-0.1.9}/scripts/release_preflight.py +0 -0
  25. {zai_cli-0.1.7 → zai_cli-0.1.9}/setup.cfg +0 -0
  26. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_agent.py +0 -0
  27. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_auth.py +0 -0
  28. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_browser.py +0 -0
  29. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_code_runner.py +0 -0
  30. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_config_main.py +0 -0
  31. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_core.py +0 -0
  32. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_errors.py +0 -0
  33. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_hooks_skills_session.py +0 -0
  34. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_input.py +0 -0
  35. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_integrations_cli.py +0 -0
  36. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_mcp.py +0 -0
  37. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_plugins.py +0 -0
  38. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_process.py +0 -0
  39. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_project_memory.py +0 -0
  40. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_providers.py +0 -0
  41. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_release_preflight.py +0 -0
  42. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_search.py +0 -0
  43. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_security.py +0 -0
  44. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_storage.py +0 -0
  45. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_streaming.py +0 -0
  46. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_tool_schema.py +0 -0
  47. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_tools.py +0 -0
  48. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_undo.py +0 -0
  49. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_utilities.py +0 -0
  50. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_vision.py +0 -0
  51. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_watch.py +0 -0
  52. {zai_cli-0.1.7 → zai_cli-0.1.9}/tests/test_workflows.py +0 -0
  53. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/__main__.py +0 -0
  54. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/__init__.py +0 -0
  55. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/common.py +0 -0
  56. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/integrations.py +0 -0
  57. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/utilities.py +0 -0
  58. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/cli/workflows.py +0 -0
  59. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/commit.md +0 -0
  60. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/explain.md +0 -0
  61. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/feature.md +0 -0
  62. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/fix.md +0 -0
  63. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/commands/review.md +0 -0
  64. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/__init__.py +0 -0
  65. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/agent.py +0 -0
  66. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/auth.py +0 -0
  67. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/cancellation.py +0 -0
  68. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/commands.py +0 -0
  69. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/errors.py +0 -0
  70. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/fallback.py +0 -0
  71. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/hooks.py +0 -0
  72. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/memory.py +0 -0
  73. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/process.py +0 -0
  74. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/project_memory.py +0 -0
  75. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/repomap.py +0 -0
  76. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/runtime.py +0 -0
  77. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/security.py +0 -0
  78. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/storage.py +0 -0
  79. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/streaming.py +0 -0
  80. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/tool_schema.py +0 -0
  81. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/undo.py +0 -0
  82. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/core/watch.py +0 -0
  83. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/mcp/__init__.py +0 -0
  84. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/mcp/client.py +0 -0
  85. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/mcp/manager.py +0 -0
  86. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/plugins/__init__.py +0 -0
  87. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/plugins/base.py +0 -0
  88. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/plugins/loader.py +0 -0
  89. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/__init__.py +0 -0
  90. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/anthropic.py +0 -0
  91. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/base.py +0 -0
  92. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/cerebras.py +0 -0
  93. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/gemini.py +0 -0
  94. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/groq.py +0 -0
  95. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/ollama.py +0 -0
  96. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/openai.py +0 -0
  97. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/openrouter.py +0 -0
  98. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/providers/qwen.py +0 -0
  99. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/skills/__init__.py +0 -0
  100. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/skills/registry.py +0 -0
  101. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/__init__.py +0 -0
  102. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/browser.py +0 -0
  103. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/code_runner.py +0 -0
  104. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/files.py +0 -0
  105. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/git.py +0 -0
  106. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/search.py +0 -0
  107. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/tools/vision.py +0 -0
  108. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/ui/__init__.py +0 -0
  109. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai/ui/input.py +0 -0
  110. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/dependency_links.txt +0 -0
  111. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/entry_points.txt +0 -0
  112. {zai_cli-0.1.7 → zai_cli-0.1.9}/zai_cli.egg-info/requires.txt +0 -0
  113. {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.7
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
- ## Context management
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 the same model-aware
456
- context pipeline:
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
- - older turns are compacted into a structured summary while recent turns stay
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 replaces the previous fixed 100k limit and manual last-message truncation.
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
- ## Context management
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 the same model-aware
399
- context pipeline:
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
- - older turns are compacted into a structured summary while recent turns stay
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 replaces the previous fixed 100k limit and manual last-message truncation.
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
- 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
- assert _run_key_setup_interactive()
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 "Authentication status" in output
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=MagicMock())) is True
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
- unknown = runner.invoke(app, ["model", "set", "missing"])
83
- usage = runner.invoke(app, ["model", "fallback", "maybe"])
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 "gemini-3.5-flash" in listed.output
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"