zai-cli 0.1.9__tar.gz → 0.2.2__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 (117) hide show
  1. {zai_cli-0.1.9 → zai_cli-0.2.2}/CHANGELOG.md +45 -0
  2. {zai_cli-0.1.9/zai_cli.egg-info → zai_cli-0.2.2}/PKG-INFO +59 -2
  3. {zai_cli-0.1.9 → zai_cli-0.2.2}/README.md +58 -1
  4. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_auth.py +42 -1
  5. zai_cli-0.2.2/tests/test_file_mentions.py +30 -0
  6. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_input.py +16 -0
  7. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_interactive.py +95 -34
  8. zai_cli-0.2.2/tests/test_project_rules.py +33 -0
  9. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_settings_cli.py +36 -0
  10. zai_cli-0.2.2/zai/__init__.py +1 -0
  11. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/interactive.py +44 -15
  12. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/settings.py +60 -10
  13. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/status.py +7 -0
  14. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/auth.py +89 -13
  15. zai_cli-0.2.2/zai/core/file_mentions.py +99 -0
  16. zai_cli-0.2.2/zai/core/project_rules.py +111 -0
  17. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/main.py +16 -1
  18. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/ui/input.py +14 -0
  19. {zai_cli-0.1.9 → zai_cli-0.2.2/zai_cli.egg-info}/PKG-INFO +59 -2
  20. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai_cli.egg-info/SOURCES.txt +4 -0
  21. zai_cli-0.1.9/zai/__init__.py +0 -1
  22. {zai_cli-0.1.9 → zai_cli-0.2.2}/LICENSE +0 -0
  23. {zai_cli-0.1.9 → zai_cli-0.2.2}/MANIFEST.in +0 -0
  24. {zai_cli-0.1.9 → zai_cli-0.2.2}/pyproject.toml +0 -0
  25. {zai_cli-0.1.9 → zai_cli-0.2.2}/scripts/release_preflight.py +0 -0
  26. {zai_cli-0.1.9 → zai_cli-0.2.2}/setup.cfg +0 -0
  27. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_agent.py +0 -0
  28. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_browser.py +0 -0
  29. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_code_runner.py +0 -0
  30. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_config_main.py +0 -0
  31. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_core.py +0 -0
  32. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_errors.py +0 -0
  33. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_hooks_skills_session.py +0 -0
  34. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_integrations_cli.py +0 -0
  35. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_mcp.py +0 -0
  36. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_plugins.py +0 -0
  37. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_process.py +0 -0
  38. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_project_memory.py +0 -0
  39. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_providers.py +0 -0
  40. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_release_preflight.py +0 -0
  41. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_search.py +0 -0
  42. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_security.py +0 -0
  43. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_storage.py +0 -0
  44. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_streaming.py +0 -0
  45. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_tokens.py +0 -0
  46. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_tool_schema.py +0 -0
  47. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_tools.py +0 -0
  48. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_undo.py +0 -0
  49. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_utilities.py +0 -0
  50. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_vision.py +0 -0
  51. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_watch.py +0 -0
  52. {zai_cli-0.1.9 → zai_cli-0.2.2}/tests/test_workflows.py +0 -0
  53. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/__main__.py +0 -0
  54. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/__init__.py +0 -0
  55. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/common.py +0 -0
  56. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/integrations.py +0 -0
  57. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/setup_wizard.py +0 -0
  58. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/utilities.py +0 -0
  59. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/cli/workflows.py +0 -0
  60. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/commands/commit.md +0 -0
  61. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/commands/explain.md +0 -0
  62. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/commands/feature.md +0 -0
  63. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/commands/fix.md +0 -0
  64. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/commands/review.md +0 -0
  65. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/config.py +0 -0
  66. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/__init__.py +0 -0
  67. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/agent.py +0 -0
  68. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/cancellation.py +0 -0
  69. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/commands.py +0 -0
  70. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/compaction.py +0 -0
  71. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/context.py +0 -0
  72. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/errors.py +0 -0
  73. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/fallback.py +0 -0
  74. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/hooks.py +0 -0
  75. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/memory.py +0 -0
  76. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/process.py +0 -0
  77. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/project_memory.py +0 -0
  78. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/repomap.py +0 -0
  79. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/runtime.py +0 -0
  80. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/security.py +0 -0
  81. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/session.py +0 -0
  82. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/storage.py +0 -0
  83. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/streaming.py +0 -0
  84. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/tokens.py +0 -0
  85. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/tool_schema.py +0 -0
  86. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/undo.py +0 -0
  87. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/core/watch.py +0 -0
  88. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/mcp/__init__.py +0 -0
  89. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/mcp/client.py +0 -0
  90. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/mcp/manager.py +0 -0
  91. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/plugins/__init__.py +0 -0
  92. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/plugins/base.py +0 -0
  93. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/plugins/loader.py +0 -0
  94. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/__init__.py +0 -0
  95. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/anthropic.py +0 -0
  96. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/base.py +0 -0
  97. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/cerebras.py +0 -0
  98. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/gemini.py +0 -0
  99. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/groq.py +0 -0
  100. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/ollama.py +0 -0
  101. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/openai.py +0 -0
  102. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/openrouter.py +0 -0
  103. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/providers/qwen.py +0 -0
  104. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/skills/__init__.py +0 -0
  105. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/skills/registry.py +0 -0
  106. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/__init__.py +0 -0
  107. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/browser.py +0 -0
  108. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/code_runner.py +0 -0
  109. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/files.py +0 -0
  110. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/git.py +0 -0
  111. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/search.py +0 -0
  112. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/tools/vision.py +0 -0
  113. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai/ui/__init__.py +0 -0
  114. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai_cli.egg-info/dependency_links.txt +0 -0
  115. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai_cli.egg-info/entry_points.txt +0 -0
  116. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai_cli.egg-info/requires.txt +0 -0
  117. {zai_cli-0.1.9 → zai_cli-0.2.2}/zai_cli.egg-info/top_level.txt +0 -0
