aizen-ai-cli 2.2.4__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {aizen_ai_cli-2.2.4/aizen_ai_cli.egg-info → aizen_ai_cli-2.4.0}/PKG-INFO +14 -5
  2. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/README.md +12 -4
  3. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/commands.py +133 -105
  4. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/config.py +67 -22
  5. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/context.py +12 -10
  6. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/main.py +257 -120
  7. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/session.py +3 -2
  8. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/tools.py +493 -101
  9. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/utils.py +20 -4
  10. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0/aizen_ai_cli.egg-info}/PKG-INFO +14 -5
  11. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen_ai_cli.egg-info/requires.txt +1 -0
  12. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/pyproject.toml +2 -1
  13. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/setup.py +4 -2
  14. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_commands.py +7 -6
  15. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_config.py +2 -5
  16. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_context.py +4 -2
  17. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_main.py +9 -8
  18. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_mcp.py +17 -15
  19. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_session.py +2 -5
  20. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_tools.py +64 -45
  21. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/tests/test_utils.py +6 -11
  22. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/MANIFEST.in +0 -0
  23. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/__init__.py +0 -0
  24. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/exceptions.py +0 -0
  25. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/logging_config.py +0 -0
  26. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/mcp.py +0 -0
  27. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/plugins.py +0 -0
  28. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen/retry.py +0 -0
  29. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen_ai_cli.egg-info/SOURCES.txt +0 -0
  30. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen_ai_cli.egg-info/dependency_links.txt +0 -0
  31. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen_ai_cli.egg-info/entry_points.txt +0 -0
  32. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/aizen_ai_cli.egg-info/top_level.txt +0 -0
  33. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/requirements.txt +0 -0
  34. {aizen_ai_cli-2.2.4 → aizen_ai_cli-2.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aizen-ai-cli
3
- Version: 2.2.4
3
+ Version: 2.4.0
4
4
  Summary: Aizen AI Agent — A professional-grade AI coding assistant for your terminal.
5
5
  Author: Irtaza Malik
6
6
  License: MIT
@@ -26,6 +26,7 @@ Requires-Dist: openai>=1.0
26
26
  Requires-Dist: python-dotenv>=1.0
27
27
  Requires-Dist: rich>=13.0
28
28
  Requires-Dist: prompt_toolkit>=3.0
29
+ Requires-Dist: questionary>=2.0.0
29
30
  Requires-Dist: mcp>=1.0.0
30
31
  Provides-Extra: tiktoken
31
32
  Requires-Dist: tiktoken>=0.5; extra == "tiktoken"
@@ -41,7 +42,7 @@ Requires-Dist: mypy>=1.0; extra == "dev"
41
42
 
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)
43
44
 
44
- A professional-grade AI coding assistant that runs directly in your terminal. Aizen reads your code, writes files with surgical precision, runs commands safely, and helps you build faster — all from a beautifully designed CLI.
45
+ A helpful AI coding assistant you can use right in your terminal. Aizen reads your code, edits files safely, runs commands, and helps you get things done faster — all with a friendly command‑line interface.
45
46
 
46
47
  ## ✨ Features
47
48
 
@@ -53,21 +54,27 @@ A professional-grade AI coding assistant that runs directly in your terminal. Ai
53
54
  - **SQLite Session Persistence** — Session storage is powered by a SQLite database (`~/.aizen_sessions/aizen.db`), auto-migrating older JSON sessions.
54
55
  - **Project-Specific Rules** — Customizes agent behavior per repository by auto-loading `.aizen_rules` or `.cursorrules` from the current working directory.
55
56
  - **Smart Autocomplete** — `@`-mention files with Tab completion that respects `.gitignore` and supports directory traversal.
57
+ - **Vision Support** — Attach images natively (e.g., `@mockup.png`) and Aizen will automatically encode them for Vision APIs (GPT-4o, Claude 3.5 Sonnet).
58
+ - **Real-time Command Streaming** — Long-running shell commands stream their output live to the terminal instead of freezing with a spinner.
59
+ - **Smart Context Pruning** — Automatically drops old, large file attachments first when hitting the context limit before resorting to LLM summarization.
56
60
 
57
61
  ### Tools
58
- Aizen has 9 built-in tools the AI can use:
62
+ Aizen has 10 built-in tools the AI can use:
59
63
 
60
64
  | Tool | Description |
61
65
  |------|-------------|
62
66
  | `read_file` | Read file contents before making changes |
63
- | `write_file` | Create new files (with preview) |
64
- | `edit_file` | Surgical search-and-replace on existing files (with diff preview) |
67
+ | `write_file` | Create new files or overwrite entirely (with preview) |
68
+ | `replace_file_content` | Surgical search-and-replace on existing files (with line-bounds and diff preview) |
69
+ | `multi_replace_file_content` | Perform multiple, non-adjacent surgical edits sequentially in a single pass |
65
70
  | `run_command` | Execute shell commands (supports background execution; safe commands auto-run, dangerous ones require approval) |
66
71
  | `check_background_task` | Check the status and read recent output of a command running in the background |
67
72
  | `kill_background_task` | Kill a running background task |
68
73
  | `list_directory` | List files/folders with sizes, respecting `.gitignore` |
69
74
  | `grep_search` | Search for text or regex patterns across the codebase |
