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.
Files changed (50) hide show
  1. aizen_ai_cli-2.4.2/PKG-INFO +103 -0
  2. aizen_ai_cli-2.4.2/README.md +63 -0
  3. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/agent.py +7 -2
  4. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/commands.py +4 -0
  5. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/config.py +1 -1
  6. aizen_ai_cli-2.4.2/aizen/tools/commands.py +279 -0
  7. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/dispatcher.py +4 -4
  8. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/utils.py +1 -1
  9. aizen_ai_cli-2.4.2/aizen_ai_cli.egg-info/PKG-INFO +103 -0
  10. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/pyproject.toml +1 -1
  11. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/setup.py +1 -1
  12. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_commands.py +1 -8
  13. aizen_ai_cli-2.4.1/PKG-INFO +0 -276
  14. aizen_ai_cli-2.4.1/README.md +0 -236
  15. aizen_ai_cli-2.4.1/aizen/tools/commands.py +0 -222
  16. aizen_ai_cli-2.4.1/aizen_ai_cli.egg-info/PKG-INFO +0 -276
  17. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/MANIFEST.in +0 -0
  18. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/__init__.py +0 -0
  19. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/context.py +0 -0
  20. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/exceptions.py +0 -0
  21. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/logging_config.py +0 -0
  22. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/main.py +0 -0
  23. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/mcp.py +0 -0
  24. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/plugins.py +0 -0
  25. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/retry.py +0 -0
  26. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/session.py +0 -0
  27. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/__init__.py +0 -0
  28. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/file_ops.py +0 -0
  29. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/helpers.py +0 -0
  30. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen/tools/search.py +0 -0
  31. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/SOURCES.txt +0 -0
  32. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/dependency_links.txt +0 -0
  33. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/entry_points.txt +0 -0
  34. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/requires.txt +0 -0
  35. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/aizen_ai_cli.egg-info/top_level.txt +0 -0
  36. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/requirements.txt +0 -0
  37. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/setup.cfg +0 -0
  38. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_agent.py +0 -0
  39. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_config.py +0 -0
  40. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_context.py +0 -0
  41. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_dispatcher.py +0 -0
  42. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_file_ops.py +0 -0
  43. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_helpers.py +0 -0
  44. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_main.py +0 -0
  45. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_mcp.py +0 -0
  46. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_plugins.py +0 -0
  47. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_retry.py +0 -0
  48. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_search.py +0 -0
  49. {aizen_ai_cli-2.4.1 → aizen_ai_cli-2.4.2}/tests/test_session.py +0 -0
  50. {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
+ [![CI](https://github.com/irtaza302/aizen-agent/actions/workflows/ci.yml/badge.svg)](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
+ [![CI](https://github.com/irtaza302/aizen-agent/actions/workflows/ci.yml/badge.svg)](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
- display_content = f"**◆ AIZEN:** {full_content}"
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 as e:
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.1"
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": "run_command",
151
- "description": "Executes a shell command. Safe read-only commands run automatically; destructive commands require user confirmation. Use the timeout parameter for long-running commands like builds or test suites.",
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 background. Returns a task ID immediately.",
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 == "run_command":
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 = 4000) -> str:
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
+ [![CI](https://github.com/irtaza302/aizen-agent/actions/workflows/ci.yml/badge.svg)](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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aizen-ai-cli"
7
- version = "2.4.1"
7
+ version = "2.4.2"
8
8
  description = "Aizen AI Agent — A professional-grade AI coding assistant for your terminal."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -10,7 +10,7 @@ def parse_requirements(filename):
10
10
 
11
11
  setup(
12
12
  name="aizen-ai-cli",
13
- version="2.4.1",
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"),