@@ -5,6 +5,51 @@ 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.2] - 2026-07-13
9
+
10
+ ### Added
11
+
12
+ - Added structured live-provider validation results with the resolved model,
13
+ token usage, failure detail, and a recommended next action.
14
+
15
+ ### Changed
16
+
17
+ - Authentication login validation now distinguishes missing or invalid
18
+ credentials, quota limits, model configuration errors, network failures, and
19
+ unexpected provider failures.
20
+ - `zai model test` now reports actionable recovery commands and shows the token
21
+ cost of a successful validation request when providers return usage data.
22
+
23
+ ## [0.2.1] - 2026-06-30
24
+
25
+ ### Added
26
+
27
+ - Added `@file` completion in normal interactive prompts.
28
+ - Added safe `@file` mention expansion so prompts like `explain @app.py`
29
+ include bounded referenced file content.
30
+
31
+ ### Changed
32
+
33
+ - Mention expansion skips missing, hidden, oversized, binary, and
34
+ sensitive-looking files without crashing the prompt.
35
+
36
+ ## [0.2.0] - 2026-06-30
37
+
38
+ ### Added
39
+
40
+ - Added project rule loading from `ZAI.md`, `.zai/rules.md`, `AGENTS.md`,
41
+ `CLAUDE.md`, and `GEMINI.md`.
42
+ - Added interactive `/rules` and top-level `zai rules` to show loaded project
43
+ instruction files.
44
+ - Added project-rule visibility to `zai context` and interactive `/context`.
45
+
46
+ ### Changed
47
+
48
+ - Interactive and one-shot chat now pin loaded project rules into model context
49
+ so the assistant can follow repo-specific instructions.
50
+ - Project rule loading is bounded, skips unreadable/oversized files, and redacts
51
+ obvious secrets before rules are added to context.
52
+
8
53
  ## [0.1.9] - 2026-06-30
9
54
 
10
55
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zai-cli
3
- Version: 0.1.9
3
+ Version: 0.2.2
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
@@ -198,6 +198,10 @@ zai auth login openai
198
198
  zai auth login anthropic
199
199
  ```
200
200
 
201
+ When prompted, authentication login can send one tiny live request to validate
202
+ the key. The result identifies the resolved model and gives a recovery command
203
+ for invalid credentials, quota limits, model errors, or network failures.
204
+
201
205
  Remove a saved API key from `zai`:
202
206
 
203
207
  ```bash
@@ -343,10 +347,49 @@ When `zai` starts in a folder that already has `.zai/project_memory.json`, it
343
347
  loads a short pinned summary so future answers can use that folder's context.