70
75
  | `find_files` | Find files by glob pattern (e.g., `*.py`, `Dockerfile`) |
76
+ | `get_file_outline` | Extract AST outline of a Python file (classes, methods, docstrings) without blowing up the context window |
77
+ | `web_search` | Search the web for current information, docs, or API references |
71
78
 
72
79
  ### Commands
73
80
 
@@ -91,9 +98,11 @@ Aizen has 9 built-in tools the AI can use:
91
98
  | `/export [file]` | Export conversation to a Markdown file |
92
99
  | `/config` | View current configuration |
93
100
  | `/mcp` | View configured MCP servers and their connection status |
101
+ | `/auto [task]` | Enter a fully autonomous agentic loop to execute a complex task step-by-step |
94
102
 
95
103
  ### Safety & UX
96
104
  - **Command Safety** — Read-only commands (`ls`, `cat`, `git status`, etc.) auto-execute. Destructive commands (`rm`, `sudo`, etc.) always require confirmation.
105
+ - **Autonomous Limits** — The `/auto` mode enforces a strict 25-step execution limit to prevent infinite loops and runaway costs.
97
106
  - **`--yolo` Mode** — Auto-approve all operations for power users.
98
107
  - **Background Tasks** — Run builds, tests, or other long-running tasks asynchronously while continuing to interact with Aizen.
99
108
  - **File Backups** — Every file modification creates a backup. Use `/undo` to restore.
@@ -2,7 +2,7 @@
2
2
 
3
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
4
 
5
- A professional-grade AI coding assistant that runs directly in your terminal. Aizen reads your code, writes files with surgical precision, runs commands safely, and helps you build faster — all from a beautifully designed CLI.
5
+ A helpful AI coding assistant you can use right in your terminal. Aizen reads your code, edits files safely, runs commands, and helps you get things done faster — all with a friendly command‑line interface.
6
6
 
7
7
  ## ✨ Features
8
8
 
@@ -14,21 +14,27 @@ A professional-grade AI coding assistant that runs directly in your terminal. Ai
14
14
  - **SQLite Session Persistence** — Session storage is powered by a SQLite database (`~/.aizen_sessions/aizen.db`), auto-migrating older JSON sessions.
15
15
  - **Project-Specific Rules** — Customizes agent behavior per repository by auto-loading `.aizen_rules` or `.cursorrules` from the current working directory.
16
16
  - **Smart Autocomplete** — `@`-mention files with Tab completion that respects `.gitignore` and supports directory traversal.
17
+ - **Vision Support** — Attach images natively (e.g., `@mockup.png`) and Aizen will automatically encode them for Vision APIs (GPT-4o, Claude 3.5 Sonnet).
18
+ - **Real-time Command Streaming** — Long-running shell commands stream their output live to the terminal instead of freezing with a spinner.
19
+ - **Smart Context Pruning** — Automatically drops old, large file attachments first when hitting the context limit before resorting to LLM summarization.
17
20
 
18
21
  ### Tools
19
- Aizen has 9 built-in tools the AI can use:
22
+ Aizen has 10 built-in tools the AI can use:
20
23
 
21
24
  | Tool | Description |
22
25
  |------|-------------|
23
26
  | `read_file` | Read file contents before making changes |
24
- | `write_file` | Create new files (with preview) |
25
- | `edit_file` | Surgical search-and-replace on existing files (with diff preview) |
27
+ | `write_file` | Create new files or overwrite entirely (with preview) |
28
+ | `replace_file_content` | Surgical search-and-replace on existing files (with line-bounds and diff preview) |
29
+ | `multi_replace_file_content` | Perform multiple, non-adjacent surgical edits sequentially in a single pass |
26
30
  | `run_command` | Execute shell commands (supports background execution; safe commands auto-run, dangerous ones require approval) |
27
31
  | `check_background_task` | Check the status and read recent output of a command running in the background |
28
32
  | `kill_background_task` | Kill a running background task |
29
33
  | `list_directory` | List files/folders with sizes, respecting `.gitignore` |
30
34
  | `grep_search` | Search for text or regex patterns across the codebase |
31
35
  | `find_files` | Find files by glob pattern (e.g., `*.py`, `Dockerfile`) |
36
+ | `get_file_outline` | Extract AST outline of a Python file (classes, methods, docstrings) without blowing up the context window |
37
+ | `web_search` | Search the web for current information, docs, or API references |
32
38
 
33
39
  ### Commands
34
40
 
@@ -52,9 +58,11 @@ Aizen has 9 built-in tools the AI can use:
52
58
  | `/export [file]` | Export conversation to a Markdown file |
53
59
  | `/config` | View current configuration |
54
60
  | `/mcp` | View configured MCP servers and their connection status |
61
+ | `/auto [task]` | Enter a fully autonomous agentic loop to execute a complex task step-by-step |
55
62
 
56
63
  ### Safety & UX
57
64
  - **Command Safety** — Read-only commands (`ls`, `cat`, `git status`, etc.) auto-execute. Destructive commands (`rm`, `sudo`, etc.) always require confirmation.
65
+ - **Autonomous Limits** — The `/auto` mode enforces a strict 25-step execution limit to prevent infinite loops and runaway costs.
58
66
  - **`--yolo` Mode** — Auto-approve all operations for power users.
