zai-cli 0.1.8__tar.gz → 0.2.0__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.8 → zai_cli-0.2.0}/CHANGELOG.md +37 -0
- {zai_cli-0.1.8/zai_cli.egg-info → zai_cli-0.2.0}/PKG-INFO +63 -4
- {zai_cli-0.1.8 → zai_cli-0.2.0}/README.md +62 -3
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_interactive.py +122 -34
- zai_cli-0.2.0/tests/test_project_rules.py +33 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_settings_cli.py +54 -0
- zai_cli-0.2.0/tests/test_tokens.py +57 -0
- zai_cli-0.2.0/zai/__init__.py +1 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/interactive.py +68 -5
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/settings.py +45 -2
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/status.py +52 -1
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/config.py +3 -0
- zai_cli-0.2.0/zai/core/compaction.py +83 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/context.py +16 -14
- zai_cli-0.2.0/zai/core/project_rules.py +111 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/session.py +24 -0
- zai_cli-0.2.0/zai/core/tokens.py +158 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/main.py +20 -2
- {zai_cli-0.1.8 → zai_cli-0.2.0/zai_cli.egg-info}/PKG-INFO +63 -4
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/SOURCES.txt +5 -0
- zai_cli-0.1.8/zai/__init__.py +0 -1
- {zai_cli-0.1.8 → zai_cli-0.2.0}/LICENSE +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/MANIFEST.in +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/pyproject.toml +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/scripts/release_preflight.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/setup.cfg +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_agent.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_auth.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_browser.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_code_runner.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_config_main.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_core.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_errors.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_hooks_skills_session.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_input.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_integrations_cli.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_mcp.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_plugins.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_process.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_project_memory.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_providers.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_release_preflight.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_search.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_security.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_storage.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_streaming.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_tool_schema.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_tools.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_undo.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_utilities.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_vision.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_watch.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_workflows.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/__main__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/common.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/integrations.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/setup_wizard.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/utilities.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/workflows.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/commit.md +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/explain.md +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/feature.md +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/fix.md +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/review.md +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/agent.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/auth.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/cancellation.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/commands.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/errors.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/fallback.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/hooks.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/memory.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/process.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/project_memory.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/repomap.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/runtime.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/security.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/storage.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/streaming.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/tool_schema.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/undo.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/watch.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/mcp/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/mcp/client.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/mcp/manager.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/plugins/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/plugins/base.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/plugins/loader.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/anthropic.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/base.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/cerebras.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/gemini.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/groq.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/ollama.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/openai.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/openrouter.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/qwen.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/skills/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/skills/registry.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/browser.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/code_runner.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/files.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/git.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/search.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/vision.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/ui/__init__.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/ui/input.py +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/dependency_links.txt +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/entry_points.txt +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/requires.txt +0 -0
- {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,43 @@ 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.2.0] - 2026-06-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added project rule loading from `ZAI.md`, `.zai/rules.md`, `AGENTS.md`,
|
|
13
|
+
`CLAUDE.md`, and `GEMINI.md`.
|
|
14
|
+
- Added interactive `/rules` and top-level `zai rules` to show loaded project
|
|
15
|
+
instruction files.
|
|
16
|
+
- Added project-rule visibility to `zai context` and interactive `/context`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Interactive and one-shot chat now pin loaded project rules into model context
|
|
21
|
+
so the assistant can follow repo-specific instructions.
|
|
22
|
+
- Project rule loading is bounded, skips unreadable/oversized files, and redacts
|
|
23
|
+
obvious secrets before rules are added to context.
|
|
24
|
+
|
|
25
|
+
## [0.1.9] - 2026-06-30
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added provider-neutral token estimation helpers and context usage reporting.
|
|
30
|
+
- Added `zai context tokens`, `zai context summary`, interactive
|
|
31
|
+
`/context tokens`, and interactive `/context summary`.
|
|
32
|
+
- Added interactive `/compact` to summarize older session history.
|
|
33
|
+
- Added `zai session compact <name-or-id>` for saved sessions.
|
|
34
|
+
- Added compacted-summary metadata to saved session payloads.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Automatic context compaction now uses configurable thresholds and prints a
|
|
39
|
+
transparent message when older history is summarized.
|
|
40
|
+
- Context status now shows estimated token usage, estimated context limit,
|
|
41
|
+
remaining tokens, and compacted-summary availability.
|
|
42
|
+
- Long-session summaries redact obvious API keys, bearer tokens, passwords, and
|
|
43
|
+
secret values before storage/display.
|
|
44
|
+
|
|
8
45
|
## [0.1.8] - 2026-06-29
|
|
9
46
|
|
|
10
47
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zai-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -343,10 +343,49 @@ When `zai` starts in a folder that already has `.zai/project_memory.json`, it
|
|
|
343
343
|
loads a short pinned summary so future answers can use that folder's context.
|
|
344
344
|
Different folders keep different memories.
|
|
345
345
|
|
|
346
|
+
## Project rules
|
|
347
|
+
|
|
348
|
+
Add project instruction files when you want `zai` to consistently follow repo
|
|
349
|
+
rules without copy-pasting them into every prompt.
|
|
350
|
+
|
|
351
|
+
Supported files:
|
|
352
|
+
|
|
353
|
+
```text
|
|
354
|
+
ZAI.md
|
|
355
|
+
.zai/rules.md
|
|
356
|
+
AGENTS.md
|
|
357
|
+
CLAUDE.md
|
|
358
|
+
GEMINI.md
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
When `zai` starts, it loads bounded text from these files in the project root
|
|
362
|
+
and current folder, redacts obvious secrets, and pins the rules into model
|
|
363
|
+
context. Use these files for coding style, preferred test commands, folders to
|
|
364
|
+
avoid, release steps, and project-specific constraints.
|
|
365
|
+
|
|
366
|
+
Inspect loaded rules:
|
|
367
|
+
|
|
368
|
+
```text
|
|
369
|
+
zai rules
|
|
370
|
+
/rules
|
|
371
|
+
zai context
|
|
372
|
+
/context
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Project rules are different from project memory:
|
|
376
|
+
|
|
377
|
+
- Project rules are explicit instructions written by the user/repo.
|
|
378
|
+
- Project memory is a generated or remembered summary in
|
|
379
|
+
`.zai/project_memory.json`.
|
|
380
|
+
- Repo maps are generated structure summaries used on demand.
|
|
381
|
+
- Session history is the current or saved conversation.
|
|
382
|
+
|
|
346
383
|
Memory types:
|
|
347
384
|
|
|
348
385
|
- Session history: previous conversation messages saved globally under `~/.zai`.
|
|
349
386
|
- Project memory: small per-folder facts in `.zai/project_memory.json`.
|
|
387
|
+
- Project rules: explicit instruction files such as `ZAI.md` or
|
|
388
|
+
`.zai/rules.md`.
|
|
350
389
|
- Repo map: generated structure summary used for repository questions; it is not
|
|
351
390
|
long-term memory.
|
|
352
391
|
|
|
@@ -393,6 +432,7 @@ zai auth login <provider> Connect a provider API key
|
|
|
393
432
|
zai auth logout <provider> Remove a provider API key from zai
|
|
394
433
|
zai auth doctor Show auth troubleshooting guidance
|
|
395
434
|
zai context Show loaded project context status
|
|
435
|
+
zai rules Show loaded project instruction files
|
|
396
436
|
zai memory show Show last task metadata
|
|
397
437
|
zai memory projects List remembered projects
|
|
398
438
|
zai memory project Show project-scoped memory
|
|
@@ -482,6 +522,7 @@ Inside `zai` interactive mode:
|
|
|
482
522
|
/model <name> Change model for this interactive session
|
|
483
523
|
/providers Show provider connection status
|
|
484
524
|
/context Show loaded project context status
|
|
525
|
+
/rules Show loaded project instruction files
|
|
485
526
|
/memory Show last task metadata
|
|
486
527
|
/commands List built-in and plugin slash commands
|
|
487
528
|
```
|
|
@@ -506,6 +547,20 @@ Use `zai context` or `/context` to inspect the current folder, whether project
|
|
|
506
547
|
memory exists, whether a repo-map cache is available, and how many conversation
|
|
507
548
|
messages are loaded in the current interactive session.
|
|
508
549
|
|
|
550
|
+
For token/context usage:
|
|
551
|
+
|
|
552
|
+
```text
|
|
553
|
+
zai context tokens
|
|
554
|
+
zai context summary
|
|
555
|
+
/context tokens
|
|
556
|
+
/context summary
|
|
557
|
+
/compact
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
`zai context tokens` and `/context tokens` show an approximate provider-neutral
|
|
561
|
+
token estimate, the configured context limit, remaining tokens, and whether the
|
|
562
|
+
session is approaching the compaction threshold.
|
|
563
|
+
|
|
509
564
|
Project memory is separate from current model context. It is a small
|
|
510
565
|
`.zai/project_memory.json` summary created with `zai memory refresh` or
|
|
511
566
|
`/memory refresh`.
|
|
@@ -514,7 +569,10 @@ Repo maps are summaries/indexes used on demand for repository questions. They
|
|
|
514
569
|
are not full source files loaded into every model call. If a file has not been
|
|
515
570
|
read or indexed for the current task, treat it as not loaded yet.
|
|
516
571
|
|
|
517
|
-
|
|
572
|
+
For long interactive sessions, `/compact` summarizes older conversation and
|
|
573
|
+
tool history so the active session can continue with less context pressure. The
|
|
574
|
+
summary preserves the goal, decisions, recent work, commands/results where
|
|
575
|
+
visible, and pending context in a compact form.
|
|
518
576
|
|
|
519
577
|
All chat, workflow, utility, and agent requests use a model-aware context
|
|
520
578
|
pipeline:
|
|
@@ -528,8 +586,9 @@ pipeline:
|
|
|
528
586
|
- context usage uses a conservative shared token estimate, while provider
|
|
529
587
|
response usage is retained where the SDK reports it.
|
|
530
588
|
|
|
531
|
-
This keeps requests bounded
|
|
532
|
-
|
|
589
|
+
This keeps requests bounded. Compaction is still approximate, so save important
|
|
590
|
+
stable project facts explicitly with `/remember <fact>` or `zai memory remember
|
|
591
|
+
<fact>`.
|
|
533
592
|
|
|
534
593
|
## Providers and fallback
|
|
535
594
|
|
|
@@ -286,10 +286,49 @@ When `zai` starts in a folder that already has `.zai/project_memory.json`, it
|
|
|
286
286
|
loads a short pinned summary so future answers can use that folder's context.
|
|
287
287
|
Different folders keep different memories.
|
|
288
288
|
|
|
289
|
+
## Project rules
|
|
290
|
+
|
|
291
|
+
Add project instruction files when you want `zai` to consistently follow repo
|
|
292
|
+
rules without copy-pasting them into every prompt.
|
|
293
|
+
|
|
294
|
+
Supported files:
|
|
295
|
+
|
|
296
|
+
```text
|
|
297
|
+
ZAI.md
|
|
298
|
+
.zai/rules.md
|
|
299
|
+
AGENTS.md
|
|
300
|
+
CLAUDE.md
|
|
301
|
+
GEMINI.md
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
When `zai` starts, it loads bounded text from these files in the project root
|
|
305
|
+
and current folder, redacts obvious secrets, and pins the rules into model
|
|
306
|
+
context. Use these files for coding style, preferred test commands, folders to
|
|
307
|
+
avoid, release steps, and project-specific constraints.
|
|
308
|
+
|
|
309
|
+
Inspect loaded rules:
|
|
310
|
+
|
|
311
|
+
```text
|
|
312
|
+
zai rules
|
|
313
|
+
/rules
|
|
314
|
+
zai context
|
|
315
|
+
/context
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Project rules are different from project memory:
|
|
319
|
+
|
|
320
|
+
- Project rules are explicit instructions written by the user/repo.
|
|
321
|
+
- Project memory is a generated or remembered summary in
|
|
322
|
+
`.zai/project_memory.json`.
|
|
323
|
+
- Repo maps are generated structure summaries used on demand.
|
|
324
|
+
- Session history is the current or saved conversation.
|
|
325
|
+
|
|
289
326
|
Memory types:
|
|
290
327
|
|
|
291
328
|
- Session history: previous conversation messages saved globally under `~/.zai`.
|
|
292
329
|
- Project memory: small per-folder facts in `.zai/project_memory.json`.
|
|
330
|
+
- Project rules: explicit instruction files such as `ZAI.md` or
|
|
331
|
+
`.zai/rules.md`.
|
|
293
332
|
- Repo map: generated structure summary used for repository questions; it is not
|
|
294
333
|
long-term memory.
|
|
295
334
|
|
|
@@ -336,6 +375,7 @@ zai auth login <provider> Connect a provider API key
|
|
|
336
375
|
zai auth logout <provider> Remove a provider API key from zai
|
|
337
376
|
zai auth doctor Show auth troubleshooting guidance
|
|
338
377
|
zai context Show loaded project context status
|
|
378
|
+
zai rules Show loaded project instruction files
|
|
339
379
|
zai memory show Show last task metadata
|
|
340
380
|
zai memory projects List remembered projects
|
|
341
381
|
zai memory project Show project-scoped memory
|
|
@@ -425,6 +465,7 @@ Inside `zai` interactive mode:
|
|
|
425
465
|
/model <name> Change model for this interactive session
|
|
426
466
|
/providers Show provider connection status
|
|
427
467
|
/context Show loaded project context status
|
|
468
|
+
/rules Show loaded project instruction files
|
|
428
469
|
/memory Show last task metadata
|
|
429
470
|
/commands List built-in and plugin slash commands
|
|
430
471
|
```
|
|
@@ -449,6 +490,20 @@ Use `zai context` or `/context` to inspect the current folder, whether project
|
|
|
449
490
|
memory exists, whether a repo-map cache is available, and how many conversation
|
|
450
491
|
messages are loaded in the current interactive session.
|
|
451
492
|
|
|
493
|
+
For token/context usage:
|
|
494
|
+
|
|
495
|
+
```text
|
|
496
|
+
zai context tokens
|
|
497
|
+
zai context summary
|
|
498
|
+
/context tokens
|
|
499
|
+
/context summary
|
|
500
|
+
/compact
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
`zai context tokens` and `/context tokens` show an approximate provider-neutral
|
|
504
|
+
token estimate, the configured context limit, remaining tokens, and whether the
|
|
505
|
+
session is approaching the compaction threshold.
|
|
506
|
+
|
|
452
507
|
Project memory is separate from current model context. It is a small
|
|
453
508
|
`.zai/project_memory.json` summary created with `zai memory refresh` or
|
|
454
509
|
`/memory refresh`.
|
|
@@ -457,7 +512,10 @@ Repo maps are summaries/indexes used on demand for repository questions. They
|
|
|
457
512
|
are not full source files loaded into every model call. If a file has not been
|
|
458
513
|
read or indexed for the current task, treat it as not loaded yet.
|
|
459
514
|
|
|
460
|
-
|
|
515
|
+
For long interactive sessions, `/compact` summarizes older conversation and
|
|
516
|
+
tool history so the active session can continue with less context pressure. The
|
|
517
|
+
summary preserves the goal, decisions, recent work, commands/results where
|
|
518
|
+
visible, and pending context in a compact form.
|
|
461
519
|
|
|
462
520
|
All chat, workflow, utility, and agent requests use a model-aware context
|
|
463
521
|
pipeline:
|
|
@@ -471,8 +529,9 @@ pipeline:
|
|
|
471
529
|
- context usage uses a conservative shared token estimate, while provider
|
|
472
530
|
response usage is retained where the SDK reports it.
|
|
473
531
|
|
|
474
|
-
This keeps requests bounded
|
|
475
|
-
|
|
532
|
+
This keeps requests bounded. Compaction is still approximate, so save important
|
|
533
|
+
stable project facts explicitly with `/remember <fact>` or `zai memory remember
|
|
534
|
+
<fact>`.
|
|
476
535
|
|
|
477
536
|
## Providers and fallback
|
|
478
537
|
|
|
@@ -180,6 +180,71 @@ def test_interactive_session_rename_routes_arguments(tmp_path, monkeypatch):
|
|
|
180
180
|
def test_interactive_project_memory_commands(tmp_path, monkeypatch, capsys):
|
|
181
181
|
monkeypatch.chdir(tmp_path)
|
|
182
182
|
(tmp_path / "pyproject.toml").write_text("[project]\nname='demo'\n")
|
|
183
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
184
|
+
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
185
|
+
with patch("zai.cli.interactive.add_project"):
|
|
186
|
+
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
187
|
+
with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
|
|
188
|
+
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
189
|
+
with patch(
|
|
190
|
+
"zai.cli.interactive.InteractiveInput.prompt",
|
|
191
|
+
side_effect=[
|
|
192
|
+
"/memory project",
|
|
193
|
+
"/memory refresh",
|
|
194
|
+
"/remember uses pytest",
|
|
195
|
+
"/forget 1",
|
|
196
|
+
EOFError(),
|
|
197
|
+
],
|
|
198
|
+
):
|
|
199
|
+
with patch("zai.cli.interactive.run_agent") as run_agent:
|
|
200
|
+
run_interactive("groq")
|
|
201
|
+
|
|
202
|
+
output = capsys.readouterr().out
|
|
203
|
+
assert "Project Memory" in output
|
|
204
|
+
assert "Project Memory Refreshed" in output
|
|
205
|
+
assert "Remembered" in output
|
|
206
|
+
assert "Forgot" in output
|
|
207
|
+
run_agent.assert_not_called()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_interactive_auth_commands_do_not_call_ai(tmp_path, monkeypatch, capsys):
|
|
211
|
+
monkeypatch.chdir(tmp_path)
|
|
212
|
+
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
213
|
+
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
214
|
+
with patch("zai.cli.interactive.add_project"):
|
|
215
|
+
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
216
|
+
with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
|
|
217
|
+
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
218
|
+
with patch(
|
|
219
|
+
"zai.cli.interactive.InteractiveInput.prompt",
|
|
220
|
+
side_effect=[
|
|
221
|
+
"/auth status",
|
|
222
|
+
"/auth login gemini",
|
|
223
|
+
"/auth logout gemini",
|
|
224
|
+
EOFError(),
|
|
225
|
+
],
|
|
226
|
+
):
|
|
227
|
+
with (
|
|
228
|
+
patch("zai.cli.interactive.Prompt.ask", return_value="gemini-key"),
|
|
229
|
+
patch("zai.cli.interactive.Confirm.ask", return_value=False),
|
|
230
|
+
patch("zai.cli.interactive.save_provider_key") as save,
|
|
231
|
+
patch("zai.cli.interactive.delete_provider_key", return_value=True) as delete,
|
|
232
|
+
patch("zai.cli.interactive.run_agent") as run_agent,
|
|
233
|
+
):
|
|
234
|
+
run_interactive("groq")
|
|
235
|
+
|
|
236
|
+
output = capsys.readouterr().out
|
|
237
|
+
assert "Provider status" in output
|
|
238
|
+
assert "Saved Gemini API key" in output
|
|
239
|
+
save.assert_called_once_with("gemini", "gemini-key")
|
|
240
|
+
delete.assert_called_once_with("gemini")
|
|
241
|
+
run_agent.assert_not_called()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch, capsys):
|
|
245
|
+
monkeypatch.chdir(tmp_path)
|
|
246
|
+
(tmp_path / "app.py").write_text("print('hi')\n")
|
|
247
|
+
(tmp_path / "ZAI.md").write_text("Use pytest.\n")
|
|
183
248
|
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
184
249
|
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
185
250
|
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
@@ -188,10 +253,11 @@ def test_interactive_project_memory_commands(tmp_path, monkeypatch, capsys):
|
|
|
188
253
|
with patch(
|
|
189
254
|
"zai.cli.interactive.InteractiveInput.prompt",
|
|
190
255
|
side_effect=[
|
|
191
|
-
"/
|
|
192
|
-
"/
|
|
193
|
-
"/
|
|
194
|
-
"/
|
|
256
|
+
"/providers",
|
|
257
|
+
"/context",
|
|
258
|
+
"/rules",
|
|
259
|
+
"/context tokens",
|
|
260
|
+
"/context summary",
|
|
195
261
|
EOFError(),
|
|
196
262
|
],
|
|
197
263
|
):
|
|
@@ -199,15 +265,21 @@ def test_interactive_project_memory_commands(tmp_path, monkeypatch, capsys):
|
|
|
199
265
|
run_interactive("groq")
|
|
200
266
|
|
|
201
267
|
output = capsys.readouterr().out
|
|
202
|
-
assert "
|
|
203
|
-
assert "Project
|
|
204
|
-
assert "
|
|
205
|
-
assert "
|
|
268
|
+
assert "Provider status" in output
|
|
269
|
+
assert "Project context" in output
|
|
270
|
+
assert "Project rules loaded:" in output
|
|
271
|
+
assert "ZAI.md" in output
|
|
272
|
+
assert "Project Rules" in output
|
|
273
|
+
assert "unread files are not loaded" in output
|
|
274
|
+
assert "Estimated tokens used" in output
|
|
275
|
+
assert "No compacted summary" in output
|
|
276
|
+
assert "app.py is loaded" not in output
|
|
206
277
|
run_agent.assert_not_called()
|
|
207
278
|
|
|
208
279
|
|
|
209
|
-
def
|
|
280
|
+
def test_interactive_pins_project_rules_before_agent_call(tmp_path, monkeypatch):
|
|
210
281
|
monkeypatch.chdir(tmp_path)
|
|
282
|
+
(tmp_path / "ZAI.md").write_text("Use the custom test command.\n")
|
|
211
283
|
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
212
284
|
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
213
285
|
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
@@ -215,33 +287,36 @@ def test_interactive_auth_commands_do_not_call_ai(tmp_path, monkeypatch, capsys)
|
|
|
215
287
|
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
216
288
|
with patch(
|
|
217
289
|
"zai.cli.interactive.InteractiveInput.prompt",
|
|
218
|
-
side_effect=[
|
|
219
|
-
"/auth status",
|
|
220
|
-
"/auth login gemini",
|
|
221
|
-
"/auth logout gemini",
|
|
222
|
-
EOFError(),
|
|
223
|
-
],
|
|
290
|
+
side_effect=["do work", EOFError()],
|
|
224
291
|
):
|
|
225
292
|
with (
|
|
226
|
-
patch(
|
|
293
|
+
patch(
|
|
294
|
+
"zai.cli.interactive.run_agent",
|
|
295
|
+
return_value="done",
|
|
296
|
+
) as run_agent,
|
|
227
297
|
patch("zai.cli.interactive.Confirm.ask", return_value=False),
|
|
228
|
-
patch("zai.cli.interactive.save_provider_key") as save,
|
|
229
|
-
patch("zai.cli.interactive.delete_provider_key", return_value=True) as delete,
|
|
230
|
-
patch("zai.cli.interactive.run_agent") as run_agent,
|
|
231
298
|
):
|
|
232
299
|
run_interactive("groq")
|
|
233
300
|
|
|
234
|
-
|
|
235
|
-
assert
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
run_agent.assert_not_called()
|
|
301
|
+
history = run_agent.call_args.args[1]
|
|
302
|
+
assert any(
|
|
303
|
+
message.pinned and "Use the custom test command" in message.content
|
|
304
|
+
for message in history
|
|
305
|
+
)
|
|
240
306
|
|
|
241
307
|
|
|
242
|
-
def
|
|
308
|
+
def test_interactive_manual_compact_summarizes_history(tmp_path, monkeypatch, capsys):
|
|
243
309
|
monkeypatch.chdir(tmp_path)
|
|
244
|
-
|
|
310
|
+
prompts = [
|
|
311
|
+
"task one",
|
|
312
|
+
"task two",
|
|
313
|
+
"task three",
|
|
314
|
+
"task four",
|
|
315
|
+
"task five",
|
|
316
|
+
"/compact",
|
|
317
|
+
"/context summary",
|
|
318
|
+
EOFError(),
|
|
319
|
+
]
|
|
245
320
|
with patch("zai.cli.interactive.has_available_provider", return_value=True):
|
|
246
321
|
with patch("zai.cli.interactive.fire_hook", return_value=True):
|
|
247
322
|
with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
|
|
@@ -249,17 +324,30 @@ def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch,
|
|
|
249
324
|
with patch("zai.cli.interactive._connect_mcp_servers"):
|
|
250
325
|
with patch(
|
|
251
326
|
"zai.cli.interactive.InteractiveInput.prompt",
|
|
252
|
-
side_effect=
|
|
327
|
+
side_effect=prompts,
|
|
253
328
|
):
|
|
254
|
-
with
|
|
329
|
+
with (
|
|
330
|
+
patch(
|
|
331
|
+
"zai.cli.interactive.run_agent",
|
|
332
|
+
side_effect=[
|
|
333
|
+
"answer one",
|
|
334
|
+
"answer two",
|
|
335
|
+
"answer three",
|
|
336
|
+
"answer four",
|
|
337
|
+
"answer five",
|
|
338
|
+
],
|
|
339
|
+
),
|
|
340
|
+
patch(
|
|
341
|
+
"zai.cli.interactive.Confirm.ask",
|
|
342
|
+
return_value=False,
|
|
343
|
+
),
|
|
344
|
+
):
|
|
255
345
|
run_interactive("groq")
|
|
256
346
|
|
|
257
347
|
output = capsys.readouterr().out
|
|
258
|
-
assert "
|
|
259
|
-
assert "
|
|
260
|
-
assert "
|
|
261
|
-
assert "app.py is loaded" not in output
|
|
262
|
-
run_agent.assert_not_called()
|
|
348
|
+
assert "Context compacted" in output
|
|
349
|
+
assert "Current compacted summary" in output
|
|
350
|
+
assert "task one" in output
|
|
263
351
|
|
|
264
352
|
|
|
265
353
|
def test_interactive_offers_project_memory_after_agent_task(tmp_path, monkeypatch, capsys):
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from zai.core.project_rules import (
|
|
2
|
+
format_project_rules,
|
|
3
|
+
load_project_rules,
|
|
4
|
+
project_rules_prompt,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_load_project_rules_from_root_and_current_folder(tmp_path):
|
|
9
|
+
(tmp_path / ".git").mkdir()
|
|
10
|
+
(tmp_path / "ZAI.md").write_text("Use pytest.\n", encoding="utf-8")
|
|
11
|
+
nested = tmp_path / "pkg"
|
|
12
|
+
nested.mkdir()
|
|
13
|
+
(nested / "AGENTS.md").write_text("Nested rule.\n", encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
rules = load_project_rules(nested)
|
|
16
|
+
|
|
17
|
+
assert [rule.relative_path for rule in rules] == ["ZAI.md", "pkg/AGENTS.md"]
|
|
18
|
+
assert "Use pytest" in project_rules_prompt(rules)
|
|
19
|
+
assert "pkg/AGENTS.md" in format_project_rules(rules)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_project_rules_redact_secrets_and_skip_large_files(tmp_path):
|
|
23
|
+
(tmp_path / "ZAI.md").write_text(
|
|
24
|
+
"api_key = sk-secret-value-1234567890\nFollow style.\n",
|
|
25
|
+
encoding="utf-8",
|
|
26
|
+
)
|
|
27
|
+
(tmp_path / "CLAUDE.md").write_text("x" * (70 * 1024), encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
rules = load_project_rules(tmp_path)
|
|
30
|
+
|
|
31
|
+
assert len(rules) == 1
|
|
32
|
+
assert "sk-secret" not in rules[0].content
|
|
33
|
+
assert "[REDACTED_SECRET]" in rules[0].content
|
|
@@ -141,17 +141,50 @@ def test_context_command_is_honest_about_unloaded_files(tmp_path, monkeypatch):
|
|
|
141
141
|
app = settings_app()
|
|
142
142
|
monkeypatch.chdir(tmp_path)
|
|
143
143
|
(tmp_path / "app.py").write_text("print('hi')\n")
|
|
144
|
+
(tmp_path / "ZAI.md").write_text("Use pytest.\n")
|
|
144
145
|
|
|
145
146
|
result = CliRunner().invoke(app, ["context"])
|
|
146
147
|
|
|
147
148
|
assert result.exit_code == 0
|
|
148
149
|
assert f"Folder: {tmp_path}" in result.output
|
|
149
150
|
assert "Project memory: missing" in result.output
|
|
151
|
+
assert "Project rules:" in result.output
|
|
152
|
+
assert "ZAI.md" in result.output
|
|
150
153
|
assert "Repo map/index: not loaded yet" in result.output
|
|
151
154
|
assert "unread files are not loaded" in result.output
|
|
155
|
+
assert "Estimated tokens used" in result.output
|
|
152
156
|
assert "app.py is loaded" not in result.output
|
|
153
157
|
|
|
154
158
|
|
|
159
|
+
def test_rules_command_shows_loaded_project_rules(tmp_path, monkeypatch):
|
|
160
|
+
app = settings_app()
|
|
161
|
+
monkeypatch.chdir(tmp_path)
|
|
162
|
+
(tmp_path / ".zai").mkdir()
|
|
163
|
+
(tmp_path / ".zai" / "rules.md").write_text("Never commit secrets.\n")
|
|
164
|
+
|
|
165
|
+
result = CliRunner().invoke(app, ["rules"])
|
|
166
|
+
|
|
167
|
+
assert result.exit_code == 0
|
|
168
|
+
assert "Project Rules" in result.output
|
|
169
|
+
assert ".zai/rules.md" in result.output
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_context_tokens_and_summary_commands(tmp_path, monkeypatch):
|
|
173
|
+
app = settings_app()
|
|
174
|
+
monkeypatch.chdir(tmp_path)
|
|
175
|
+
|
|
176
|
+
tokens = CliRunner().invoke(app, ["context", "tokens"])
|
|
177
|
+
summary = CliRunner().invoke(app, ["context", "summary"])
|
|
178
|
+
bad = CliRunner().invoke(app, ["context", "bad"])
|
|
179
|
+
|
|
180
|
+
assert tokens.exit_code == 0
|
|
181
|
+
assert "Estimated tokens used" in tokens.output
|
|
182
|
+
assert "Compacted summary" in tokens.output
|
|
183
|
+
assert summary.exit_code == 0
|
|
184
|
+
assert "No compacted summary" in summary.output
|
|
185
|
+
assert "Usage: zai context" in bad.output
|
|
186
|
+
|
|
187
|
+
|
|
155
188
|
def test_model_check_reports_valid_and_invalid_configuration():
|
|
156
189
|
app = settings_app()
|
|
157
190
|
runner = CliRunner()
|
|
@@ -408,6 +441,27 @@ def test_session_rename_delete_and_export():
|
|
|
408
441
|
assert "Export failed" in export_failed.output
|
|
409
442
|
|
|
410
443
|
|
|
444
|
+
def test_session_compact_routes_history():
|
|
445
|
+
from zai.providers.base import Message
|
|
446
|
+
|
|
447
|
+
app = settings_app()
|
|
448
|
+
history = [
|
|
449
|
+
Message(role="user", content=f"message {index}")
|
|
450
|
+
for index in range(12)
|
|
451
|
+
]
|
|
452
|
+
info = {"name": "auth", "project_path": "project"}
|
|
453
|
+
with (
|
|
454
|
+
patch("zai.cli.settings.load_history", return_value=history),
|
|
455
|
+
patch("zai.cli.settings.get_session_info", return_value=info),
|
|
456
|
+
patch("zai.cli.settings.save_history") as save,
|
|
457
|
+
):
|
|
458
|
+
result = CliRunner().invoke(app, ["session", "compact", "auth"])
|
|
459
|
+
|
|
460
|
+
assert result.exit_code == 0
|
|
461
|
+
assert "Session compacted" in result.output
|
|
462
|
+
save.assert_called_once()
|
|
463
|
+
|
|
464
|
+
|
|
411
465
|
def test_undo_list_empty_and_populated():
|
|
412
466
|
app = settings_app()
|
|
413
467
|
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.2.0"
|