344
348
  Different folders keep different memories.
345
349
 
350
+ ## Project rules
351
+
352
+ Add project instruction files when you want `zai` to consistently follow repo
353
+ rules without copy-pasting them into every prompt.
354
+
355
+ Supported files:
356
+
357
+ ```text
358
+ ZAI.md
359
+ .zai/rules.md
360
+ AGENTS.md
361
+ CLAUDE.md
362
+ GEMINI.md
363
+ ```
364
+
365
+ When `zai` starts, it loads bounded text from these files in the project root
366
+ and current folder, redacts obvious secrets, and pins the rules into model
367
+ context. Use these files for coding style, preferred test commands, folders to
368
+ avoid, release steps, and project-specific constraints.
369
+
370
+ Inspect loaded rules:
371
+
372
+ ```text
373
+ zai rules
374
+ /rules
375
+ zai context
376
+ /context
377
+ ```
378
+
379
+ Project rules are different from project memory:
380
+
381
+ - Project rules are explicit instructions written by the user/repo.
382
+ - Project memory is a generated or remembered summary in
383
+ `.zai/project_memory.json`.
384
+ - Repo maps are generated structure summaries used on demand.
385
+ - Session history is the current or saved conversation.
386
+
346
387
  Memory types:
347
388
 
348
389
  - Session history: previous conversation messages saved globally under `~/.zai`.
349
390
  - Project memory: small per-folder facts in `.zai/project_memory.json`.
391
+ - Project rules: explicit instruction files such as `ZAI.md` or
392
+ `.zai/rules.md`.
350
393
  - Repo map: generated structure summary used for repository questions; it is not
351
394
  long-term memory.
352
395
 
@@ -387,12 +430,13 @@ zai model add <name> Add a custom model alias
387
430
  zai model remove <name> Remove a custom model alias
388
431
  zai model configure <name> Set timeout and retry policy
389
432
  zai model info <name> Show effective model configuration
390
- zai model test <name> Verify credentials and model availability
433
+ zai model test <name> Verify credentials/model and show recovery guidance
391
434
  zai auth status Show API-key and external CLI auth status
392
435
  zai auth login <provider> Connect a provider API key
393
436
  zai auth logout <provider> Remove a provider API key from zai
394
437
  zai auth doctor Show auth troubleshooting guidance
395
438
  zai context Show loaded project context status
439
+ zai rules Show loaded project instruction files
396
440
  zai memory show Show last task metadata
397
441
  zai memory projects List remembered projects
398
442
  zai memory project Show project-scoped memory
@@ -482,6 +526,7 @@ Inside `zai` interactive mode:
482
526
  /model <name> Change model for this interactive session
483
527
  /providers Show provider connection status
484
528
  /context Show loaded project context status
529
+ /rules Show loaded project instruction files
485
530
  /memory Show last task metadata
486
531
  /commands List built-in and plugin slash commands
487
532
  ```
@@ -497,6 +542,18 @@ Interactive input also provides:
497
542
  - `Ctrl+C` to cancel the current input or operation without ending the session;
498
543
  - a basic Rich prompt fallback in redirected or unsupported terminals.
499
544
 
545
+ You can reference files in natural prompts with `@file`:
546
+
547
+ ```text
548
+ explain @app.py
549
+ fix @src/main.py
550
+ compare @README.md and @CHANGELOG.md
551
+ ```
552
+
553
+ `zai` loads bounded text from valid referenced files and appends it to the
554
+ prompt context. Hidden files, sensitive-looking paths, binary files, and very
555
+ large files are skipped with a note instead of crashing the session.
556
+
500
557
  ## How much of my project does zai see?
501
558
 
502
559
  `zai` starts from the folder where you run it. It does not load every file in a
@@ -141,6 +141,10 @@ zai auth login openai
141
141
  zai auth login anthropic
142
142
  ```
143
143
 
144
+ When prompted, authentication login can send one tiny live request to validate
145
+ the key. The result identifies the resolved model and gives a recovery command
146
+ for invalid credentials, quota limits, model errors, or network failures.
147
+
144
148
  Remove a saved API key from `zai`:
145
149
 
