zai-cli 0.1.9__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.9 → zai_cli-0.2.0}/CHANGELOG.md +17 -0
  2. {zai_cli-0.1.9/zai_cli.egg-info → zai_cli-0.2.0}/PKG-INFO +42 -1
  3. {zai_cli-0.1.9 → zai_cli-0.2.0}/README.md +41 -0
  4. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_interactive.py +69 -34
  5. zai_cli-0.2.0/tests/test_project_rules.py +33 -0
  6. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_settings_cli.py +16 -0
  7. zai_cli-0.2.0/zai/__init__.py +1 -0
  8. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/interactive.py +28 -1
  9. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/settings.py +11 -0
  10. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/status.py +7 -0
  11. zai_cli-0.2.0/zai/core/project_rules.py +111 -0
  12. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/main.py +13 -0
  13. {zai_cli-0.1.9 → zai_cli-0.2.0/zai_cli.egg-info}/PKG-INFO +42 -1
  14. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/SOURCES.txt +2 -0
  15. zai_cli-0.1.9/zai/__init__.py +0 -1
  16. {zai_cli-0.1.9 → zai_cli-0.2.0}/LICENSE +0 -0
  17. {zai_cli-0.1.9 → zai_cli-0.2.0}/MANIFEST.in +0 -0
  18. {zai_cli-0.1.9 → zai_cli-0.2.0}/pyproject.toml +0 -0
  19. {zai_cli-0.1.9 → zai_cli-0.2.0}/scripts/release_preflight.py +0 -0
  20. {zai_cli-0.1.9 → zai_cli-0.2.0}/setup.cfg +0 -0
  21. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_agent.py +0 -0
  22. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_auth.py +0 -0
  23. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_browser.py +0 -0
  24. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_code_runner.py +0 -0
  25. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_config_main.py +0 -0
  26. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_core.py +0 -0
  27. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_errors.py +0 -0
  28. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_hooks_skills_session.py +0 -0
  29. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_input.py +0 -0
  30. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_integrations_cli.py +0 -0
  31. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_mcp.py +0 -0
  32. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_plugins.py +0 -0
  33. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_process.py +0 -0
  34. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_project_memory.py +0 -0
  35. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_providers.py +0 -0
  36. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_release_preflight.py +0 -0
  37. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_search.py +0 -0
  38. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_security.py +0 -0
  39. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_storage.py +0 -0
  40. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_streaming.py +0 -0
  41. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_tokens.py +0 -0
  42. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_tool_schema.py +0 -0
  43. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_tools.py +0 -0
  44. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_undo.py +0 -0
  45. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_utilities.py +0 -0
  46. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_vision.py +0 -0
  47. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_watch.py +0 -0
  48. {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_workflows.py +0 -0
  49. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/__main__.py +0 -0
  50. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/__init__.py +0 -0
  51. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/common.py +0 -0
  52. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/integrations.py +0 -0
  53. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/setup_wizard.py +0 -0
  54. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/utilities.py +0 -0
  55. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/workflows.py +0 -0
  56. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/commit.md +0 -0
  57. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/explain.md +0 -0
  58. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/feature.md +0 -0
  59. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/fix.md +0 -0
  60. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/review.md +0 -0
  61. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/config.py +0 -0
  62. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/__init__.py +0 -0
  63. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/agent.py +0 -0
  64. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/auth.py +0 -0
  65. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/cancellation.py +0 -0
  66. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/commands.py +0 -0
  67. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/compaction.py +0 -0
  68. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/context.py +0 -0
  69. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/errors.py +0 -0
  70. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/fallback.py +0 -0
  71. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/hooks.py +0 -0
  72. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/memory.py +0 -0
  73. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/process.py +0 -0
  74. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/project_memory.py +0 -0
  75. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/repomap.py +0 -0
  76. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/runtime.py +0 -0
  77. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/security.py +0 -0
  78. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/session.py +0 -0
  79. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/storage.py +0 -0
  80. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/streaming.py +0 -0
  81. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/tokens.py +0 -0
  82. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/tool_schema.py +0 -0
  83. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/undo.py +0 -0
  84. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/watch.py +0 -0
  85. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/mcp/__init__.py +0 -0
  86. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/mcp/client.py +0 -0
  87. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/mcp/manager.py +0 -0
  88. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/plugins/__init__.py +0 -0
  89. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/plugins/base.py +0 -0
  90. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/plugins/loader.py +0 -0
  91. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/__init__.py +0 -0
  92. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/anthropic.py +0 -0
  93. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/base.py +0 -0
  94. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/cerebras.py +0 -0
  95. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/gemini.py +0 -0
  96. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/groq.py +0 -0
  97. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/ollama.py +0 -0
  98. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/openai.py +0 -0
  99. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/openrouter.py +0 -0
  100. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/qwen.py +0 -0
  101. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/skills/__init__.py +0 -0
  102. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/skills/registry.py +0 -0
  103. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/__init__.py +0 -0
  104. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/browser.py +0 -0
  105. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/code_runner.py +0 -0
  106. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/files.py +0 -0
  107. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/git.py +0 -0
  108. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/search.py +0 -0
  109. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/vision.py +0 -0
  110. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/ui/__init__.py +0 -0
  111. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/ui/input.py +0 -0
  112. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/dependency_links.txt +0 -0
  113. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/entry_points.txt +0 -0
  114. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/requires.txt +0 -0
  115. {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/top_level.txt +0 -0
@@ -5,6 +5,23 @@ 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
+
8
25
  ## [0.1.9] - 2026-06-30
9
26
 
10
27
  ### 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.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
  ```
@@ -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
  ```
@@ -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,34 @@ 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
+
273
308
  def test_interactive_manual_compact_summarizes_history(tmp_path, monkeypatch, capsys):
274
309
  monkeypatch.chdir(tmp_path)
275
310
  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,34 @@ 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 / ".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
+
156
172
  def test_context_tokens_and_summary_commands(tmp_path, monkeypatch):
157
173
  app = settings_app()
158
174
  monkeypatch.chdir(tmp_path)
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -33,6 +33,11 @@ from ..core.project_memory import (
33
33
  refresh_project_memory,
34
34
  remember_fact,
35
35
  )
36
+ from ..core.project_rules import (
37
+ format_project_rules,
38
+ load_project_rules,
39
+ project_rules_prompt,
40
+ )
36
41
  from ..core.session import (
37
42
  delete_session,
38
43
  list_sessions,
@@ -58,7 +63,7 @@ CORE_COMMANDS = {
58
63
  "help", "clear", "files", "diff", "undo", "plan", "test",
59
64
  "watch", "commit", "review", "fix", "explain", "feature",
60
65
  "session", "resume", "model", "providers", "context", "compact", "memory", "remember",
61
- "forget", "commands", "auth", "setup", "exit", "quit",
66
+ "forget", "rules", "commands", "auth", "setup", "exit", "quit",
62
67
  }
63
68
 
64
69
  INTERACTIVE_COMMANDS = [
@@ -71,6 +76,7 @@ INTERACTIVE_COMMANDS = [
71
76
  ("/context", "Show loaded project context"),
72
77
  ("/context tokens", "Show estimated context token usage"),
73
78
  ("/context summary", "Show compacted context summary"),
79
+ ("/rules", "Show loaded project instruction files"),
74
80
  ("/compact", "Summarize older session history now"),
75
81
  ("/exit", "Exit interactive mode"),
76
82
  ("/quit", "Exit interactive mode"),
@@ -102,6 +108,7 @@ PLAIN_COMMAND_ALIASES = {
102
108
  "auth status": "/auth status",
103
109
  "providers": "/providers",
104
110
  "context": "/context",
111
+ "rules": "/rules",
105
112
  "keys": "/setup",
106
113
  "api keys": "/setup",
107
114
  "model list": "/model list",
@@ -193,6 +200,7 @@ def _show_commands() -> None:
193
200
  ("Project", "/context", "Show loaded project context"),
194
201
  ("Project", "/context tokens", "Show estimated context token usage"),
195
202
  ("Project", "/context summary", "Show compacted context summary"),
203
+ ("Project", "/rules", "Show loaded project instruction files"),
196
204
  ("Project", "/compact", "Summarize older session history now"),
197
205
  ("Project", "/files", "List files in the current folder"),
198
206
  ("Project", "/diff", "Show git diff"),
@@ -493,6 +501,13 @@ def run_interactive(model: str = None) -> None:
493
501
  project_memory = load_project_memory(cwd)
494
502
  if project_memory:
495
503
  console.print("[dim]Project memory loaded: .zai/project_memory.json[/dim]")
504
+ project_rules = load_project_rules(cwd)
505
+ if project_rules:
506
+ console.print(
507
+ "[dim]Project rules loaded: "
508
+ + ", ".join(rule.relative_path for rule in project_rules)
509
+ + "[/dim]"
510
+ )
496
511
  memory_offer_shown = project_memory is not None
497
512
  if (Path(cwd) / "CLAUDE.md").exists():
498
513
  console.print(f"[dim]CLAUDE.md loaded from {cwd}[/dim]")
@@ -554,6 +569,12 @@ def run_interactive(model: str = None) -> None:
554
569
  content=project_memory_prompt(project_memory),
555
570
  pinned=True,
556
571
  ))
572
+ if project_rules:
573
+ initial_history.append(Message(
574
+ role="user",
575
+ content=project_rules_prompt(project_rules),
576
+ pinned=True,
577
+ ))
557
578
  session_context.replace_messages(initial_history)
558
579
  history = session_context.get_messages()
559
580
 
@@ -630,6 +651,12 @@ def run_interactive(model: str = None) -> None:
630
651
  context_limit=session_context.max_tokens,
631
652
  detail=detail,
632
653
  ))
654
+ elif stripped == "/rules":
655
+ console.print(Panel(
656
+ format_project_rules(load_project_rules(cwd)),
657
+ title="[cyan]Project Rules[/cyan]",
658
+ border_style="cyan",
659
+ ))
633
660
  elif stripped == "/compact":
634
661
  before = len(history)
635
662
  compacted, summary = manual_compact_messages(
@@ -36,6 +36,7 @@ from ..core.project_memory import (
36
36
  refresh_project_memory,
37
37
  remember_fact,
38
38
  )
39
+ from ..core.project_rules import format_project_rules, load_project_rules
39
40
  from ..core.session import (
40
41
  delete_session,
41
42
  export_session,
@@ -445,6 +446,15 @@ def context(
445
446
  console.print("[red]Usage: zai context [show|tokens|summary][/red]")
446
447
 
447
448
 
449
+ def rules():
450
+ """Show project instruction files loaded by zai."""
451
+ console.print(Panel(
452
+ format_project_rules(load_project_rules(".")),
453
+ title="[cyan]Project Rules[/cyan]",
454
+ border_style="cyan",
455
+ ))
456
+
457
+
448
458
  def session(
449
459
  action: str = typer.Argument("list", help="list, show, search, rename, delete"),
450
460
  name: Optional[str] = typer.Argument(None),
@@ -616,6 +626,7 @@ def register_settings_commands(app: typer.Typer) -> None:
616
626
  app.command()(auth)
617
627
  app.command()(model)
618
628
  app.command()(context)
629
+ app.command()(rules)
619
630
  app.command()(memory)
620
631
  app.command()(session)
621
632
  app.command()(undo)
@@ -10,6 +10,7 @@ from rich.table import Table
10
10
  from ..config import get_api_key, get_model_config, get_models, load_config
11
11
  from ..core.auth import auth_status, get_auth_provider
12
12
  from ..core.project_memory import load_project_memory
13
+ from ..core.project_rules import load_project_rules
13
14
  from ..core.repomap import _cache_path
14
15
  from ..core.tokens import (
15
16
  compacted_summaries,
@@ -152,6 +153,7 @@ def context_lines(
152
153
  ) -> list[str]:
153
154
  root = Path(cwd).resolve()
154
155
  memory = load_project_memory(root)
156
+ rules = load_project_rules(root)
155
157
  repo_cache = _cache_path(root)
156
158
  recent_files = recent_files or []
157
159
  messages = messages or []
@@ -164,6 +166,11 @@ def context_lines(
164
166
  if memory
165
167
  else "Project memory: missing"
166
168
  ),
169
+ (
170
+ "Project rules: " + ", ".join(rule.relative_path for rule in rules)
171
+ if rules
172
+ else "Project rules: none found"
173
+ ),
167
174
  (
168
175
  "Repo map/index: cache available at .zai/cache/repomap.json"
169
176
  if repo_cache.exists()
@@ -0,0 +1,111 @@
1
+ """Project instruction file discovery and prompt formatting."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ RULE_FILENAMES = (
9
+ "ZAI.md",
10
+ ".zai/rules.md",
11
+ "AGENTS.md",
12
+ "CLAUDE.md",
13
+ "GEMINI.md",
14
+ )
15
+ MAX_RULE_FILE_BYTES = 64 * 1024
16
+ MAX_RULE_PROMPT_CHARS = 24_000
17
+
18
+ SECRET_PATTERNS = (
19
+ re.compile(r"(?i)(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[^'\"\s]+"),
20
+ re.compile(r"(?i)bearer\s+[a-z0-9._\-]+"),
21
+ re.compile(r"sk-[A-Za-z0-9_\-]{12,}"),
22
+ )
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ProjectRule:
27
+ """A loaded project instruction file."""
28
+
29
+ path: Path
30
+ relative_path: str
31
+ content: str
32
+
33
+
34
+ def _project_root(cwd: str | Path = ".") -> Path:
35
+ path = Path(cwd).resolve()
36
+ for candidate in (path, *path.parents):
37
+ if (candidate / ".git").exists():
38
+ return candidate
39
+ return path
40
+
41
+
42
+ def redact_rule_secrets(text: str) -> str:
43
+ redacted = text
44
+ for pattern in SECRET_PATTERNS:
45
+ redacted = pattern.sub("[REDACTED_SECRET]", redacted)
46
+ return redacted
47
+
48
+
49
+ def _read_rule(path: Path, base: Path) -> ProjectRule | None:
50
+ try:
51
+ if not path.is_file() or path.stat().st_size > MAX_RULE_FILE_BYTES:
52
+ return None
53
+ content = path.read_text(encoding="utf-8")
54
+ except (OSError, UnicodeError):
55
+ return None
56
+ content = redact_rule_secrets(content.strip())
57
+ if not content:
58
+ return None
59
+ if len(content) > MAX_RULE_PROMPT_CHARS:
60
+ content = content[:MAX_RULE_PROMPT_CHARS] + "\n\n[... project rules truncated ...]"
61
+ return ProjectRule(
62
+ path=path,
63
+ relative_path=path.relative_to(base).as_posix(),
64
+ content=content,
65
+ )
66
+
67
+
68
+ def load_project_rules(cwd: str | Path = ".") -> list[ProjectRule]:
69
+ """Load bounded project rules from project root and current folder."""
70
+ current = Path(cwd).resolve()
71
+ root = _project_root(current)
72
+ bases = [root]
73
+ if current != root:
74
+ bases.append(current)
75
+
76
+ rules: list[ProjectRule] = []
77
+ seen: set[Path] = set()
78
+ for base in bases:
79
+ for name in RULE_FILENAMES:
80
+ path = (base / name).resolve()
81
+ if path in seen:
82
+ continue
83
+ seen.add(path)
84
+ rule = _read_rule(path, root)
85
+ if rule:
86
+ rules.append(rule)
87
+ return rules
88
+
89
+
90
+ def project_rules_prompt(rules: list[ProjectRule]) -> str:
91
+ if not rules:
92
+ return ""
93
+ lines = [
94
+ "Project rules loaded from instruction files.",
95
+ "Follow these rules for this project unless the user explicitly says otherwise.",
96
+ ]
97
+ for rule in rules:
98
+ lines.append(f"\n--- {rule.relative_path} ---\n{rule.content}")
99
+ return "\n".join(lines).strip()
100
+
101
+
102
+ def format_project_rules(rules: list[ProjectRule]) -> str:
103
+ if not rules:
104
+ return (
105
+ "No project rules found. Add ZAI.md or .zai/rules.md in this project "
106
+ "to give zai persistent instructions."
107
+ )
108
+ lines = ["Loaded project rules:"]
109
+ for rule in rules:
110
+ lines.append(f"- {rule.relative_path} ({len(rule.content):,} chars)")
111
+ return "\n".join(lines)
@@ -1,3 +1,5 @@
1
+ import os
2
+
1
3
  import typer
2
4
  from typing import Optional
3
5
  from typer.core import TyperGroup
@@ -15,6 +17,7 @@ from .core.fallback import (
15
17
  )
16
18
  from .core.context import ContextManager
17
19
  from .core.memory import save_session, get_last_session
20
+ from .core.project_rules import load_project_rules, project_rules_prompt
18
21
  from .core.errors import show_error, AllModelsFailedError
19
22
  from .core.runtime import configure as configure_runtime
20
23
  from .core.runtime import plain_enabled, print_exception
@@ -55,6 +58,7 @@ if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
55
58
  console = Console()
56
59
 
57
60
  context = ContextManager()
61
+ _RULES_LOADED_FOR: str | None = None
58
62
  INTERACTIVE_COMMANDS = [
59
63
  ("/help", "Show interactive help"),
60
64
  ("/commands", "List built-in and plugin slash commands"),
@@ -63,6 +67,7 @@ INTERACTIVE_COMMANDS = [
63
67
  ("/auth login <provider>", "Connect a provider"),
64
68
  ("/providers", "Show provider connection status"),
65
69
  ("/context", "Show loaded project context"),
70
+ ("/rules", "Show loaded project instruction files"),
66
71
  ("/clear", "Clear the terminal"),
67
72
  ("/files", "List files in the current folder"),
68
73
  ("/diff", "Show git diff"),
@@ -124,6 +129,7 @@ def _parse_files_from_response(content: str) -> dict:
124
129
 
125
130
 
126
131
  def _chat(message: str, model: str = None) -> str:
132
+ global _RULES_LOADED_FOR
127
133
  config = load_config()
128
134
  preferred = model or config["default_model"]
129
135
  context.set_model(preferred)
@@ -132,6 +138,13 @@ def _chat(message: str, model: str = None) -> str:
132
138
  show_error("No AI provider available. Run: zai setup, or start Ollama.")
133
139
  return ""
134
140
 
141
+ cwd = os.getcwd()
142
+ if _RULES_LOADED_FOR != cwd:
143
+ rules = load_project_rules(cwd)
144
+ if rules:
145
+ context.add("user", project_rules_prompt(rules), pinned=True)
146
+ _RULES_LOADED_FOR = cwd
147
+
135
148
  context.add("user", message)
136
149
 
137
150
  if config.get("auto_compact", True) and context.is_near_limit(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zai-cli
3
- Version: 0.1.9
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
  ```
@@ -19,6 +19,7 @@ tests/test_mcp.py
19
19
  tests/test_plugins.py
20
20
  tests/test_process.py
21
21
  tests/test_project_memory.py
22
+ tests/test_project_rules.py
22
23
  tests/test_providers.py
23
24
  tests/test_release_preflight.py
24
25
  tests/test_search.py
@@ -65,6 +66,7 @@ zai/core/hooks.py
65
66
  zai/core/memory.py
66
67
  zai/core/process.py
67
68
  zai/core/project_memory.py
69
+ zai/core/project_rules.py
68
70
  zai/core/repomap.py
69
71
  zai/core/runtime.py
70
72
  zai/core/security.py
@@ -1 +0,0 @@
1
- __version__ = "0.1.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes