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.
Files changed (115) hide show
  1. {zai_cli-0.1.8 → zai_cli-0.2.0}/CHANGELOG.md +37 -0
  2. {zai_cli-0.1.8/zai_cli.egg-info → zai_cli-0.2.0}/PKG-INFO +63 -4
  3. {zai_cli-0.1.8 → zai_cli-0.2.0}/README.md +62 -3
  4. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_interactive.py +122 -34
  5. zai_cli-0.2.0/tests/test_project_rules.py +33 -0
  6. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_settings_cli.py +54 -0
  7. zai_cli-0.2.0/tests/test_tokens.py +57 -0
  8. zai_cli-0.2.0/zai/__init__.py +1 -0
  9. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/interactive.py +68 -5
  10. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/settings.py +45 -2
  11. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/status.py +52 -1
  12. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/config.py +3 -0
  13. zai_cli-0.2.0/zai/core/compaction.py +83 -0
  14. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/context.py +16 -14
  15. zai_cli-0.2.0/zai/core/project_rules.py +111 -0
  16. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/session.py +24 -0
  17. zai_cli-0.2.0/zai/core/tokens.py +158 -0
  18. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/main.py +20 -2
  19. {zai_cli-0.1.8 → zai_cli-0.2.0/zai_cli.egg-info}/PKG-INFO +63 -4
  20. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/SOURCES.txt +5 -0
  21. zai_cli-0.1.8/zai/__init__.py +0 -1
  22. {zai_cli-0.1.8 → zai_cli-0.2.0}/LICENSE +0 -0
  23. {zai_cli-0.1.8 → zai_cli-0.2.0}/MANIFEST.in +0 -0
  24. {zai_cli-0.1.8 → zai_cli-0.2.0}/pyproject.toml +0 -0
  25. {zai_cli-0.1.8 → zai_cli-0.2.0}/scripts/release_preflight.py +0 -0
  26. {zai_cli-0.1.8 → zai_cli-0.2.0}/setup.cfg +0 -0
  27. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_agent.py +0 -0
  28. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_auth.py +0 -0
  29. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_browser.py +0 -0
  30. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_code_runner.py +0 -0
  31. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_config_main.py +0 -0
  32. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_core.py +0 -0
  33. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_errors.py +0 -0
  34. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_hooks_skills_session.py +0 -0
  35. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_input.py +0 -0
  36. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_integrations_cli.py +0 -0
  37. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_mcp.py +0 -0
  38. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_plugins.py +0 -0
  39. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_process.py +0 -0
  40. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_project_memory.py +0 -0
  41. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_providers.py +0 -0
  42. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_release_preflight.py +0 -0
  43. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_search.py +0 -0
  44. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_security.py +0 -0
  45. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_storage.py +0 -0
  46. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_streaming.py +0 -0
  47. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_tool_schema.py +0 -0
  48. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_tools.py +0 -0
  49. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_undo.py +0 -0
  50. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_utilities.py +0 -0
  51. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_vision.py +0 -0
  52. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_watch.py +0 -0
  53. {zai_cli-0.1.8 → zai_cli-0.2.0}/tests/test_workflows.py +0 -0
  54. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/__main__.py +0 -0
  55. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/__init__.py +0 -0
  56. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/common.py +0 -0
  57. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/integrations.py +0 -0
  58. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/setup_wizard.py +0 -0
  59. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/utilities.py +0 -0
  60. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/cli/workflows.py +0 -0
  61. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/commit.md +0 -0
  62. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/explain.md +0 -0
  63. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/feature.md +0 -0
  64. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/fix.md +0 -0
  65. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/commands/review.md +0 -0
  66. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/__init__.py +0 -0
  67. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/agent.py +0 -0
  68. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/auth.py +0 -0
  69. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/cancellation.py +0 -0
  70. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/commands.py +0 -0
  71. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/errors.py +0 -0
  72. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/fallback.py +0 -0
  73. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/hooks.py +0 -0
  74. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/memory.py +0 -0
  75. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/process.py +0 -0
  76. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/project_memory.py +0 -0
  77. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/repomap.py +0 -0
  78. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/runtime.py +0 -0
  79. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/security.py +0 -0
  80. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/storage.py +0 -0
  81. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/streaming.py +0 -0
  82. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/tool_schema.py +0 -0
  83. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/undo.py +0 -0
  84. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/core/watch.py +0 -0
  85. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/mcp/__init__.py +0 -0
  86. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/mcp/client.py +0 -0
  87. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/mcp/manager.py +0 -0
  88. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/plugins/__init__.py +0 -0
  89. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/plugins/base.py +0 -0
  90. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/plugins/loader.py +0 -0
  91. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/__init__.py +0 -0
  92. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/anthropic.py +0 -0
  93. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/base.py +0 -0
  94. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/cerebras.py +0 -0
  95. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/gemini.py +0 -0
  96. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/groq.py +0 -0
  97. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/ollama.py +0 -0
  98. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/openai.py +0 -0
  99. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/openrouter.py +0 -0
  100. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/providers/qwen.py +0 -0
  101. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/skills/__init__.py +0 -0
  102. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/skills/registry.py +0 -0
  103. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/__init__.py +0 -0
  104. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/browser.py +0 -0
  105. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/code_runner.py +0 -0
  106. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/files.py +0 -0
  107. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/git.py +0 -0
  108. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/search.py +0 -0
  109. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/tools/vision.py +0 -0
  110. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/ui/__init__.py +0 -0
  111. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai/ui/input.py +0 -0
  112. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/dependency_links.txt +0 -0
  113. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/entry_points.txt +0 -0
  114. {zai_cli-0.1.8 → zai_cli-0.2.0}/zai_cli.egg-info/requires.txt +0 -0
  115. {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.1.8
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
- Token tracking and automatic long-session compaction are planned for `0.1.9`.
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, but it is not a replacement for future
532
- long-session token tracking and automatic compaction.
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
- Token tracking and automatic long-session compaction are planned for `0.1.9`.
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, but it is not a replacement for future
475
- long-session token tracking and automatic compaction.
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
- "/memory project",
192
- "/memory refresh",
193
- "/remember uses pytest",
194
- "/forget 1",
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 "Project Memory" in output
203
- assert "Project Memory Refreshed" in output
204
- assert "Remembered" in output
205
- assert "Forgot" in output
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 test_interactive_auth_commands_do_not_call_ai(tmp_path, monkeypatch, capsys):
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("zai.cli.interactive.Prompt.ask", return_value="gemini-key"),
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
- output = capsys.readouterr().out
235
- assert "Provider status" in output
236
- assert "Saved Gemini API key" in output
237
- save.assert_called_once_with("gemini", "gemini-key")
238
- delete.assert_called_once_with("gemini")
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 test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch, capsys):
308
+ def test_interactive_manual_compact_summarizes_history(tmp_path, monkeypatch, capsys):
243
309
  monkeypatch.chdir(tmp_path)
244
- (tmp_path / "app.py").write_text("print('hi')\n")
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=["/providers", "/context", EOFError()],
327
+ side_effect=prompts,
253
328
  ):
254
- with patch("zai.cli.interactive.run_agent") as run_agent:
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 "Provider status" in output
259
- assert "Project context" in output
260
- assert "unread files are not loaded" in output
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"