146
150
  ```bash
@@ -286,10 +290,49 @@ When `zai` starts in a folder that already has `.zai/project_memory.json`, it
286
290
  loads a short pinned summary so future answers can use that folder's context.
287
291
  Different folders keep different memories.
288
292
 
293
+ ## Project rules
294
+
295
+ Add project instruction files when you want `zai` to consistently follow repo
296
+ rules without copy-pasting them into every prompt.
297
+
298
+ Supported files:
299
+
300
+ ```text
301
+ ZAI.md
302
+ .zai/rules.md
303
+ AGENTS.md
304
+ CLAUDE.md
305
+ GEMINI.md
306
+ ```
307
+
308
+ When `zai` starts, it loads bounded text from these files in the project root
309
+ and current folder, redacts obvious secrets, and pins the rules into model
310
+ context. Use these files for coding style, preferred test commands, folders to
311
+ avoid, release steps, and project-specific constraints.
312
+
313
+ Inspect loaded rules:
314
+
315
+ ```text
316
+ zai rules
317
+ /rules
318
+ zai context
319
+ /context
320
+ ```
321
+
322
+ Project rules are different from project memory:
323
+
324
+ - Project rules are explicit instructions written by the user/repo.
325
+ - Project memory is a generated or remembered summary in
326
+ `.zai/project_memory.json`.
327
+ - Repo maps are generated structure summaries used on demand.
328
+ - Session history is the current or saved conversation.
329
+
289
330
  Memory types:
290
331
 
291
332
  - Session history: previous conversation messages saved globally under `~/.zai`.
292
333
  - Project memory: small per-folder facts in `.zai/project_memory.json`.
334
+ - Project rules: explicit instruction files such as `ZAI.md` or
335
+ `.zai/rules.md`.
293
336
  - Repo map: generated structure summary used for repository questions; it is not
294
337
  long-term memory.
295
338
 
@@ -330,12 +373,13 @@ zai model add <name> Add a custom model alias
330
373
  zai model remove <name> Remove a custom model alias
331
374
  zai model configure <name> Set timeout and retry policy
332
375
  zai model info <name> Show effective model configuration
333
- zai model test <name> Verify credentials and model availability
376
+ zai model test <name> Verify credentials/model and show recovery guidance
334
377
  zai auth status Show API-key and external CLI auth status
335
378
  zai auth login <provider> Connect a provider API key
336
379
  zai auth logout <provider> Remove a provider API key from zai
337
380
  zai auth doctor Show auth troubleshooting guidance
338
381
  zai context Show loaded project context status
382
+ zai rules Show loaded project instruction files
339
383
  zai memory show Show last task metadata
340
384
  zai memory projects List remembered projects
341
385
  zai memory project Show project-scoped memory
@@ -425,6 +469,7 @@ Inside `zai` interactive mode:
425
469
  /model <name> Change model for this interactive session
426
470
  /providers Show provider connection status
427
471
  /context Show loaded project context status
472
+ /rules Show loaded project instruction files
428
473
  /memory Show last task metadata
429
474
  /commands List built-in and plugin slash commands
430
475
  ```
@@ -440,6 +485,18 @@ Interactive input also provides:
440
485
  - `Ctrl+C` to cancel the current input or operation without ending the session;
441
486
  - a basic Rich prompt fallback in redirected or unsupported terminals.
442
487
 
488
+ You can reference files in natural prompts with `@file`:
489
+
490
+ ```text
491
+ explain @app.py
492
+ fix @src/main.py
493
+ compare @README.md and @CHANGELOG.md
494
+ ```
495
+
496
+ `zai` loads bounded text from valid referenced files and appends it to the
497
+ prompt context. Hidden files, sensitive-looking paths, binary files, and very
498
+ large files are skipped with a note instead of crashing the session.
499
+
443
500
  ## How much of my project does zai see?
444
501
 
445
502
  `zai` starts from the folder where you run it. It does not load every file in a
@@ -14,6 +14,7 @@ from zai.core.auth import (
14
14
  mask_secret,
15
15
  save_provider_key,
16
16
  validate_provider,
17
+ validate_provider_details,
17
18
  )
18
19
 
19
20
 
@@ -66,18 +67,58 @@ def test_external_login_command_for_codex_and_claude():
66
67
  def test_validate_provider_normalizes_results():
67
68
  provider = MagicMock()
68
69
  provider.is_available.return_value = True
