aizen-ai-cli 2.4.1__tar.gz → 2.4.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.
- aizen_ai_cli-2.4.2/PKG-INFO +103 -0
- aizen_ai_cli-2.4.2/README.md +63 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/agent.py +7 -2
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/commands.py +4 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/config.py +1 -1
- aizen_ai_cli-2.4.2/aizen/tools/commands.py +279 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/dispatcher.py +4 -4
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/utils.py +1 -1
- aizen_ai_cli-2.4.2/aizen_ai_cli.egg-info/PKG-INFO +103 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/pyproject.toml +1 -1
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/setup.py +1 -1
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_commands.py +1 -8
- aizen_ai_cli-2.4.1/PKG-INFO +0 -276
- aizen_ai_cli-2.4.1/README.md +0 -236
- aizen_ai_cli-2.4.1/aizen/tools/commands.py +0 -222
- aizen_ai_cli-2.4.1/aizen_ai_cli.egg-info/PKG-INFO +0 -276
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/MANIFEST.in +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/__init__.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/context.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/exceptions.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/logging_config.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/main.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/mcp.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/plugins.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/retry.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/session.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/__init__.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/file_ops.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/helpers.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/search.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/SOURCES.txt +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/dependency_links.txt +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/entry_points.txt +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/requires.txt +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/top_level.txt +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/requirements.txt +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/setup.cfg +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_agent.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_config.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_context.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_dispatcher.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_file_ops.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_helpers.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_main.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_mcp.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_plugins.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_retry.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_search.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_session.py +0 -0
- {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aizen-ai-cli
|
|
3
|
+
Version: 2.4.2
|
|
4
|
+
Summary: Aizen AI Agent — A professional-grade AI coding assistant for your terminal.
|
|
5
|
+
Author: Irtaza Malik
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/irtaza302/aizen-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/irtaza302/aizen-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/irtaza302/aizen-agent/issues
|
|
10
|
+
Keywords: ai,cli,coding-assistant,terminal,openrouter,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: openai>=1.0
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0
|
|
27
|
+
Requires-Dist: rich>=13.0
|
|
28
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
29
|
+
Requires-Dist: questionary>=2.0.0
|
|
30
|
+
Requires-Dist: mcp>=1.0.0
|
|
31
|
+
Provides-Extra: tiktoken
|
|
32
|
+
Requires-Dist: tiktoken>=0.5; extra == "tiktoken"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-mock>=3.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
40
|
+
|
|
41
|
+
# Aizen AI Agent 🚀
|
|
42
|
+
|
|
43
|
+
[](https://github.com/irtaza302/aizen-agent/actions/workflows/ci.yml)
|
|
44
|
+
|
|
45
|
+
Aizen is a powerful, asynchronous AI assistant that integrates seamlessly into your terminal workflow. It reads your code, edits files safely, runs commands, and provides real‑time, richly formatted assistance—all while keeping costs transparent and sessions persistent.
|
|
46
|
+
|
|
47
|
+
## 🌟 Key Benefits
|
|
48
|
+
|
|
49
|
+
- **Effortless Integration** — Operates directly in your terminal, preserving shell state across commands.
|
|
50
|
+
- **Intelligent Editing** — Perform precise, color‑coded file edits with `edit_file`.
|
|
51
|
+
- **Background Execution** — Run long‑running tasks asynchronously and retrieve results later.
|
|
52
|
+
- **Cost‑Aware Usage** — Real‑time cost estimation for all major LLMs.
|
|
53
|
+
- **Persistent Sessions** — Save and restore conversations with checkpoints.
|
|
54
|
+
- **Rich Visual Feedback** — Stream responses with live previews and animated thought indicators.
|
|
55
|
+
- **Extensible Architecture** — Custom plugins and project‑specific rules tailor Aizen to your workflow.
|
|
56
|
+
- **Comprehensive Logging** — Rotating logs with optional verbose output for debugging.
|
|
57
|
+
|
|
58
|
+
## 🚀 Core Features
|
|
59
|
+
|
|
60
|
+
### Asynchronous Architecture
|
|
61
|
+
- Fully asynchronous operations using `asyncio` and `AsyncOpenAI` for concurrent processing, parallel tool runs, and streaming.
|
|
62
|
+
|
|
63
|
+
### Stateful Terminal Session
|
|
64
|
+
- Environment variables and directory changes persist across interactions.
|
|
65
|
+
|
|
66
|
+
### Rich Markdown Rendering
|
|
67
|
+
- Full Markdown support with headers, code blocks, lists, and styling via Rich.
|
|
68
|
+
|
|
69
|
+
### Surgical File Editing
|
|
70
|
+
- Precise search‑and‑replace with color‑coded diff previews (`edit_file`).
|
|
71
|
+
|
|
72
|
+
### Vision Support
|
|
73
|
+
- Native image handling and encoding for Vision APIs (e.g., GPT‑4o, Claude 3.5 Sonnet).
|
|
74
|
+
|
|
75
|
+
### Real‑Time Command Streaming
|
|
76
|
+
- Background command execution with async streaming of stdout/stderr; use `run_command --background`.
|
|
77
|
+
|
|
78
|
+
## 🎛️ Workflow Tools
|
|
79
|
+
|
|
80
|
+
- **Background Tasks** — Run non‑blocking commands; monitor with `check_background_task`; cancel with `kill_background_task`.
|
|
81
|
+
- **Session Persistence** — Powered by SQLite (`~/.aizen_sessions/aizen.db`), auto‑migrating older JSON sessions.
|
|
82
|
+
- **Project‑Specific Rules** — Auto‑load `.aizen_rules` or `.cursorrules` for repo‑specific behavior.
|
|
83
|
+
- **Smart Autocomplete** — TAB‑completion with `.gitignore` awareness and directory traversal.
|
|
84
|
+
|
|
85
|
+
## 💰 Cost Tracking
|
|
86
|
+
|
|
87
|
+
- Real‑time token counting for inputs and outputs.
|
|
88
|
+
- Current cost estimate shown in the CLI status bar.
|
|
89
|
+
- Supports Anthropic (Claude 3.5/3.7 Sonnet, Opus, Haiku), Google (Gemini 2.5 Pro/Flash), and OpenAI (GPT‑4o, o1, o3‑mini).
|
|
90
|
+
|
|
91
|
+
## 📌 Session Management
|
|
92
|
+
|
|
93
|
+
- `/checkpoint [name]` — Save conversation snapshots.
|
|
94
|
+
- `/restore [name]` — Restore a previous checkpoint.
|
|
95
|
+
|
|
96
|
+
## 📁 Structured Logging
|
|
97
|
+
|
|
98
|
+
- Logs stored at `~/.aizen_logs/aizen.log` (rotated, 5 MB caps, 3 files).
|
|
99
|
+
- Verbose flag mirrors output to console.
|
|
100
|
+
|
|
101
|
+
## 📦 Publishing & Development
|
|
102
|
+
|
|
103
|
+
- Use `publish.sh` to build and publish to PyPI, NPM, and PyInstaller binaries.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Aizen AI Agent 🚀
|
|
2
|
+
|
|
3
|
+
[](https://github.com/irtaza302/aizen-agent/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Aizen is a powerful, asynchronous AI assistant that integrates seamlessly into your terminal workflow. It reads your code, edits files safely, runs commands, and provides real‑time, richly formatted assistance—all while keeping costs transparent and sessions persistent.
|
|
6
|
+
|
|
7
|
+
## 🌟 Key Benefits
|
|
8
|
+
|
|
9
|
+
- **Effortless Integration** — Operates directly in your terminal, preserving shell state across commands.
|
|
10
|
+
- **Intelligent Editing** — Perform precise, color‑coded file edits with `edit_file`.
|
|
11
|
+
- **Background Execution** — Run long‑running tasks asynchronously and retrieve results later.
|
|
12
|
+
- **Cost‑Aware Usage** — Real‑time cost estimation for all major LLMs.
|
|
13
|
+
- **Persistent Sessions** — Save and restore conversations with checkpoints.
|
|
14
|
+
- **Rich Visual Feedback** — Stream responses with live previews and animated thought indicators.
|
|
15
|
+
- **Extensible Architecture** — Custom plugins and project‑specific rules tailor Aizen to your workflow.
|
|
16
|
+
- **Comprehensive Logging** — Rotating logs with optional verbose output for debugging.
|
|
17
|
+
|
|
18
|
+
## 🚀 Core Features
|
|
19
|
+
|
|
20
|
+
### Asynchronous Architecture
|
|
21
|
+
- Fully asynchronous operations using `asyncio` and `AsyncOpenAI` for concurrent processing, parallel tool runs, and streaming.
|
|
22
|
+
|
|
23
|
+
### Stateful Terminal Session
|
|
24
|
+
- Environment variables and directory changes persist across interactions.
|
|
25
|
+
|
|
26
|
+
### Rich Markdown Rendering
|
|
27
|
+
- Full Markdown support with headers, code blocks, lists, and styling via Rich.
|
|
28
|
+
|
|
29
|
+
### Surgical File Editing
|
|
30
|
+
- Precise search‑and‑replace with color‑coded diff previews (`edit_file`).
|
|
31
|
+
|
|
32
|
+
### Vision Support
|
|
33
|
+
- Native image handling and encoding for Vision APIs (e.g., GPT‑4o, Claude 3.5 Sonnet).
|
|
34
|
+
|
|
35
|
+
### Real‑Time Command Streaming
|
|
36
|
+
- Background command execution with async streaming of stdout/stderr; use `run_command --background`.
|
|
37
|
+
|
|
38
|
+
## 🎛️ Workflow Tools
|
|
39
|
+
|
|
40
|
+
- **Background Tasks** — Run non‑blocking commands; monitor with `check_background_task`; cancel with `kill_background_task`.
|
|
41
|
+
- **Session Persistence** — Powered by SQLite (`~/.aizen_sessions/aizen.db`), auto‑migrating older JSON sessions.
|
|
42
|
+
- **Project‑Specific Rules** — Auto‑load `.aizen_rules` or `.cursorrules` for repo‑specific behavior.
|
|
43
|
+
- **Smart Autocomplete** — TAB‑completion with `.gitignore` awareness and directory traversal.
|
|
44
|
+
|
|
45
|
+
## 💰 Cost Tracking
|
|
46
|
+
|
|
47
|
+
- Real‑time token counting for inputs and outputs.
|
|
48
|
+
- Current cost estimate shown in the CLI status bar.
|
|
49
|
+
- Supports Anthropic (Claude 3.5/3.7 Sonnet, Opus, Haiku), Google (Gemini 2.5 Pro/Flash), and OpenAI (GPT‑4o, o1, o3‑mini).
|
|
50
|
+
|
|
51
|
+
## 📌 Session Management
|
|
52
|
+
|
|
53
|
+
- `/checkpoint [name]` — Save conversation snapshots.
|
|
54
|
+
- `/restore [name]` — Restore a previous checkpoint.
|
|
55
|
+
|
|
56
|
+
## 📁 Structured Logging
|
|
57
|
+
|
|
58
|
+
- Logs stored at `~/.aizen_logs/aizen.log` (rotated, 5 MB caps, 3 files).
|
|
59
|
+
- Verbose flag mirrors output to console.
|
|
60
|
+
|
|
61
|
+
## 📦 Publishing & Development
|
|
62
|
+
|
|
63
|
+
- Use `publish.sh` to build and publish to PyPI, NPM, and PyInstaller binaries.
|
|
@@ -10,6 +10,7 @@ Extracted from main.py to enable:
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import json
|
|
12
12
|
import random
|
|
13
|
+
import re
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
from rich.live import Live
|
|
@@ -159,7 +160,11 @@ class AgentRunner:
|
|
|
159
160
|
full_content += delta.content
|
|
160
161
|
if full_content.strip():
|
|
161
162
|
try:
|
|
162
|
-
|
|
163
|
+
# Strip reasoning/thought tags for cleaner UI display
|
|
164
|
+
cleaned_content = re.sub(r'<think>.*?(?:</think>|$)', '', full_content, flags=re.DOTALL)
|
|
165
|
+
cleaned_content = re.sub(r'<\|channel>thought.*?(?:<channel\|>|$)', '', cleaned_content, flags=re.DOTALL)
|
|
166
|
+
|
|
167
|
+
display_content = f"**◆ AIZEN:** {cleaned_content.strip()}"
|
|
163
168
|
rendered = Markdown(display_content)
|
|
164
169
|
live.update(rendered)
|
|
165
170
|
except Exception:
|
|
@@ -192,7 +197,7 @@ class AgentRunner:
|
|
|
192
197
|
tool_text.append(" ...", style=f"{Theme.MUTED}")
|
|
193
198
|
live.update(tool_text)
|
|
194
199
|
|
|
195
|
-
except Exception
|
|
200
|
+
except Exception:
|
|
196
201
|
# Re-raise — let the caller (main_loop) handle specific exception types
|
|
197
202
|
raise
|
|
198
203
|
|
|
@@ -658,6 +658,10 @@ async def handle_slash_command(
|
|
|
658
658
|
console.print("[yellow]Commit aborted.[/yellow]\n")
|
|
659
659
|
return False
|
|
660
660
|
|
|
661
|
+
if final_msg is None:
|
|
662
|
+
console.print(f" [{Theme.WARNING}]Commit aborted.[/{Theme.WARNING}]\n")
|
|
663
|
+
return False
|
|
664
|
+
|
|
661
665
|
final_msg = final_msg.strip()
|
|
662
666
|
if not final_msg:
|
|
663
667
|
console.print(f" [{Theme.ERROR}]Error: Commit message cannot be empty. Aborted.[/{Theme.ERROR}]\n")
|
|
@@ -20,7 +20,7 @@ logger = logging.getLogger("aizen")
|
|
|
20
20
|
|
|
21
21
|
# Read version from installed package metadata (stays in sync with pyproject.toml).
|
|
22
22
|
# Falls back to a hardcoded value only when running from source without installing.
|
|
23
|
-
_FALLBACK_VERSION = "2.4.
|
|
23
|
+
_FALLBACK_VERSION = "2.4.2"
|
|
24
24
|
try:
|
|
25
25
|
VERSION = _pkg_version("aizen-ai-cli")
|
|
26
26
|
except PackageNotFoundError:
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command execution tools: run_command, background task management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.live import Live
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
from ..config import DANGEROUS_PATTERNS, SAFE_COMMAND_PREFIXES, Theme, console
|
|
18
|
+
from ..logging_config import logger
|
|
19
|
+
from .helpers import _ask_permission, terminal_lock
|
|
20
|
+
|
|
21
|
+
# Global dictionary for tracking background tasks
|
|
22
|
+
# task_id -> {"process": Popen, "stdout": list, "stderr": list, "command": str}
|
|
23
|
+
background_tasks: dict[str, dict[str, Any]] = {}
|
|
24
|
+
background_tasks_lock = threading.Lock() # Protects background_tasks dict
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PersistentTerminal:
|
|
28
|
+
"""A stateful bash terminal session that persists across command executions."""
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.lock = threading.Lock()
|
|
31
|
+
self.proc = None
|
|
32
|
+
self.marker = "===AIZEN_CMD_END==="
|
|
33
|
+
self.stdout_buf = []
|
|
34
|
+
self.stderr_buf = []
|
|
35
|
+
|
|
36
|
+
def _start(self):
|
|
37
|
+
self.proc = subprocess.Popen(
|
|
38
|
+
["bash"],
|
|
39
|
+
stdin=subprocess.PIPE,
|
|
40
|
+
stdout=subprocess.PIPE,
|
|
41
|
+
stderr=subprocess.PIPE,
|
|
42
|
+
text=True,
|
|
43
|
+
bufsize=1,
|
|
44
|
+
env=os.environ.copy()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def reader(pipe, buf):
|
|
48
|
+
for line in iter(pipe.readline, ''):
|
|
49
|
+
buf.append(line)
|
|
50
|
+
|
|
51
|
+
threading.Thread(target=reader, args=(self.proc.stdout, self.stdout_buf), daemon=True).start()
|
|
52
|
+
threading.Thread(target=reader, args=(self.proc.stderr, self.stderr_buf), daemon=True).start()
|
|
53
|
+
|
|
54
|
+
def run(self, command: str, timeout: int = 120) -> tuple[str, str, int, bool, str]:
|
|
55
|
+
"""Runs a command and returns (stdout, stderr, exit_code, timeout_occurred, new_pwd)."""
|
|
56
|
+
with self.lock:
|
|
57
|
+
if self.proc is None or self.proc.poll() is not None:
|
|
58
|
+
self._start()
|
|
59
|
+
|
|
60
|
+
self.stdout_buf.clear()
|
|
61
|
+
self.stderr_buf.clear()
|
|
62
|
+
|
|
63
|
+
marker_str = f"{self.marker}_{uuid.uuid4().hex[:8]}"
|
|
64
|
+
|
|
65
|
+
# The payload. We echo the exit code and the current working directory.
|
|
66
|
+
cmd_payload = f"{command}\n__aizen_exit=$?; echo \"{marker_str}:$__aizen_exit:$(pwd)\"\n"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
self.proc.stdin.write(cmd_payload)
|
|
70
|
+
self.proc.stdin.flush()
|
|
71
|
+
except BrokenPipeError:
|
|
72
|
+
self._start()
|
|
73
|
+
self.proc.stdin.write(cmd_payload)
|
|
74
|
+
self.proc.stdin.flush()
|
|
75
|
+
|
|
76
|
+
start_time = time.time()
|
|
77
|
+
exit_code = 0
|
|
78
|
+
timeout_occurred = False
|
|
79
|
+
new_pwd = ""
|
|
80
|
+
|
|
81
|
+
with Live(
|
|
82
|
+
Text(" ▶ Running...", style="dim italic"),
|
|
83
|
+
console=console,
|
|
84
|
+
refresh_per_second=4,
|
|
85
|
+
transient=True,
|
|
86
|
+
) as live:
|
|
87
|
+
while True:
|
|
88
|
+
elapsed = time.time() - start_time
|
|
89
|
+
if elapsed > timeout:
|
|
90
|
+
timeout_occurred = True
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
if self.proc.poll() is not None:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
out_str = "".join(self.stdout_buf)
|
|
97
|
+
if marker_str in out_str:
|
|
98
|
+
# Give a tiny bit of time for stderr reader to catch up
|
|
99
|
+
time.sleep(0.05)
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
tail = "".join(self.stdout_buf[-15:])
|
|
103
|
+
display = Text()
|
|
104
|
+
display.append(f" ▶ Running ({elapsed:.0f}s)\n", style="dim italic")
|
|
105
|
+
display.append(tail.rstrip(), style="dim")
|
|
106
|
+
live.update(display)
|
|
107
|
+
|
|
108
|
+
time.sleep(0.1)
|
|
109
|
+
|
|
110
|
+
out_str = "".join(self.stdout_buf)
|
|
111
|
+
err_str = "".join(self.stderr_buf)
|
|
112
|
+
|
|
113
|
+
final_out = []
|
|
114
|
+
for line in out_str.splitlines():
|
|
115
|
+
if line.startswith(marker_str):
|
|
116
|
+
parts = line.split(":", 2)
|
|
117
|
+
if len(parts) >= 2:
|
|
118
|
+
try:
|
|
119
|
+
exit_code = int(parts[1])
|
|
120
|
+
except ValueError:
|
|
121
|
+
pass
|
|
122
|
+
if len(parts) >= 3:
|
|
123
|
+
new_pwd = parts[2].strip()
|
|
124
|
+
break
|
|
125
|
+
final_out.append(line)
|
|
126
|
+
|
|
127
|
+
output = "\n".join(final_out)
|
|
128
|
+
stderr_output = err_str.strip()
|
|
129
|
+
|
|
130
|
+
if timeout_occurred:
|
|
131
|
+
self.proc.kill()
|
|
132
|
+
self.proc = None
|
|
133
|
+
|
|
134
|
+
return output, stderr_output, exit_code, timeout_occurred, new_pwd
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Global persistent terminal instance
|
|
138
|
+
_terminal = PersistentTerminal()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_command_safe(command: str) -> bool:
|
|
142
|
+
"""Check if a command is safe to auto-execute without confirmation."""
|
|
143
|
+
cmd_stripped = command.strip()
|
|
144
|
+
|
|
145
|
+
for pattern in DANGEROUS_PATTERNS:
|
|
146
|
+
if re.search(pattern, cmd_stripped):
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
for safe in SAFE_COMMAND_PREFIXES:
|
|
150
|
+
if cmd_stripped == safe or cmd_stripped.startswith(safe + " "):
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_command_impl(command: str, auto_approve: bool = False, timeout: int = 120, background: bool = False) -> str:
|
|
157
|
+
"""Execute a shell command with safety checks. Uses PersistentTerminal unless background=True."""
|
|
158
|
+
logger.debug("run_command: %s (timeout=%ds, background=%s)", command, timeout, background)
|
|
159
|
+
|
|
160
|
+
safe = is_command_safe(command)
|
|
161
|
+
if not safe:
|
|
162
|
+
console.print(
|
|
163
|
+
Panel(
|
|
164
|
+
f"[bold {Theme.ACCENT}]◆ AIZEN[/bold {Theme.ACCENT}] [{Theme.TEXT}]wants to run:[/{Theme.TEXT}]\n\n[bold {Theme.TEXT}]{command}[/bold {Theme.TEXT}]",
|
|
165
|
+
border_style=Theme.BORDER,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
with terminal_lock:
|
|
169
|
+
if not _ask_permission(" ▸ Allow?", auto_approve):
|
|
170
|
+
return "User denied command execution."
|
|
171
|
+
elif safe:
|
|
172
|
+
console.print(f" [dim]▶ {command}{' (background)' if background else ''}[/dim]")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
if background:
|
|
176
|
+
# For background tasks, use isolated Popen so we don't block the persistent terminal
|
|
177
|
+
proc = subprocess.Popen(
|
|
178
|
+
command,
|
|
179
|
+
shell=True,
|
|
180
|
+
text=True,
|
|
181
|
+
stdout=subprocess.PIPE,
|
|
182
|
+
stderr=subprocess.PIPE,
|
|
183
|
+
)
|
|
184
|
+
task_id = f"bg_{uuid.uuid4().hex[:8]}"
|
|
185
|
+
task_info = {
|
|
186
|
+
"process": proc,
|
|
187
|
+
"stdout": [],
|
|
188
|
+
"stderr": [],
|
|
189
|
+
"command": command,
|
|
190
|
+
"start_time": time.time()
|
|
191
|
+
}
|
|
192
|
+
with background_tasks_lock:
|
|
193
|
+
background_tasks[task_id] = task_info
|
|
194
|
+
|
|
195
|
+
def stream_reader(pipe, dest_list):
|
|
196
|
+
for line in iter(pipe.readline, ''):
|
|
197
|
+
dest_list.append(line)
|
|
198
|
+
pipe.close()
|
|
199
|
+
|
|
200
|
+
threading.Thread(target=stream_reader, args=(proc.stdout, task_info["stdout"]), daemon=True).start()
|
|
201
|
+
threading.Thread(target=stream_reader, args=(proc.stderr, task_info["stderr"]), daemon=True).start()
|
|
202
|
+
|
|
203
|
+
return f"Task started in background with ID: {task_id}"
|
|
204
|
+
|
|
205
|
+
# Foreground interactive commands use the stateful terminal
|
|
206
|
+
output, stderr_output, exit_code, timeout_occurred, new_pwd = _terminal.run(command, timeout)
|
|
207
|
+
|
|
208
|
+
# Sync python's working directory with bash's working directory
|
|
209
|
+
if new_pwd and os.path.exists(new_pwd) and new_pwd != os.getcwd():
|
|
210
|
+
try:
|
|
211
|
+
os.chdir(new_pwd)
|
|
212
|
+
logger.info("Synced python cwd with bash: %s", new_pwd)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error("Failed to sync python cwd with bash: %s", e)
|
|
215
|
+
|
|
216
|
+
if timeout_occurred:
|
|
217
|
+
logger.warning("Command timed out after %ds: %s", timeout, command)
|
|
218
|
+
return f"Error: Command timed out after {timeout} seconds.\nPartial Output:\n{output}"
|
|
219
|
+
|
|
220
|
+
if stderr_output:
|
|
221
|
+
if output:
|
|
222
|
+
output += f"\nSTDERR:\n{stderr_output}"
|
|
223
|
+
else:
|
|
224
|
+
output = stderr_output
|
|
225
|
+
if exit_code != 0:
|
|
226
|
+
output += f"\n[Exit code: {exit_code}]"
|
|
227
|
+
|
|
228
|
+
return output.strip() if output.strip() else f"Command completed (exit code {exit_code})"
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.exception("Error executing command: %s", command)
|
|
232
|
+
return f"Error executing command: {e}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def check_background_task_impl(task_id: str) -> str:
|
|
236
|
+
"""Checks the status of a background task and returns its recent output."""
|
|
237
|
+
with background_tasks_lock:
|
|
238
|
+
if task_id not in background_tasks:
|
|
239
|
+
return f"Error: No such background task '{task_id}'."
|
|
240
|
+
task = background_tasks[task_id]
|
|
241
|
+
|
|
242
|
+
proc = task["process"]
|
|
243
|
+
|
|
244
|
+
out_lines = list(task["stdout"])
|
|
245
|
+
err_lines = list(task["stderr"])
|
|
246
|
+
|
|
247
|
+
stdout_str = "".join(out_lines[-100:]).strip()
|
|
248
|
+
stderr_str = "".join(err_lines[-100:]).strip()
|
|
249
|
+
|
|
250
|
+
status = "RUNNING" if proc.poll() is None else f"FINISHED (Exit code {proc.returncode})"
|
|
251
|
+
|
|
252
|
+
result = f"Task: {task_id}\nCommand: {task['command']}\nStatus: {status}\n\n"
|
|
253
|
+
if stdout_str:
|
|
254
|
+
result += f"--- STDOUT (last 100 lines) ---\n{stdout_str}\n\n"
|
|
255
|
+
if stderr_str:
|
|
256
|
+
result += f"--- STDERR (last 100 lines) ---\n{stderr_str}\n"
|
|
257
|
+
|
|
258
|
+
# Cleanup if done
|
|
259
|
+
if proc.poll() is not None:
|
|
260
|
+
with background_tasks_lock:
|
|
261
|
+
background_tasks.pop(task_id, None)
|
|
262
|
+
|
|
263
|
+
return result.strip()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def kill_background_task_impl(task_id: str) -> str:
|
|
267
|
+
"""Kills a running background task."""
|
|
268
|
+
with background_tasks_lock:
|
|
269
|
+
if task_id not in background_tasks:
|
|
270
|
+
return f"Error: No such background task '{task_id}'."
|
|
271
|
+
task = background_tasks.pop(task_id)
|
|
272
|
+
|
|
273
|
+
proc = task["process"]
|
|
274
|
+
|
|
275
|
+
if proc.poll() is None:
|
|
276
|
+
proc.kill()
|
|
277
|
+
return f"Task {task_id} killed."
|
|
278
|
+
else:
|
|
279
|
+
return f"Task {task_id} was already finished."
|
|
@@ -147,8 +147,8 @@ tools = [
|
|
|
147
147
|
{
|
|
148
148
|
"type": "function",
|
|
149
149
|
"function": {
|
|
150
|
-
"name": "
|
|
151
|
-
"description": "Executes a shell command.
|
|
150
|
+
"name": "run_terminal_command",
|
|
151
|
+
"description": "Executes a shell command in a stateful, persistent bash session. Environment variables and working directory changes (cd) persist across calls. Use the timeout parameter for long-running commands like builds or test suites.",
|
|
152
152
|
"parameters": {
|
|
153
153
|
"type": "object",
|
|
154
154
|
"properties": {
|
|
@@ -162,7 +162,7 @@ tools = [
|
|
|
162
162
|
},
|
|
163
163
|
"background": {
|
|
164
164
|
"type": "boolean",
|
|
165
|
-
"description": "If true, runs the command asynchronously in the
|
|
165
|
+
"description": "If true, runs the command asynchronously in a separate isolated background process (not the persistent shell). Returns a task ID immediately.",
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
"required": ["command"],
|
|
@@ -374,7 +374,7 @@ def execute_tool(tool_call, auto_approve: bool = False) -> str:
|
|
|
374
374
|
console.print(tool_label)
|
|
375
375
|
return multi_replace_file_content(filepath, chunks, auto_approve=auto_approve)
|
|
376
376
|
|
|
377
|
-
elif func_name == "
|
|
377
|
+
elif func_name == "run_terminal_command":
|
|
378
378
|
command = str(args.get("command", ""))
|
|
379
379
|
timeout = int(args.get("timeout", 120))
|
|
380
380
|
background = bool(args.get("background", False))
|
|
@@ -237,7 +237,7 @@ class BackupManager:
|
|
|
237
237
|
logger.debug("Cleanup backups failed: %s", e)
|
|
238
238
|
|
|
239
239
|
|
|
240
|
-
def truncate_output(text: str, max_chars: int =
|
|
240
|
+
def truncate_output(text: str, max_chars: int = 100000) -> str:
|
|
241
241
|
if len(text) <= max_chars:
|
|
242
242
|
return text
|
|
243
243
|
half = max_chars // 2
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aizen-ai-cli
|
|
3
|
+
Version: 2.4.2
|
|
4
|
+
Summary: Aizen AI Agent — A professional-grade AI coding assistant for your terminal.
|
|
5
|
+
Author: Irtaza Malik
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/irtaza302/aizen-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/irtaza302/aizen-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/irtaza302/aizen-agent/issues
|
|
10
|
+
Keywords: ai,cli,coding-assistant,terminal,openrouter,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: openai>=1.0
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0
|
|
27
|
+
Requires-Dist: rich>=13.0
|
|
28
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
29
|
+
Requires-Dist: questionary>=2.0.0
|
|
30
|
+
Requires-Dist: mcp>=1.0.0
|
|
31
|
+
Provides-Extra: tiktoken
|
|
32
|
+
Requires-Dist: tiktoken>=0.5; extra == "tiktoken"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-mock>=3.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
40
|
+
|
|
41
|
+
# Aizen AI Agent 🚀
|
|
42
|
+
|
|
43
|
+
[](https://github.com/irtaza302/aizen-agent/actions/workflows/ci.yml)
|
|
44
|
+
|
|
45
|
+
Aizen is a powerful, asynchronous AI assistant that integrates seamlessly into your terminal workflow. It reads your code, edits files safely, runs commands, and provides real‑time, richly formatted assistance—all while keeping costs transparent and sessions persistent.
|
|
46
|
+
|
|
47
|
+
## 🌟 Key Benefits
|
|
48
|
+
|
|
49
|
+
- **Effortless Integration** — Operates directly in your terminal, preserving shell state across commands.
|
|
50
|
+
- **Intelligent Editing** — Perform precise, color‑coded file edits with `edit_file`.
|
|
51
|
+
- **Background Execution** — Run long‑running tasks asynchronously and retrieve results later.
|
|
52
|
+
- **Cost‑Aware Usage** — Real‑time cost estimation for all major LLMs.
|
|
53
|
+
- **Persistent Sessions** — Save and restore conversations with checkpoints.
|
|
54
|
+
- **Rich Visual Feedback** — Stream responses with live previews and animated thought indicators.
|
|
55
|
+
- **Extensible Architecture** — Custom plugins and project‑specific rules tailor Aizen to your workflow.
|
|
56
|
+
- **Comprehensive Logging** — Rotating logs with optional verbose output for debugging.
|
|
57
|
+
|
|
58
|
+
## 🚀 Core Features
|
|
59
|
+
|
|
60
|
+
### Asynchronous Architecture
|
|
61
|
+
- Fully asynchronous operations using `asyncio` and `AsyncOpenAI` for concurrent processing, parallel tool runs, and streaming.
|
|
62
|
+
|
|
63
|
+
### Stateful Terminal Session
|
|
64
|
+
- Environment variables and directory changes persist across interactions.
|
|
65
|
+
|
|
66
|
+
### Rich Markdown Rendering
|
|
67
|
+
- Full Markdown support with headers, code blocks, lists, and styling via Rich.
|
|
68
|
+
|
|
69
|
+
### Surgical File Editing
|
|
70
|
+
- Precise search‑and‑replace with color‑coded diff previews (`edit_file`).
|
|
71
|
+
|
|
72
|
+
### Vision Support
|
|
73
|
+
- Native image handling and encoding for Vision APIs (e.g., GPT‑4o, Claude 3.5 Sonnet).
|
|
74
|
+
|
|
75
|
+
### Real‑Time Command Streaming
|
|
76
|
+
- Background command execution with async streaming of stdout/stderr; use `run_command --background`.
|
|
77
|
+
|
|
78
|
+
## 🎛️ Workflow Tools
|
|
79
|
+
|
|
80
|
+
- **Background Tasks** — Run non‑blocking commands; monitor with `check_background_task`; cancel with `kill_background_task`.
|
|
81
|
+
- **Session Persistence** — Powered by SQLite (`~/.aizen_sessions/aizen.db`), auto‑migrating older JSON sessions.
|
|
82
|
+
- **Project‑Specific Rules** — Auto‑load `.aizen_rules` or `.cursorrules` for repo‑specific behavior.
|
|
83
|
+
- **Smart Autocomplete** — TAB‑completion with `.gitignore` awareness and directory traversal.
|
|
84
|
+
|
|
85
|
+
## 💰 Cost Tracking
|
|
86
|
+
|
|
87
|
+
- Real‑time token counting for inputs and outputs.
|
|
88
|
+
- Current cost estimate shown in the CLI status bar.
|
|
89
|
+
- Supports Anthropic (Claude 3.5/3.7 Sonnet, Opus, Haiku), Google (Gemini 2.5 Pro/Flash), and OpenAI (GPT‑4o, o1, o3‑mini).
|
|
90
|
+
|
|
91
|
+
## 📌 Session Management
|
|
92
|
+
|
|
93
|
+
- `/checkpoint [name]` — Save conversation snapshots.
|
|
94
|
+
- `/restore [name]` — Restore a previous checkpoint.
|
|
95
|
+
|
|
96
|
+
## 📁 Structured Logging
|
|
97
|
+
|
|
98
|
+
- Logs stored at `~/.aizen_logs/aizen.log` (rotated, 5 MB caps, 3 files).
|
|
99
|
+
- Verbose flag mirrors output to console.
|
|
100
|
+
|
|
101
|
+
## 📦 Publishing & Development
|
|
102
|
+
|
|
103
|
+
- Use `publish.sh` to build and publish to PyPI, NPM, and PyInstaller binaries.
|
|
@@ -10,7 +10,7 @@ def parse_requirements(filename):
|
|
|
10
10
|
|
|
11
11
|
setup(
|
|
12
12
|
name="aizen-ai-cli",
|
|
13
|
-
version="2.4.
|
|
13
|
+
version="2.4.2",
|
|
14
14
|
description="Aizen AI Agent — A professional-grade AI coding assistant for your terminal.",
|
|
15
15
|
packages=["aizen"],
|
|
16
16
|
install_requires=parse_requirements("requirements.txt"),
|