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.
- {zai_cli-0.1.9 → zai_cli-0.2.0}/CHANGELOG.md +17 -0
- {zai_cli-0.1.9/zai_cli.egg-info → zai_cli-0.2.0}/PKG-INFO +42 -1
- {zai_cli-0.1.9 → zai_cli-0.2.0}/README.md +41 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_interactive.py +69 -34
- zai_cli-0.2.0/tests/test_project_rules.py +33 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_settings_cli.py +16 -0
- zai_cli-0.2.0/zai/__init__.py +1 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/interactive.py +28 -1
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/settings.py +11 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/status.py +7 -0
- zai_cli-0.2.0/zai/core/project_rules.py +111 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/main.py +13 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0/zai_cli.egg-info}/PKG-INFO +42 -1
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/SOURCES.txt +2 -0
- zai_cli-0.1.9/zai/__init__.py +0 -1
- {zai_cli-0.1.9 → zai_cli-0.2.0}/LICENSE +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/MANIFEST.in +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/pyproject.toml +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/scripts/release_preflight.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/setup.cfg +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_agent.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_auth.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_browser.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_code_runner.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_config_main.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_core.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_errors.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_hooks_skills_session.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_input.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_integrations_cli.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_mcp.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_plugins.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_process.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_project_memory.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_providers.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_release_preflight.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_search.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_security.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_storage.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_streaming.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_tokens.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_tool_schema.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_tools.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_undo.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_utilities.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_vision.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_watch.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/tests/test_workflows.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/__main__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/common.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/integrations.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/setup_wizard.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/utilities.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/cli/workflows.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/commit.md +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/explain.md +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/feature.md +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/fix.md +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/commands/review.md +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/config.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/agent.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/auth.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/cancellation.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/commands.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/compaction.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/context.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/errors.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/fallback.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/hooks.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/memory.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/process.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/project_memory.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/repomap.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/runtime.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/security.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/session.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/storage.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/streaming.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/tokens.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/tool_schema.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/undo.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/core/watch.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/mcp/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/mcp/client.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/mcp/manager.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/plugins/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/plugins/base.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/plugins/loader.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/anthropic.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/base.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/cerebras.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/gemini.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/groq.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/ollama.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/openai.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/openrouter.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/providers/qwen.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/skills/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/skills/registry.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/browser.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/code_runner.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/files.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/git.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/search.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/tools/vision.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/ui/__init__.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai/ui/input.py +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/dependency_links.txt +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/entry_points.txt +0 -0
- {zai_cli-0.1.9 → zai_cli-0.2.0}/zai_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
186
|
-
with patch("zai.cli.interactive.plugin_loader.
|
|
187
|
-
with patch("zai.cli.interactive.
|
|
188
|
-
with patch(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
214
|
-
with patch("zai.cli.interactive.plugin_loader.
|
|
215
|
-
with patch("zai.cli.interactive.
|
|
216
|
-
with patch(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
zai_cli-0.1.9/zai/__init__.py
DELETED
|
@@ -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
|
|
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
|