69
- provider.chat.return_value = MagicMock()
70
+ provider.chat.return_value = MagicMock(model="provider/model", tokens_used=7)
70
71
  with patch("zai.core.auth.get_provider", return_value=provider):
71
72
  assert validate_provider("groq") == "OK"
73
+ details = validate_provider_details("groq")
74
+ assert details.status == "OK"
75
+ assert details.model == "provider/model"
76
+ assert details.tokens_used == 7
77
+ assert "zai model set groq" in details.next_action
72
78
 
73
79
  provider.chat.side_effect = RuntimeError("unauthorized api key")
74
80
  with patch("zai.core.auth.get_provider", return_value=provider):
75
81
  assert validate_provider("groq") == "invalid"
82
+ details = validate_provider_details("groq")
83
+ assert details.status == "invalid"
84
+ assert "zai auth login groq" in details.next_action
76
85
 
77
86
  provider.chat.side_effect = RuntimeError("rate limit")
78
87
  with patch("zai.core.auth.get_provider", return_value=provider):
79
88
  assert validate_provider("groq") == "quota/rate limit"
80
89
 
90
+ provider.chat.side_effect = RuntimeError("unknown model does not exist")
91
+ with patch("zai.core.auth.get_provider", return_value=provider):
92
+ details = validate_provider_details("groq")
93
+ assert details.status == "model error"
94
+ assert "zai model info groq" in details.next_action
95
+
96
+ provider.chat.side_effect = RuntimeError("unexpected provider failure")
97
+ with patch("zai.core.auth.get_provider", return_value=provider):
98
+ details = validate_provider_details("groq")
99
+ assert details.status == "provider error"
100
+ assert "zai auth doctor" in details.next_action
101
+
102
+ provider.chat.side_effect = RuntimeError("api_key=super-secret-provider-token")
103
+ with patch("zai.core.auth.get_provider", return_value=provider):
104
+ details = validate_provider_details("groq")
105
+ assert "super-secret-provider-token" not in details.detail
106
+ assert "[REDACTED_SECRET]" in details.detail
107
+
108
+
109
+ def test_validate_provider_reports_unknown_and_unavailable():
110
+ unknown = validate_provider_details("unknown")
111
+ assert unknown.status == "missing"
112
+ assert unknown.detail == "Unknown provider."
113
+
114
+ provider = MagicMock()
115
+ provider.is_available.return_value = False
116
+ with patch("zai.core.auth.get_provider", return_value=provider):
117
+ unavailable = validate_provider_details("groq")
118
+
119
+ assert unavailable.status == "missing"
120
+ assert "zai auth login groq" in unavailable.next_action
121
+
81
122
 
82
123
  def test_top_level_auth_status_does_not_print_secret(monkeypatch):
83
124
  monkeypatch.setenv("OPENAI_API_KEY", "sk-live-secret")
@@ -0,0 +1,30 @@
1
+ from zai.core.file_mentions import expand_file_mentions, extract_file_mentions
2
+
3
+
4
+ def test_extract_file_mentions_keeps_order_and_uniqueness():
5
+ text = "explain @app.py then compare @README.md and @app.py."
6
+
7
+ assert extract_file_mentions(text) == ["app.py", "README.md"]
8
+
9
+
10
+ def test_expand_file_mentions_loads_bounded_file_context(tmp_path):
11
+ (tmp_path / "app.py").write_text("print('hi')\n", encoding="utf-8")
12
+
13
+ expanded = expand_file_mentions("explain @app.py", tmp_path)
14
+
15
+ assert "explain @app.py" in expanded
16
+ assert "Referenced @file context" in expanded
17
+ assert "--- @app.py ---" in expanded
18
+ assert "print('hi')" in expanded
19
+
20
+
21
+ def test_expand_file_mentions_skips_hidden_and_sensitive_files(tmp_path):
22
+ (tmp_path / ".env").write_text("API_KEY=secret\n", encoding="utf-8")
23
+ (tmp_path / "token.txt").write_text("secret\n", encoding="utf-8")
24
+
25
+ expanded = expand_file_mentions("read @.env and @token.txt", tmp_path)
26
+
27
+ assert "API_KEY=secret" not in expanded
28
+ assert "Mention notes" in expanded
29
+ assert "Skipped @.env" in expanded
30
+ assert "Skipped @token.txt" in expanded
@@ -97,6 +97,22 @@ def test_path_completion(tmp_path):
97
97
  assert not any(".secret" in candidate[0] for candidate in candidates)