59
67
  - **Background Tasks** — Run builds, tests, or other long-running tasks asynchronously while continuing to interact with Aizen.
60
68
  - **File Backups** — Every file modification creates a backup. Use `/undo` to restore.
@@ -13,6 +13,7 @@ from .config import (
13
13
  BACKUPS_DIR,
14
14
  CONFIG_PATH,
15
15
  SESSIONS_DIR,
16
+ Theme,
16
17
  console,
17
18
  get_active_model,
18
19
  get_cached_models,
@@ -43,6 +44,7 @@ SLASH_COMMANDS = [
43
44
  ("/mcp", "View configured MCP servers and their status"),
44
45
  ("/commit", "Auto-generate and commit changes"),
45
46
  ("/diff", "Show all uncommitted changes"),
47
+ ("/auto", "Enter autonomous agentic mode for a complex task"),
46
48
  ]
47
49
 
48
50
  # In-memory checkpoint storage for conversation branching
@@ -149,7 +151,7 @@ async def handle_slash_command(
149
151
  if cmd == "/clear":
150
152
  if len(messages) > 1:
151
153
  messages[:] = [messages[0]]
152
- console.print("[green]✓ Conversation cleared.[/green]\n")
154
+ console.print(f" [{Theme.SUCCESS}]✓ Conversation cleared.[/{Theme.SUCCESS}]\n")
153
155
 
154
156
  elif cmd == "/drop":
155
157
  dropped_count = 0
@@ -184,31 +186,36 @@ async def handle_slash_command(
184
186
  msg["content"] = new_content
185
187
  dropped_count += 1
186
188
  if dropped_count > 0:
187
- console.print(f"[green]✓ Dropped attached contexts from {dropped_count} past messages.[/green]\n")
189
+ console.print(f" [{Theme.SUCCESS}]✓ Dropped attached contexts from {dropped_count} past messages.[/{Theme.SUCCESS}]\n")
188
190
  else:
189
- console.print("[yellow]No attached contexts found to drop.[/yellow]\n")
191
+ console.print(f" [{Theme.WARNING}]No attached contexts found to drop.[/{Theme.WARNING}]\n")
190
192
 
191
193
  elif cmd == "/model":
192
194
  if arg:
193
195
  if arg.startswith("search ") or arg == "list" or arg == "search":
194
196
  models = get_cached_models()
195
197
  if not models:
196
- console.print("[yellow]Model list is still fetching or unavailable. Try again in a moment.[/yellow]\n")
198
+ console.print(f" [{Theme.WARNING}]Model list is still fetching or unavailable. Try again in a moment.[/{Theme.WARNING}]\n")
197
199
  return False
198
200
 
199
201
  search_query = arg[7:].lower().strip() if arg.startswith("search ") else ""
200
202
 
201
- table = Table(title=f"🧠 OpenRouter Models{' (Search: ' + search_query + ')' if search_query else ''}")
202
- table.add_column("Model ID", style="cyan")
203
- table.add_column("Name", style="white")
204
- table.add_column("Context", style="dim")
205
- table.add_column("Pricing", style="green")
203
+ table = Table(
204
+ title=f"🧠 OpenRouter Models{' (Search: ' + search_query + ')' if search_query else ''}",
205
+ border_style=Theme.BORDER,
206
+ header_style=f"bold {Theme.PRIMARY}",
207
+ )
208
+ table.add_column("Model ID", style=Theme.ACCENT)
209
+ table.add_column("Name", style=Theme.TEXT)
210
+ table.add_column("Context", style=Theme.MUTED)
211
+ table.add_column("Pricing", style=Theme.SUCCESS)
206
212
 
207
213
  count = 0
208
214
  for m in models:
209
215
  if not search_query or search_query in m["id"].lower() or search_query in m["name"].lower():
210
- price_prompt = m.get("pricing", {}).get("prompt", "?")
211
- price_comp = m.get("pricing", {}).get("completion", "?")
216
+ pricing = m.get("pricing") or {}
217
+ price_prompt = pricing.get("prompt", "?")
218
+ price_comp = pricing.get("completion", "?")
212
219
  pricing_str = f"P: {price_prompt} C: {price_comp}"
213
220
  table.add_row(m["id"], m["name"], str(m.get("context_length")), pricing_str)
214
221
  count += 1
@@ -217,7 +224,7 @@ async def handle_slash_command(
217
224
 
218
225
  console.print(table)
219
226
  if count >= 30:
220
- console.print("[dim]... and more (showing top 30). Use `/model search <query>` to narrow down.[/dim]\n")
227
+ console.print(f" [{Theme.MUTED}]... and more (showing top 30). Use `/model search <query>` to narrow down.[/{Theme.MUTED}]\n")
221
228
  else:
222
229
  console.print()
223
230
  else:
@@ -225,47 +232,68 @@ async def handle_slash_command(
225
232
  found = any(m["id"] == arg for m in models)
226
233
 
227
234
  if models and not found:
228
- console.print(f"[yellow]⚠️ Warning: Model '{arg}' not found in OpenRouter API list.[/yellow]")
235
+ console.print(f" [{Theme.WARNING}]⚠️ Warning: Model '{arg}' not found in OpenRouter API list.[/{Theme.WARNING}]")
229
236
 
230
237
  set_active_model(arg, save=True)
231
- console.print(f"[green]✓ Model switched to:[/green] [bold cyan]{arg}[/bold cyan]\n")
238
+ console.print(f" [{Theme.SUCCESS}]✓ Model switched to:[/{Theme.SUCCESS}] [bold {Theme.ACCENT}]{arg}[/bold {Theme.ACCENT}]\n")
232
239
  else:
233
- console.print(f"[bold]Current model:[/bold] [cyan]{current_model}[/cyan]")
234
- console.print("[dim]Usage: /model <model_name>[/dim]")
235
- console.print("[dim] /model search <query> (or `/model list`)[/dim]\n")
240
+ console.print(f" [bold {Theme.TEXT}]Current model:[/bold {Theme.TEXT}] [{Theme.ACCENT}]{current_model}[/{Theme.ACCENT}]")
241
+ console.print(f" [{Theme.MUTED}]Usage: /model <model_name>[/{Theme.MUTED}]")
242
+ console.print(f" [{Theme.MUTED}] /model search <query> (or `/model list`)[/{Theme.MUTED}]\n")
236
243
 
237
244
  elif cmd == "/help":
238
245
  help_table = Table(
239
246
  title="⚡ Aizen Commands",
240
- border_style="magenta",
247
+ border_style=Theme.BORDER,
241
248
  show_header=True,
242
- header_style="bold magenta",
249
+ header_style=f"bold {Theme.PRIMARY}",
250
+ title_style=f"bold {Theme.ACCENT}",
243
251
  )
244
- help_table.add_column("Command", style="cyan bold", min_width=22)
245
- help_table.add_column("Description", style="white")
246
- help_table.add_row("/help", "Show this help message")
247
- help_table.add_row("/model [name]", "View or switch the active model")
248
- help_table.add_row("/clear", "Clear conversation history")
249
- help_table.add_row("/drop", "Drop attached files/URLs from history")
250
- help_table.add_row("/save [name]", "Save current conversation")
251
- help_table.add_row("/load [name]", "Load a saved conversation")
252
- help_table.add_row("/usage", "Show token usage statistics")
253
- help_table.add_row("/compact", "Summarize conversation to save tokens")
254
- help_table.add_row("/undo", "Undo the last file modification")
255
- help_table.add_row("/retry", "Retry the last user message")
256
- help_table.add_row("/copy", "Copy last AI response to clipboard")
257
- help_table.add_row("/export [file]", "Export conversation to Markdown")
258
- help_table.add_row("/checkpoint [name]", "Save a conversation snapshot")
259
- help_table.add_row("/restore [name]", "Restore a saved checkpoint")
260
- help_table.add_row("/config", "View current configuration")
261
- help_table.add_row("/mcp", "View configured MCP servers and their status")
262
- help_table.add_row("/commit", "Auto-generate and commit changes")
263
- help_table.add_row("/diff", "Show all uncommitted changes")
264
- help_table.add_row("", "")
265
- help_table.add_row("@filename / @url", "Attach file context or web URL")
266
- help_table.add_row("exit / quit", "Exit Aizen")
267
- help_table.add_row("", "")
268
- help_table.add_row("[dim]Tip[/dim]", "[dim]End a line with \\\\ for multi-line input[/dim]")
252
+ help_table.add_column("Command", style=f"{Theme.ACCENT} bold", min_width=24)
253
+ help_table.add_column("Description", style=Theme.TEXT)
254
+
255
+ # ── Navigation & Info ──
256
+ help_table.add_row(f"[bold {Theme.MUTED}]── Navigation ──[/bold {Theme.MUTED}]", "")
257
+ help_table.add_row(" 📖 /help", "Show this help message")
258
+ help_table.add_row(" ⚙️ /config", "View current configuration")
259
+ help_table.add_row(" 📊 /usage", "Show token usage statistics")
260
+ help_table.add_row(" 🔌 /mcp", "View MCP servers and status")
261
+
262
+ # ── Model ──
263
+ help_table.add_row(f"[bold {Theme.MUTED}]── Model ──[/bold {Theme.MUTED}]", "")
264
+ help_table.add_row(" 🧠 /model [name]", "View or switch the active model")
265
+
266
+ # ── Session ──
267
+ help_table.add_row(f"[bold {Theme.MUTED}]── Session ──[/bold {Theme.MUTED}]", "")
268
+ help_table.add_row(" 💾 /save [name]", "Save current conversation")
269
+ help_table.add_row(" 📂 /load [name]", "Load a saved conversation")
270
+ help_table.add_row(" 📌 /checkpoint [n]", "Save a conversation snapshot")
271
+ help_table.add_row(" 🔄 /restore [name]", "Restore a saved checkpoint")
272
+ help_table.add_row(" 📋 /export [file]", "Export conversation to Markdown")
273
+
274
+ # ── Editing ──
275
+ help_table.add_row(f"[bold {Theme.MUTED}]── Editing ──[/bold {Theme.MUTED}]", "")
276
+ help_table.add_row(" 🗑️ /clear", "Clear conversation history")
277
+ help_table.add_row(" 📎 /drop", "Drop attached files/URLs from history")
278
+ help_table.add_row(" 🧹 /compact", "Summarize conversation to save tokens")
279
+ help_table.add_row(" ↩️ /undo", "Undo the last file modification")
280
+ help_table.add_row(" 🔁 /retry", "Retry the last user message")
281
+ help_table.add_row(" 📝 /copy", "Copy last AI response to clipboard")
282
+
283
+ # ── Git ──
284
+ help_table.add_row(f"[bold {Theme.MUTED}]── Git ──[/bold {Theme.MUTED}]", "")
285
+ help_table.add_row(" 🔀 /commit", "Auto-generate and commit changes")
286
+ help_table.add_row(" 📊 /diff", "Show all uncommitted changes")
287
+
288
+ # ── Agent ──
289
+ help_table.add_row(f"[bold {Theme.MUTED}]── Agent ──[/bold {Theme.MUTED}]", "")
290
+ help_table.add_row(" 🤖 /auto [task]", "Enter autonomous mode for a complex task (max iterations apply)")
291
+
292
+ # ── Shortcuts ──
293
+ help_table.add_row(f"[bold {Theme.MUTED}]── Shortcuts ──[/bold {Theme.MUTED}]", "")
294
+ help_table.add_row(f" [{Theme.PINK}]@file / @url[/{Theme.PINK}]", "Attach file context or web URL")
295
+ help_table.add_row(f" [{Theme.PINK}]exit / quit[/{Theme.PINK}]", "Exit Aizen")
296
+ help_table.add_row(f" [{Theme.MUTED}]Tip[/{Theme.MUTED}]", f"[{Theme.MUTED}]End a line with \\\\ for multi-line input[/{Theme.MUTED}]")
269
297
  console.print(help_table)
270
298
  console.print()
271
299
 
@@ -276,51 +304,51 @@ async def handle_slash_command(
276
304
  elif cmd == "/save":
277
305
  try:
278
306
  path = save_session(messages, arg if arg else None, token_tracker)
279
- console.print(f"[green]✓ Session saved to {path}[/green]\n")
307
+ console.print(f" [{Theme.SUCCESS}]✓ Session saved to {path}[/{Theme.SUCCESS}]\n")
280
308
  except Exception as e:
281
- console.print(f"[red]Error saving session: {e}[/red]\n")
309
+ console.print(f" [{Theme.ERROR}]Error saving session: {e}[/{Theme.ERROR}]\n")
282
310
 
283
311
  elif cmd == "/load":
284
312
  if not arg:
285
313
  sessions = list_sessions()
286
314
  if not sessions:
287
- console.print("[yellow]No saved sessions found.[/yellow]\n")
315
+ console.print(f" [{Theme.WARNING}]No saved sessions found.[/{Theme.WARNING}]\n")
288
316
  else:
289
317
  table = Table(
290
318
  title="📂 Saved Sessions",
291
- border_style="magenta",
292
- header_style="bold magenta",
319
+ border_style=Theme.BORDER,
320
+ header_style=f"bold {Theme.PRIMARY}",
293
321
  )
294
- table.add_column("Name", style="cyan")
295
- table.add_column("Saved At", style="dim")
322
+ table.add_column("Name", style=Theme.ACCENT)
323
+ table.add_column("Saved At", style=Theme.MUTED)
296
324
  table.add_column("Messages", style="white", justify="right")
297
325
  for s in sessions[:10]:
298
326
  table.add_row(s["name"], s["saved_at"][:19], str(s["messages"]))
299
327
  console.print(table)
300
- console.print("[dim]Usage: /load <session_name>[/dim]\n")
328
+ console.print(f" [{Theme.MUTED}]Usage: /load <session_name>[/{Theme.MUTED}]\n")
301
329
  else:
302
330
  loaded = load_session(arg)
303
331
  if loaded:
304
332
  messages[:] = loaded
305
333
  console.print(
306
- f"[green]✓ Loaded session '{arg}' ({len(loaded)} messages)[/green]\n"
334
+ f" [{Theme.SUCCESS}]✓ Loaded session '{arg}' ({len(loaded)} messages)[/{Theme.SUCCESS}]\n"
307
335
  )
308
336
  else:
309
- console.print(f"[red]Session '{arg}' not found.[/red]\n")
337
+ console.print(f" [{Theme.ERROR}]Session '{arg}' not found.[/{Theme.ERROR}]\n")
310
338
 
311
339
  elif cmd == "/undo":
312
340
  result = backup_manager.undo()
313
- console.print(f"[green]{result}[/green]\n")
341
+ console.print(f" [{Theme.SUCCESS}]{result}[/{Theme.SUCCESS}]\n")
314
342
 
315
343
  elif cmd == "/retry":
316
344
  # Remove last assistant + tool messages, then re-process the last user message
317
345
  while messages and messages[-1]["role"] in ("assistant", "tool"):
318
346
  messages.pop()
319
347
  if messages and messages[-1]["role"] == "user":
320
- console.print("[green]✓ Retrying last message...[/green]\n")
348
+ console.print(f" [{Theme.SUCCESS}]✓ Retrying last message...[/{Theme.SUCCESS}]\n")
321
349
  return True # Signal to re-process
322
350
  else:
323
- console.print("[yellow]Nothing to retry.[/yellow]\n")
351
+ console.print(f" [{Theme.WARNING}]Nothing to retry.[/{Theme.WARNING}]\n")
324
352
 
325
353
  elif cmd == "/copy":
326
354
  last_response = None
@@ -349,13 +377,13 @@ async def handle_slash_command(
349
377
  subprocess.run(
350
378
  ["clip"], input=last_response, text=True, check=True
351
379
  )
352
- console.print("[green]✓ Copied to clipboard.[/green]\n")
380
+ console.print(f" [{Theme.SUCCESS}]✓ Copied to clipboard.[/{Theme.SUCCESS}]\n")
353
381
  except Exception:
354
382
  console.print(
355
- "[yellow]⚠️ Could not copy to clipboard.[/yellow]\n"
383
+ f" [{Theme.WARNING}]⚠️ Could not copy to clipboard.[/{Theme.WARNING}]\n"
356
384
  )
357
385
  else:
358
- console.print("[yellow]No response to copy.[/yellow]\n")
386
+ console.print(f" [{Theme.WARNING}]No response to copy.[/{Theme.WARNING}]\n")
359
387
 
360
388
  elif cmd == "/export":
361
389
  filename = (
@@ -377,13 +405,13 @@ async def handle_slash_command(
377
405
  f.write(f"## 👤 You\n\n{msg['content']}\n\n")
378
406
  elif msg["role"] == "assistant" and msg.get("content"):
379
407
  f.write(f"## ✦ Aizen\n\n{msg['content']}\n\n")
380
- console.print(f"[green]✓ Exported to {filename}[/green]\n")
408
+ console.print(f" [{Theme.SUCCESS}]✓ Exported to {filename}[/{Theme.SUCCESS}]\n")
381
409
  except Exception as e:
382
- console.print(f"[red]Error exporting: {e}[/red]\n")
410
+ console.print(f" [{Theme.ERROR}]Error exporting: {e}[/{Theme.ERROR}]\n")
383
411
 
384
412
  elif cmd == "/compact":
385
413
  if len(messages) <= 4:
386
- console.print("[yellow]Conversation is already compact.[/yellow]\n")
414
+ console.print(f" [{Theme.WARNING}]Conversation is already compact.[/{Theme.WARNING}]\n")
387
415
  else:
388
416
  system_msg = messages[0]
389
417
  recent = messages[-4:]
@@ -391,7 +419,7 @@ async def handle_slash_command(
391
419
 
392
420
  if middle:
393
421
  # Attempt LLM-based summarization for much better context retention
394
- console.print("[dim]Summarizing conversation with AI...[/dim]")
422
+ console.print(f" [{Theme.MUTED}]Summarizing conversation with AI...[/{Theme.MUTED}]")
395
423
  try:
396
424
  from openai import AsyncOpenAI as _AsyncOpenAI
397
425
 
@@ -449,20 +477,20 @@ async def handle_slash_command(
449
477
  },
450
478
  ] + recent
451
479
  console.print(
452
- f"[green]✓ Compacted {len(middle)} messages into an AI-generated summary.[/green]\n"
480
+ f" [{Theme.SUCCESS}]✓ Compacted {len(middle)} messages into an AI-generated summary.[/{Theme.SUCCESS}]\n"
453
481
  )
454
482
  else:
455
- console.print("[yellow]Not enough messages to compact.[/yellow]\n")
483
+ console.print(f" [{Theme.WARNING}]Not enough messages to compact.[/{Theme.WARNING}]\n")
456
484
 
457
485
  elif cmd == "/config":
458
486
  config = load_config()
459
487
  table = Table(
460
488
  title="⚙️ Configuration",
461
- border_style="magenta",
462
- header_style="bold magenta",
489
+ border_style=Theme.BORDER,
490
+ header_style=f"bold {Theme.PRIMARY}",
463
491
  )
464
- table.add_column("Key", style="cyan")
465
- table.add_column("Value", style="white")
492
+ table.add_column("Key", style=Theme.ACCENT)
493
+ table.add_column("Value", style=Theme.TEXT)
466
494
  table.add_row("Model", current_model)
467
495
  table.add_row(
468
496
  "API Base URL",
@@ -478,22 +506,22 @@ async def handle_slash_command(
478
506
 
479
507
  elif cmd == "/mcp":
480
508
  if not mcp_manager:
481
- console.print("[yellow]MCP Manager is not available.[/yellow]\n")
509
+ console.print(f" [{Theme.WARNING}]MCP Manager is not available.[/{Theme.WARNING}]\n")
482
510
  return False
483
511
 
484
512
  if not mcp_manager.config:
485
- console.print("[yellow]No MCP servers configured in ~/.aizen_config.json[/yellow]\n")
486
- console.print("[dim]Add an 'mcp_servers' block to your config to enable MCP plugins.[/dim]\n")
513
+ console.print(f" [{Theme.WARNING}]No MCP servers configured in ~/.aizen_config.json[/{Theme.WARNING}]\n")
514
+ console.print(f" [{Theme.MUTED}]Add an 'mcp_servers' block to your config to enable MCP plugins.[/{Theme.MUTED}]\n")
487
515
  return False
488
516
 
489
517
  table = Table(
490
518
  title="🔌 Configured MCP Servers",
491
- border_style="magenta",
492
- header_style="bold magenta",
519
+ border_style=Theme.BORDER,
520
+ header_style=f"bold {Theme.PRIMARY}",
493
521
  )
494
- table.add_column("Server Name", style="cyan bold")
495
- table.add_column("Status", style="white")
496
- table.add_column("Tools Available", style="dim")
522
+ table.add_column("Server Name", style=f"{Theme.ACCENT} bold")
523
+ table.add_column("Status", style=Theme.TEXT)
524
+ table.add_column("Tools Available", style=Theme.MUTED)
497
525
 
498
526
  tools = mcp_manager.get_tools()
499
527
  server_tools: dict[str, list[str]] = {srv: [] for srv in mcp_manager.config.keys()}
@@ -508,9 +536,9 @@ async def handle_slash_command(
508
536
 
509
537
  for server_name in mcp_manager.config.keys():
510
538
  if server_name in mcp_manager.sessions:
511
- status = "[green]Connected[/green]"
539
+ status = f"[{Theme.SUCCESS}]Connected[/{Theme.SUCCESS}]"
512
540
  else:
513
- status = "[red]Disconnected / Failed[/red]"
541
+ status = f"[{Theme.ERROR}]Disconnected[/{Theme.ERROR}]"
514
542
 
515
543
  tool_count = len(server_tools[server_name])
516
544
  if tool_count > 0:
@@ -531,37 +559,37 @@ async def handle_slash_command(
531
559
  name = arg or f"cp_{datetime.now().strftime('%H%M%S')}"
532
560
  _checkpoints[name] = copy.deepcopy(messages)
533
561
  console.print(
534
- f"[green]✓ Checkpoint '{name}' saved ({len(messages)} messages)[/green]\n"
562
+ f" [{Theme.SUCCESS}]✓ Checkpoint '{name}' saved ({len(messages)} messages)[/{Theme.SUCCESS}]\n"
535
563
  )
536
564
 
537
565
  elif cmd == "/restore":
538
566
  if not arg:
539
567
  if not _checkpoints:
540
- console.print("[yellow]No checkpoints saved. Use /checkpoint [name] first.[/yellow]\n")
568
+ console.print(f" [{Theme.WARNING}]No checkpoints saved. Use /checkpoint [name] first.[/{Theme.WARNING}]\n")
541
569
  else:
542
570
  table = Table(
543
571
  title="📌 Checkpoints",
544
- border_style="magenta",
545
- header_style="bold magenta",
572
+ border_style=Theme.BORDER,
573
+ header_style=f"bold {Theme.PRIMARY}",
546
574
  )
547
- table.add_column("Name", style="cyan")
575
+ table.add_column("Name", style=Theme.ACCENT)
548
576
  table.add_column("Messages", style="white", justify="right")
549
577
  for cp_name, cp_msgs in _checkpoints.items():
550
578
  table.add_row(cp_name, str(len(cp_msgs)))
551
579
  console.print(table)
552
- console.print("[dim]Usage: /restore <name>[/dim]\n")
580
+ console.print(f" [{Theme.MUTED}]Usage: /restore <name>[/{Theme.MUTED}]\n")
553
581
  else:
554
582
  if arg in _checkpoints:
555
583
  messages[:] = copy.deepcopy(_checkpoints[arg])
556
584
  console.print(
557
- f"[green]✓ Restored checkpoint '{arg}' ({len(messages)} messages)[/green]\n"
585
+ f" [{Theme.SUCCESS}]✓ Restored checkpoint '{arg}' ({len(messages)} messages)[/{Theme.SUCCESS}]\n"
558
586
  )
559
587
  else:
560
- console.print(f"[red]Checkpoint '{arg}' not found.[/red]\n")
588
+ console.print(f" [{Theme.ERROR}]Checkpoint '{arg}' not found.[/{Theme.ERROR}]\n")
561
589
 
562
590
  elif cmd == "/commit":
563
591
  if not client:
564
- console.print("[red]API client is not available for /commit.[/red]\n")
592
+ console.print(f" [{Theme.ERROR}]API client is not available for /commit.[/{Theme.ERROR}]\n")
565
593
  return False
566
594
 
567
595
  try:
@@ -575,12 +603,12 @@ async def handle_slash_command(
575
603
  unstaged_diff = result_unstaged.stdout.strip()
576
604
 
577
605
  if not unstaged_diff:
578
- console.print("[yellow]No changes found to commit.[/yellow]\n")
606
+ console.print(f" [{Theme.WARNING}]No changes found to commit.[/{Theme.WARNING}]\n")
579
607
  return False
580
608
 
581
609
  answer = prompt("No staged changes. Stage all current changes? [Y/n] ")
582
610
  if answer.lower() not in ("y", "yes", ""):
583
- console.print("[yellow]Commit aborted.[/yellow]\n")
611
+ console.print(f" [{Theme.WARNING}]Commit aborted.[/{Theme.WARNING}]\n")
584
612
  return False
585
613
 
586
614
  subprocess.run(["git", "add", "-u"], check=True)
@@ -588,10 +616,10 @@ async def handle_slash_command(
588
616
  diff = result.stdout.strip()
589
617
 
590
618
  if not diff:
591
- console.print("[yellow]No changes staged to commit.[/yellow]\n")
619
+ console.print(f" [{Theme.WARNING}]No changes staged to commit.[/{Theme.WARNING}]\n")
592
620
  return False
593
621
 
594
- console.print("[dim]Generating commit message...[/dim]")
622
+ console.print(f" [{Theme.MUTED}]Generating commit message...[/{Theme.MUTED}]")
595
623
 
596
624
  commit_messages = [
597
625
  {"role": "system", "content": "You are a senior developer. Write a concise, conventional commit message for the following diff. Output ONLY the commit message, no explanation, no markdown blocks."},
@@ -607,8 +635,8 @@ async def handle_slash_command(
607
635
  # Remove any markdown codeblocks if model didn't listen
608
636
  commit_msg = commit_msg.replace("```text", "").replace("```", "").strip()
609
637
 
610
- console.print("\n[bold]Generated Commit Message:[/bold]")
611
- console.print(f"[cyan]{commit_msg}[/cyan]\n")
638
+ console.print(f"\n [bold {Theme.TEXT}]Generated Commit Message:[/bold {Theme.TEXT}]")
639
+ console.print(f" [{Theme.ACCENT}]{commit_msg}[/{Theme.ACCENT}]\n")
612
640
 
613
641
  action = prompt("Commit with this message? [Y/n/e(dit)] ")
614
642
  action = action.lower().strip()
@@ -622,12 +650,12 @@ async def handle_slash_command(
622
650
  return False
623
651
 
624
652
  subprocess.run(["git", "commit", "-m", final_msg], check=True)
625
- console.print("[green]✓ Committed successfully.[/green]\n")
653
+ console.print(f" [{Theme.SUCCESS}]✓ Committed successfully.[/{Theme.SUCCESS}]\n")
626
654
 
627
655
  except subprocess.CalledProcessError:
628
- console.print("[red]Error: Not a git repository or git command failed.[/red]\n")
656
+ console.print(f" [{Theme.ERROR}]Error: Not a git repository or git command failed.[/{Theme.ERROR}]\n")
629
657
  except Exception as e:
630
- console.print(f"[red]Error during auto-commit: {e}[/red]\n")
658
+ console.print(f" [{Theme.ERROR}]Error during auto-commit: {e}[/{Theme.ERROR}]\n")
631
659
 
632
660
  elif cmd == "/diff":
633
661
  try:
@@ -648,18 +676,18 @@ async def handle_slash_command(
648
676
  has_output = False
649
677
 
650
678
  if result_staged.stdout.strip():
651
- console.print("[bold green]Staged changes:[/bold green]")
679
+ console.print(f" [bold {Theme.SUCCESS}]Staged changes:[/bold {Theme.SUCCESS}]")
652
680
  console.print(f"[dim]{result_staged.stdout.strip()}[/dim]")
653
681
  has_output = True
654
682
 
655
683
  if result_unstaged.stdout.strip():
656
- console.print("[bold yellow]Unstaged changes:[/bold yellow]")
684
+ console.print(f" [bold {Theme.WARNING}]Unstaged changes:[/bold {Theme.WARNING}]")
657
685
  console.print(f"[dim]{result_unstaged.stdout.strip()}[/dim]")
658
686
  has_output = True
659
687
 
660
688
  if result_untracked.stdout.strip():
661
689
  untracked = result_untracked.stdout.strip().split("\n")
662
- console.print(f"[bold cyan]Untracked files ({len(untracked)}):[/bold cyan]")
690
+ console.print(f" [bold {Theme.ACCENT}]Untracked files ({len(untracked)}):[/bold {Theme.ACCENT}]")
663
691
  for f in untracked[:20]:
664
692
  console.print(f" [dim]+ {f}[/dim]")
665
693
  if len(untracked) > 20:
@@ -667,7 +695,7 @@ async def handle_slash_command(
667
695
  has_output = True
668
696
 
669
697
  if not has_output:
670
- console.print("[green]✓ Working tree is clean.[/green]")
698
+ console.print(f" [{Theme.SUCCESS}]✓ Working tree is clean.[/{Theme.SUCCESS}]")
671
699
 
672
700
  # Show full diff if requested
673
701
  if arg == "--full" or arg == "-f":
@@ -682,13 +710,13 @@ async def handle_slash_command(
682
710
 
683
711
  console.print()
684
712
  except subprocess.CalledProcessError:
685
- console.print("[red]Error: Not a git repository or git command failed.[/red]\n")
713
+ console.print(f" [{Theme.ERROR}]Error: Not a git repository or git command failed.[/{Theme.ERROR}]\n")
686
714
  except Exception as e:
687
- console.print(f"[red]Error showing diff: {e}[/red]\n")
715
+ console.print(f" [{Theme.ERROR}]Error showing diff: {e}[/{Theme.ERROR}]\n")
688
716
 
689
717
  else:
690
718
  console.print(
691
- f"[red]Unknown command: {cmd}[/red] — type [bold]/help[/bold] for commands.\n"
719
+ f" [{Theme.ERROR}]Unknown command: {cmd}[/{Theme.ERROR}] — type [bold {Theme.ACCENT}]/help[/bold {Theme.ACCENT}] for commands.\n"
692
720
  )
693
721
 
694
722
  return False