98
98
 
99
99
 
100
+ def test_file_mention_completion_in_natural_prompt(tmp_path):
101
+ (tmp_path / "src").mkdir()
102
+ (tmp_path / "sample.py").write_text("")
103
+ (tmp_path / ".secret").write_text("")
104
+
105
+ candidates = completion_candidates(
106
+ "explain @s",
107
+ str(tmp_path),
108
+ {"fix"},
109
+ )
110
+
111
+ assert ("@src/", "@src/", "file mention") in candidates
112
+ assert ("@sample.py", "@sample.py", "file mention") in candidates
113
+ assert not any(".secret" in candidate[0] for candidate in candidates)
114
+
115
+
100
116
  def test_prompt_falls_back_when_prompt_toolkit_missing(tmp_path):
101
117
  real_import = __import__
102
118
 
@@ -182,21 +182,22 @@ def test_interactive_project_memory_commands(tmp_path, monkeypatch, capsys):
182
182
  (tmp_path / "pyproject.toml").write_text("[project]\nname='demo'\n")
183
183
  with patch("zai.cli.interactive.has_available_provider", return_value=True):
184
184
  with patch("zai.cli.interactive.fire_hook", return_value=True):
185
- with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
186
- with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
187
- with patch("zai.cli.interactive._connect_mcp_servers"):
188
- with patch(
189
- "zai.cli.interactive.InteractiveInput.prompt",
190
- side_effect=[
191
- "/memory project",
192
- "/memory refresh",
193
- "/remember uses pytest",
194
- "/forget 1",
195
- EOFError(),
196
- ],
197
- ):
198
- with patch("zai.cli.interactive.run_agent") as run_agent:
199
- run_interactive("groq")
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")
200
201
 
201
202
  output = capsys.readouterr().out
202
203
  assert "Project Memory" in output
@@ -210,26 +211,27 @@ def test_interactive_auth_commands_do_not_call_ai(tmp_path, monkeypatch, capsys)
210
211
  monkeypatch.chdir(tmp_path)
211
212
  with patch("zai.cli.interactive.has_available_provider", return_value=True):
212
213
  with patch("zai.cli.interactive.fire_hook", return_value=True):
213
- with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
214
- with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
215
- with patch("zai.cli.interactive._connect_mcp_servers"):
216
- with patch(
217
- "zai.cli.interactive.InteractiveInput.prompt",
218
- side_effect=[
219
- "/auth status",
220
- "/auth login gemini",
221
- "/auth logout gemini",
222
- EOFError(),
223
- ],
224
- ):
225
- with (
226
- patch("zai.cli.interactive.Prompt.ask", return_value="gemini-key"),
227
- 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,
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
+ ],
231
226
  ):
232
- run_interactive("groq")
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")
233
235
 
234
236
  output = capsys.readouterr().out
235
237
  assert "Provider status" in output
@@ -242,6 +244,7 @@ def test_interactive_auth_commands_do_not_call_ai(tmp_path, monkeypatch, capsys)
242
244
  def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch, capsys):
243
245
  monkeypatch.chdir(tmp_path)
244
246
  (tmp_path / "app.py").write_text("print('hi')\n")
247
+ (tmp_path / "ZAI.md").write_text("Use pytest.\n")
245
248
  with patch("zai.cli.interactive.has_available_provider", return_value=True):
246
249
  with patch("zai.cli.interactive.fire_hook", return_value=True):
247
250
  with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
@@ -252,6 +255,7 @@ def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch,
252
255
  side_effect=[
253
256
  "/providers",
254
257
  "/context",
258
+ "/rules",
255
259
  "/context tokens",
256
260
  "/context summary",
257
261
  EOFError(),
@@ -263,6 +267,9 @@ def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch,
263
267
  output = capsys.readouterr().out
264
268
  assert "Provider status" in output
265
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
266
273
  assert "unread files are not loaded" in output
267
274
  assert "Estimated tokens used" in output
268
275
  assert "No compacted summary" in output
@@ -270,6 +277,60 @@ def test_interactive_context_and_providers_do_not_call_ai(tmp_path, monkeypatch,
270
277
  run_agent.assert_not_called()
271
278
 
272
279
 
280
+ def test_interactive_pins_project_rules_before_agent_call(tmp_path, monkeypatch):
281
+ monkeypatch.chdir(tmp_path)
282
+ (tmp_path / "ZAI.md").write_text("Use the custom test command.\n")
283
+ with patch("zai.cli.interactive.has_available_provider", return_value=True):
284
+ with patch("zai.cli.interactive.fire_hook", return_value=True):
285
+ with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
286
+ with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
287
+ with patch("zai.cli.interactive._connect_mcp_servers"):
288
+ with patch(
289
+ "zai.cli.interactive.InteractiveInput.prompt",
290
+ side_effect=["do work", EOFError()],
291
+ ):
292
+ with (
293
+ patch(
294
+ "zai.cli.interactive.run_agent",
295
+ return_value="done",
296
+ ) as run_agent,
297
+ patch("zai.cli.interactive.Confirm.ask", return_value=False),
298
+ ):
299
+ run_interactive("groq")
300
+
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
+ )
306
+
307
+
308
+ def test_interactive_expands_file_mentions_before_agent_call(tmp_path, monkeypatch):
309
+ monkeypatch.chdir(tmp_path)
310
+ (tmp_path / "app.py").write_text("print('hello')\n")
311
+ with patch("zai.cli.interactive.has_available_provider", return_value=True):
312
+ with patch("zai.cli.interactive.fire_hook", return_value=True):
313
+ with patch("zai.cli.interactive.plugin_loader.load_all", return_value={}):
314
+ with patch("zai.cli.interactive.plugin_loader.get_errors", return_value={}):
315
+ with patch("zai.cli.interactive._connect_mcp_servers"):
316
+ with patch(
317
+ "zai.cli.interactive.InteractiveInput.prompt",
318
+ side_effect=["explain @app.py", EOFError()],
319
+ ):
320
+ with (
321
+ patch(
322
+ "zai.cli.interactive.run_agent",
323
+ return_value="done",
324
+ ) as run_agent,
325
+ patch("zai.cli.interactive.Confirm.ask", return_value=False),
326
+ ):
327
+ run_interactive("groq")
328
+
329
+ prompt = run_agent.call_args.args[0]
330
+ assert "Referenced @file context" in prompt
331
+ assert "print('hello')" in prompt
332
+
333
+
273
334
  def test_interactive_manual_compact_summarizes_history(tmp_path, monkeypatch, capsys):
274
335
  monkeypatch.chdir(tmp_path)
275
336
  prompts = [
@@ -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,18 +141,35 @@ 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
152
155
  assert "Estimated tokens used" in result.output
153
156
  assert "app.py is loaded" not in result.output
154
157
 
155
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 / ".git").mkdir()
163
+ (tmp_path / ".zai").mkdir()
164
+ (tmp_path / ".zai" / "rules.md").write_text("Never commit secrets.\n")
165
+
166
+ result = CliRunner().invoke(app, ["rules"])
167
+
168
+ assert result.exit_code == 0
169
+ assert "Project Rules" in result.output
170
+ assert ".zai/rules.md" in result.output
171
+
172
+
156
173
  def test_context_tokens_and_summary_commands(tmp_path, monkeypatch):
157
174
  app = settings_app()
158
175
  monkeypatch.chdir(tmp_path)
@@ -255,6 +272,25 @@ def test_model_test_passes_and_reports_unavailable():
255
272
  assert "Model unavailable" in unavailable.output
256
273
 
257
274
 
275
+ def test_model_test_reports_actionable_auth_failure():
276
+ from zai.core.errors import AuthenticationError
277
+
278
+ app = settings_app()
279
+ runner = CliRunner()
280
+ provider = MagicMock()
281
+ provider.name = "groq"
282
+ provider.is_available.return_value = True
283
+ provider.chat.side_effect = AuthenticationError("groq", "bad key")
284
+
285
+ with patch("zai.core.fallback.get_provider", return_value=provider):
286
+ result = runner.invoke(app, ["model", "test", "groq"])
287
+
288
+ assert result.exit_code == 0
289
+ assert "invalid credentials" in result.output
290
+ assert "zai auth login groq" in result.output
291
+ assert "bad key" in result.output
292
+
293
+
258
294
  def test_model_configure_updates_builtin_runtime_settings():
259
295
  app = settings_app()
260
296
  config = {
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"