aizen-ai-cli 2.2.3__tar.gz → 2.2.5__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.3/aizen_ai_cli.egg-info → aizen_ai_cli-2.2.5}/PKG-INFO +6 -4
  2. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/README.md +4 -3
  3. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/commands.py +128 -105
  4. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/config.py +65 -21
  5. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/context.py +12 -10
  6. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/main.py +82 -64
  7. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/tools.py +311 -102
  8. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/utils.py +9 -4
  9. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5/aizen_ai_cli.egg-info}/PKG-INFO +6 -4
  10. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen_ai_cli.egg-info/requires.txt +1 -0
  11. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/pyproject.toml +2 -1
  12. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/setup.py +4 -2
  13. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_commands.py +7 -6
  14. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_config.py +2 -5
  15. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_context.py +4 -2
  16. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_main.py +9 -8
  17. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_mcp.py +17 -15
  18. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_session.py +2 -5
  19. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_tools.py +61 -42
  20. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/tests/test_utils.py +6 -11
  21. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/MANIFEST.in +0 -0
  22. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/__init__.py +0 -0
  23. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/exceptions.py +0 -0
  24. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/logging_config.py +0 -0
  25. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/mcp.py +0 -0
  26. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/plugins.py +0 -0
  27. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/retry.py +0 -0
  28. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen/session.py +0 -0
  29. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen_ai_cli.egg-info/SOURCES.txt +0 -0
  30. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen_ai_cli.egg-info/dependency_links.txt +0 -0
  31. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen_ai_cli.egg-info/entry_points.txt +0 -0
  32. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/aizen_ai_cli.egg-info/top_level.txt +0 -0
  33. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/requirements.txt +0 -0
  34. {aizen_ai_cli-2.2.3 → aizen_ai_cli-2.2.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aizen-ai-cli
3
- Version: 2.2.3
3
+ Version: 2.2.5
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
 
@@ -60,8 +61,9 @@ Aizen has 9 built-in tools the AI can use:
60
61
  | Tool | Description |
61
62
  |------|-------------|
62
63
  | `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) |
64
+ | `write_file` | Create new files or overwrite entirely (with preview) |
65
+ | `replace_file_content` | Surgical search-and-replace on existing files (with line-bounds and diff preview) |
66
+ | `multi_replace_file_content` | Perform multiple, non-adjacent surgical edits sequentially in a single pass |
65
67
  | `run_command` | Execute shell commands (supports background execution; safe commands auto-run, dangerous ones require approval) |
66
68
  | `check_background_task` | Check the status and read recent output of a command running in the background |
67
69
  | `kill_background_task` | Kill a running background task |
@@ -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
 
@@ -21,8 +21,9 @@ Aizen has 9 built-in tools the AI can use:
21
21
  | Tool | Description |
22
22
  |------|-------------|
23
23
  | `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) |
24
+ | `write_file` | Create new files or overwrite entirely (with preview) |
25
+ | `replace_file_content` | Surgical search-and-replace on existing files (with line-bounds and diff preview) |
26
+ | `multi_replace_file_content` | Perform multiple, non-adjacent surgical edits sequentially in a single pass |
26
27
  | `run_command` | Execute shell commands (supports background execution; safe commands auto-run, dangerous ones require approval) |
27
28
  | `check_background_task` | Check the status and read recent output of a command running in the background |
28
29
  | `kill_background_task` | Kill a running background task |
@@ -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,
@@ -149,7 +150,7 @@ async def handle_slash_command(
149
150
  if cmd == "/clear":
150
151
  if len(messages) > 1:
151
152
  messages[:] = [messages[0]]
152
- console.print("[green]✓ Conversation cleared.[/green]\n")
153
+ console.print(f" [{Theme.SUCCESS}]✓ Conversation cleared.[/{Theme.SUCCESS}]\n")
153
154
 
154
155
  elif cmd == "/drop":
155
156
  dropped_count = 0
@@ -184,31 +185,36 @@ async def handle_slash_command(
184
185
  msg["content"] = new_content
185
186
  dropped_count += 1
186
187
  if dropped_count > 0:
187
- console.print(f"[green]✓ Dropped attached contexts from {dropped_count} past messages.[/green]\n")
188
+ console.print(f" [{Theme.SUCCESS}]✓ Dropped attached contexts from {dropped_count} past messages.[/{Theme.SUCCESS}]\n")
188
189
  else:
189
- console.print("[yellow]No attached contexts found to drop.[/yellow]\n")
190
+ console.print(f" [{Theme.WARNING}]No attached contexts found to drop.[/{Theme.WARNING}]\n")
190
191
 
191
192
  elif cmd == "/model":
192
193
  if arg:
193
194
  if arg.startswith("search ") or arg == "list" or arg == "search":
194
195
  models = get_cached_models()
195
196
  if not models:
196
- console.print("[yellow]Model list is still fetching or unavailable. Try again in a moment.[/yellow]\n")
197
+ console.print(f" [{Theme.WARNING}]Model list is still fetching or unavailable. Try again in a moment.[/{Theme.WARNING}]\n")
197
198
  return False
198
199
 
199
200
  search_query = arg[7:].lower().strip() if arg.startswith("search ") else ""
200
201
 
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")
202
+ table = Table(
203
+ title=f"🧠 OpenRouter Models{' (Search: ' + search_query + ')' if search_query else ''}",
204
+ border_style=Theme.BORDER,
205
+ header_style=f"bold {Theme.PRIMARY}",
206
+ )
207
+ table.add_column("Model ID", style=Theme.ACCENT)
208
+ table.add_column("Name", style=Theme.TEXT)
209
+ table.add_column("Context", style=Theme.MUTED)
210
+ table.add_column("Pricing", style=Theme.SUCCESS)
206
211
 
207
212
  count = 0
208
213
  for m in models:
209
214
  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", "?")
215
+ pricing = m.get("pricing") or {}
216
+ price_prompt = pricing.get("prompt", "?")
217
+ price_comp = pricing.get("completion", "?")
212
218
  pricing_str = f"P: {price_prompt} C: {price_comp}"
213
219
  table.add_row(m["id"], m["name"], str(m.get("context_length")), pricing_str)
214
220
  count += 1
@@ -217,7 +223,7 @@ async def handle_slash_command(
217
223
 
218
224
  console.print(table)
219
225
  if count >= 30:
220
- console.print("[dim]... and more (showing top 30). Use `/model search <query>` to narrow down.[/dim]\n")
226
+ console.print(f" [{Theme.MUTED}]... and more (showing top 30). Use `/model search <query>` to narrow down.[/{Theme.MUTED}]\n")
221
227
  else:
222
228
  console.print()
223
229
  else:
@@ -225,47 +231,64 @@ async def handle_slash_command(
225
231
  found = any(m["id"] == arg for m in models)
226
232
 
227
233
  if models and not found:
228
- console.print(f"[yellow]⚠️ Warning: Model '{arg}' not found in OpenRouter API list.[/yellow]")
234
+ console.print(f" [{Theme.WARNING}]⚠️ Warning: Model '{arg}' not found in OpenRouter API list.[/{Theme.WARNING}]")
229
235
 
230
236
  set_active_model(arg, save=True)
231
- console.print(f"[green]✓ Model switched to:[/green] [bold cyan]{arg}[/bold cyan]\n")
237
+ console.print(f" [{Theme.SUCCESS}]✓ Model switched to:[/{Theme.SUCCESS}] [bold {Theme.ACCENT}]{arg}[/bold {Theme.ACCENT}]\n")
232
238
  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")
239
+ console.print(f" [bold {Theme.TEXT}]Current model:[/bold {Theme.TEXT}] [{Theme.ACCENT}]{current_model}[/{Theme.ACCENT}]")
240
+ console.print(f" [{Theme.MUTED}]Usage: /model <model_name>[/{Theme.MUTED}]")
241
+ console.print(f" [{Theme.MUTED}] /model search <query> (or `/model list`)[/{Theme.MUTED}]\n")
236
242
 
237
243
  elif cmd == "/help":
238
244
  help_table = Table(
239
245
  title="⚡ Aizen Commands",
240
- border_style="magenta",
246
+ border_style=Theme.BORDER,
241
247
  show_header=True,
242
- header_style="bold magenta",
248
+ header_style=f"bold {Theme.PRIMARY}",
249
+ title_style=f"bold {Theme.ACCENT}",
243
250
  )
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]")
251
+ help_table.add_column("Command", style=f"{Theme.ACCENT} bold", min_width=24)
252
+ help_table.add_column("Description", style=Theme.TEXT)
253
+
254
+ # ── Navigation & Info ──
255
+ help_table.add_row(f"[bold {Theme.MUTED}]── Navigation ──[/bold {Theme.MUTED}]", "")
256
+ help_table.add_row(" 📖 /help", "Show this help message")
257
+ help_table.add_row(" ⚙️ /config", "View current configuration")
258
+ help_table.add_row(" 📊 /usage", "Show token usage statistics")
259
+ help_table.add_row(" 🔌 /mcp", "View MCP servers and status")
260
+
261
+ # ── Model ──
262
+ help_table.add_row(f"[bold {Theme.MUTED}]── Model ──[/bold {Theme.MUTED}]", "")
263
+ help_table.add_row(" 🧠 /model [name]", "View or switch the active model")
264
+
265
+ # ── Session ──
266
+ help_table.add_row(f"[bold {Theme.MUTED}]── Session ──[/bold {Theme.MUTED}]", "")
267
+ help_table.add_row(" 💾 /save [name]", "Save current conversation")
268
+ help_table.add_row(" 📂 /load [name]", "Load a saved conversation")
269
+ help_table.add_row(" 📌 /checkpoint [n]", "Save a conversation snapshot")
270
+ help_table.add_row(" 🔄 /restore [name]", "Restore a saved checkpoint")
271
+ help_table.add_row(" 📋 /export [file]", "Export conversation to Markdown")
272
+
273
+ # ── Editing ──
274
+ help_table.add_row(f"[bold {Theme.MUTED}]── Editing ──[/bold {Theme.MUTED}]", "")
275
+ help_table.add_row(" 🗑️ /clear", "Clear conversation history")
276
+ help_table.add_row(" 📎 /drop", "Drop attached files/URLs from history")
277
+ help_table.add_row(" 🧹 /compact", "Summarize conversation to save tokens")
278
+ help_table.add_row(" ↩️ /undo", "Undo the last file modification")
279
+ help_table.add_row(" 🔁 /retry", "Retry the last user message")
280
+ help_table.add_row(" 📝 /copy", "Copy last AI response to clipboard")
281
+
282
+ # ── Git ──
283
+ help_table.add_row(f"[bold {Theme.MUTED}]── Git ──[/bold {Theme.MUTED}]", "")
284
+ help_table.add_row(" 🔀 /commit", "Auto-generate and commit changes")
285
+ help_table.add_row(" 📊 /diff", "Show all uncommitted changes")
286
+
287
+ # ── Shortcuts ──
288
+ help_table.add_row(f"[bold {Theme.MUTED}]── Shortcuts ──[/bold {Theme.MUTED}]", "")
289
+ help_table.add_row(f" [{Theme.PINK}]@file / @url[/{Theme.PINK}]", "Attach file context or web URL")
290
+ help_table.add_row(f" [{Theme.PINK}]exit / quit[/{Theme.PINK}]", "Exit Aizen")
291
+ help_table.add_row(f" [{Theme.MUTED}]Tip[/{Theme.MUTED}]", f"[{Theme.MUTED}]End a line with \\\\ for multi-line input[/{Theme.MUTED}]")
269
292
  console.print(help_table)
270
293
  console.print()
271
294
 
@@ -276,51 +299,51 @@ async def handle_slash_command(
276
299
  elif cmd == "/save":
277
300
  try:
278
301
  path = save_session(messages, arg if arg else None, token_tracker)
279
- console.print(f"[green]✓ Session saved to {path}[/green]\n")
302
+ console.print(f" [{Theme.SUCCESS}]✓ Session saved to {path}[/{Theme.SUCCESS}]\n")
280
303
  except Exception as e:
281
- console.print(f"[red]Error saving session: {e}[/red]\n")
304
+ console.print(f" [{Theme.ERROR}]Error saving session: {e}[/{Theme.ERROR}]\n")
282
305
 
283
306
  elif cmd == "/load":
284
307
  if not arg:
285
308
  sessions = list_sessions()
286
309
  if not sessions:
287
- console.print("[yellow]No saved sessions found.[/yellow]\n")
310
+ console.print(f" [{Theme.WARNING}]No saved sessions found.[/{Theme.WARNING}]\n")
288
311
  else:
289
312
  table = Table(
290
313
  title="📂 Saved Sessions",
291
- border_style="magenta",
292
- header_style="bold magenta",
314
+ border_style=Theme.BORDER,
315
+ header_style=f"bold {Theme.PRIMARY}",
293
316
  )
294
- table.add_column("Name", style="cyan")
295
- table.add_column("Saved At", style="dim")
317
+ table.add_column("Name", style=Theme.ACCENT)
318
+ table.add_column("Saved At", style=Theme.MUTED)
296
319
  table.add_column("Messages", style="white", justify="right")
297
320
  for s in sessions[:10]:
298
321
  table.add_row(s["name"], s["saved_at"][:19], str(s["messages"]))
299
322
  console.print(table)
300
- console.print("[dim]Usage: /load <session_name>[/dim]\n")
323
+ console.print(f" [{Theme.MUTED}]Usage: /load <session_name>[/{Theme.MUTED}]\n")
301
324
  else:
302
325
  loaded = load_session(arg)
303
326
  if loaded:
304
327
  messages[:] = loaded
305
328
  console.print(
306
- f"[green]✓ Loaded session '{arg}' ({len(loaded)} messages)[/green]\n"
329
+ f" [{Theme.SUCCESS}]✓ Loaded session '{arg}' ({len(loaded)} messages)[/{Theme.SUCCESS}]\n"
307
330
  )
308
331
  else:
309
- console.print(f"[red]Session '{arg}' not found.[/red]\n")
332
+ console.print(f" [{Theme.ERROR}]Session '{arg}' not found.[/{Theme.ERROR}]\n")
310
333
 
311
334
  elif cmd == "/undo":
312
335
  result = backup_manager.undo()
313
- console.print(f"[green]{result}[/green]\n")
336
+ console.print(f" [{Theme.SUCCESS}]{result}[/{Theme.SUCCESS}]\n")
314
337
 
315
338
  elif cmd == "/retry":
316
339
  # Remove last assistant + tool messages, then re-process the last user message
317
340
  while messages and messages[-1]["role"] in ("assistant", "tool"):
318
341
  messages.pop()
319
342
  if messages and messages[-1]["role"] == "user":
320
- console.print("[green]✓ Retrying last message...[/green]\n")
343
+ console.print(f" [{Theme.SUCCESS}]✓ Retrying last message...[/{Theme.SUCCESS}]\n")
321
344
  return True # Signal to re-process
322
345
  else:
323
- console.print("[yellow]Nothing to retry.[/yellow]\n")
346
+ console.print(f" [{Theme.WARNING}]Nothing to retry.[/{Theme.WARNING}]\n")
324
347
 
325
348
  elif cmd == "/copy":
326
349
  last_response = None
@@ -349,13 +372,13 @@ async def handle_slash_command(
349
372
  subprocess.run(
350
373
  ["clip"], input=last_response, text=True, check=True
351
374
  )
352
- console.print("[green]✓ Copied to clipboard.[/green]\n")
375
+ console.print(f" [{Theme.SUCCESS}]✓ Copied to clipboard.[/{Theme.SUCCESS}]\n")
353
376
  except Exception:
354
377
  console.print(
355
- "[yellow]⚠️ Could not copy to clipboard.[/yellow]\n"
378
+ f" [{Theme.WARNING}]⚠️ Could not copy to clipboard.[/{Theme.WARNING}]\n"
356
379
  )
357
380
  else:
358
- console.print("[yellow]No response to copy.[/yellow]\n")
381
+ console.print(f" [{Theme.WARNING}]No response to copy.[/{Theme.WARNING}]\n")
359
382
 
360
383
  elif cmd == "/export":
361
384
  filename = (
@@ -377,13 +400,13 @@ async def handle_slash_command(
377
400
  f.write(f"## 👤 You\n\n{msg['content']}\n\n")
378
401
  elif msg["role"] == "assistant" and msg.get("content"):
379
402
  f.write(f"## ✦ Aizen\n\n{msg['content']}\n\n")
380
- console.print(f"[green]✓ Exported to {filename}[/green]\n")
403
+ console.print(f" [{Theme.SUCCESS}]✓ Exported to {filename}[/{Theme.SUCCESS}]\n")
381
404
  except Exception as e:
382
- console.print(f"[red]Error exporting: {e}[/red]\n")
405
+ console.print(f" [{Theme.ERROR}]Error exporting: {e}[/{Theme.ERROR}]\n")
383
406
 
384
407
  elif cmd == "/compact":
385
408
  if len(messages) <= 4:
386
- console.print("[yellow]Conversation is already compact.[/yellow]\n")
409
+ console.print(f" [{Theme.WARNING}]Conversation is already compact.[/{Theme.WARNING}]\n")
387
410
  else:
388
411
  system_msg = messages[0]
389
412
  recent = messages[-4:]
@@ -391,7 +414,7 @@ async def handle_slash_command(
391
414
 
392
415
  if middle:
393
416
  # Attempt LLM-based summarization for much better context retention
394
- console.print("[dim]Summarizing conversation with AI...[/dim]")
417
+ console.print(f" [{Theme.MUTED}]Summarizing conversation with AI...[/{Theme.MUTED}]")
395
418
  try:
396
419
  from openai import AsyncOpenAI as _AsyncOpenAI
397
420
 
@@ -449,20 +472,20 @@ async def handle_slash_command(
449
472
  },
450
473
  ] + recent
451
474
  console.print(
452
- f"[green]✓ Compacted {len(middle)} messages into an AI-generated summary.[/green]\n"
475
+ f" [{Theme.SUCCESS}]✓ Compacted {len(middle)} messages into an AI-generated summary.[/{Theme.SUCCESS}]\n"
453
476
  )
454
477
  else:
455
- console.print("[yellow]Not enough messages to compact.[/yellow]\n")
478
+ console.print(f" [{Theme.WARNING}]Not enough messages to compact.[/{Theme.WARNING}]\n")
456
479
 
457
480
  elif cmd == "/config":
458
481
  config = load_config()
459
482
  table = Table(
460
483
  title="⚙️ Configuration",
461
- border_style="magenta",
462
- header_style="bold magenta",
484
+ border_style=Theme.BORDER,
485
+ header_style=f"bold {Theme.PRIMARY}",
463
486
  )
464
- table.add_column("Key", style="cyan")
465
- table.add_column("Value", style="white")
487
+ table.add_column("Key", style=Theme.ACCENT)
488
+ table.add_column("Value", style=Theme.TEXT)
466
489
  table.add_row("Model", current_model)
467
490
  table.add_row(
468
491
  "API Base URL",
@@ -478,22 +501,22 @@ async def handle_slash_command(
478
501
 
479
502
  elif cmd == "/mcp":
480
503
  if not mcp_manager:
481
- console.print("[yellow]MCP Manager is not available.[/yellow]\n")
504
+ console.print(f" [{Theme.WARNING}]MCP Manager is not available.[/{Theme.WARNING}]\n")
482
505
  return False
483
506
 
484
507
  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")
508
+ console.print(f" [{Theme.WARNING}]No MCP servers configured in ~/.aizen_config.json[/{Theme.WARNING}]\n")
509
+ console.print(f" [{Theme.MUTED}]Add an 'mcp_servers' block to your config to enable MCP plugins.[/{Theme.MUTED}]\n")
487
510
  return False
488
511
 
489
512
  table = Table(
490
513
  title="🔌 Configured MCP Servers",
491
- border_style="magenta",
492
- header_style="bold magenta",
514
+ border_style=Theme.BORDER,
515
+ header_style=f"bold {Theme.PRIMARY}",
493
516
  )
494
- table.add_column("Server Name", style="cyan bold")
495
- table.add_column("Status", style="white")
496
- table.add_column("Tools Available", style="dim")
517
+ table.add_column("Server Name", style=f"{Theme.ACCENT} bold")
518
+ table.add_column("Status", style=Theme.TEXT)
519
+ table.add_column("Tools Available", style=Theme.MUTED)
497
520
 
498
521
  tools = mcp_manager.get_tools()
499
522
  server_tools: dict[str, list[str]] = {srv: [] for srv in mcp_manager.config.keys()}
@@ -508,9 +531,9 @@ async def handle_slash_command(
508
531
 
509
532
  for server_name in mcp_manager.config.keys():
510
533
  if server_name in mcp_manager.sessions:
511
- status = "[green]Connected[/green]"
534
+ status = f"[{Theme.SUCCESS}]Connected[/{Theme.SUCCESS}]"
512
535
  else:
513
- status = "[red]Disconnected / Failed[/red]"
536
+ status = f"[{Theme.ERROR}]Disconnected[/{Theme.ERROR}]"
514
537
 
515
538
  tool_count = len(server_tools[server_name])
516
539
  if tool_count > 0:
@@ -531,37 +554,37 @@ async def handle_slash_command(
531
554
  name = arg or f"cp_{datetime.now().strftime('%H%M%S')}"
532
555
  _checkpoints[name] = copy.deepcopy(messages)
533
556
  console.print(
534
- f"[green]✓ Checkpoint '{name}' saved ({len(messages)} messages)[/green]\n"
557
+ f" [{Theme.SUCCESS}]✓ Checkpoint '{name}' saved ({len(messages)} messages)[/{Theme.SUCCESS}]\n"
535
558
  )
536
559
 
537
560
  elif cmd == "/restore":
538
561
  if not arg:
539
562
  if not _checkpoints:
540
- console.print("[yellow]No checkpoints saved. Use /checkpoint [name] first.[/yellow]\n")
563
+ console.print(f" [{Theme.WARNING}]No checkpoints saved. Use /checkpoint [name] first.[/{Theme.WARNING}]\n")
541
564
  else:
542
565
  table = Table(
543
566
  title="📌 Checkpoints",
544
- border_style="magenta",
545
- header_style="bold magenta",
567
+ border_style=Theme.BORDER,
568
+ header_style=f"bold {Theme.PRIMARY}",
546
569
  )
547
- table.add_column("Name", style="cyan")
570
+ table.add_column("Name", style=Theme.ACCENT)
548
571
  table.add_column("Messages", style="white", justify="right")
549
572
  for cp_name, cp_msgs in _checkpoints.items():
550
573
  table.add_row(cp_name, str(len(cp_msgs)))
551
574
  console.print(table)
552
- console.print("[dim]Usage: /restore <name>[/dim]\n")
575
+ console.print(f" [{Theme.MUTED}]Usage: /restore <name>[/{Theme.MUTED}]\n")
553
576
  else:
554
577
  if arg in _checkpoints:
555
578
  messages[:] = copy.deepcopy(_checkpoints[arg])
556
579
  console.print(
557
- f"[green]✓ Restored checkpoint '{arg}' ({len(messages)} messages)[/green]\n"
580
+ f" [{Theme.SUCCESS}]✓ Restored checkpoint '{arg}' ({len(messages)} messages)[/{Theme.SUCCESS}]\n"
558
581
  )
559
582
  else:
560
- console.print(f"[red]Checkpoint '{arg}' not found.[/red]\n")
583
+ console.print(f" [{Theme.ERROR}]Checkpoint '{arg}' not found.[/{Theme.ERROR}]\n")
561
584
 
562
585
  elif cmd == "/commit":
563
586
  if not client:
564
- console.print("[red]API client is not available for /commit.[/red]\n")
587
+ console.print(f" [{Theme.ERROR}]API client is not available for /commit.[/{Theme.ERROR}]\n")
565
588
  return False
566
589
 
567
590
  try:
@@ -575,12 +598,12 @@ async def handle_slash_command(
575
598
  unstaged_diff = result_unstaged.stdout.strip()
576
599
 
577
600
  if not unstaged_diff:
578
- console.print("[yellow]No changes found to commit.[/yellow]\n")
601
+ console.print(f" [{Theme.WARNING}]No changes found to commit.[/{Theme.WARNING}]\n")
579
602
  return False
580
603
 
581
604
  answer = prompt("No staged changes. Stage all current changes? [Y/n] ")
582
605
  if answer.lower() not in ("y", "yes", ""):
583
- console.print("[yellow]Commit aborted.[/yellow]\n")
606
+ console.print(f" [{Theme.WARNING}]Commit aborted.[/{Theme.WARNING}]\n")
584
607
  return False
585
608
 
586
609
  subprocess.run(["git", "add", "-u"], check=True)
@@ -588,10 +611,10 @@ async def handle_slash_command(
588
611
  diff = result.stdout.strip()
589
612
 
590
613
  if not diff:
591
- console.print("[yellow]No changes staged to commit.[/yellow]\n")
614
+ console.print(f" [{Theme.WARNING}]No changes staged to commit.[/{Theme.WARNING}]\n")
592
615
  return False
593
616
 
594
- console.print("[dim]Generating commit message...[/dim]")
617
+ console.print(f" [{Theme.MUTED}]Generating commit message...[/{Theme.MUTED}]")
595
618
 
596
619
  commit_messages = [
597
620
  {"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 +630,8 @@ async def handle_slash_command(
607
630
  # Remove any markdown codeblocks if model didn't listen
608
631
  commit_msg = commit_msg.replace("```text", "").replace("```", "").strip()
609
632
 
610
- console.print("\n[bold]Generated Commit Message:[/bold]")
611
- console.print(f"[cyan]{commit_msg}[/cyan]\n")
633
+ console.print(f"\n [bold {Theme.TEXT}]Generated Commit Message:[/bold {Theme.TEXT}]")
634
+ console.print(f" [{Theme.ACCENT}]{commit_msg}[/{Theme.ACCENT}]\n")
612
635
 
613
636
  action = prompt("Commit with this message? [Y/n/e(dit)] ")
614
637
  action = action.lower().strip()
@@ -622,12 +645,12 @@ async def handle_slash_command(
622
645
  return False
623
646
 
624
647
  subprocess.run(["git", "commit", "-m", final_msg], check=True)
625
- console.print("[green]✓ Committed successfully.[/green]\n")
648
+ console.print(f" [{Theme.SUCCESS}]✓ Committed successfully.[/{Theme.SUCCESS}]\n")
626
649
 
627
650
  except subprocess.CalledProcessError:
628
- console.print("[red]Error: Not a git repository or git command failed.[/red]\n")
651
+ console.print(f" [{Theme.ERROR}]Error: Not a git repository or git command failed.[/{Theme.ERROR}]\n")
629
652
  except Exception as e:
630
- console.print(f"[red]Error during auto-commit: {e}[/red]\n")
653
+ console.print(f" [{Theme.ERROR}]Error during auto-commit: {e}[/{Theme.ERROR}]\n")
631
654
 
632
655
  elif cmd == "/diff":
633
656
  try:
@@ -648,18 +671,18 @@ async def handle_slash_command(
648
671
  has_output = False
649
672
 
650
673
  if result_staged.stdout.strip():
651
- console.print("[bold green]Staged changes:[/bold green]")
674
+ console.print(f" [bold {Theme.SUCCESS}]Staged changes:[/bold {Theme.SUCCESS}]")
652
675
  console.print(f"[dim]{result_staged.stdout.strip()}[/dim]")
653
676
  has_output = True
654
677
 
655
678
  if result_unstaged.stdout.strip():
656
- console.print("[bold yellow]Unstaged changes:[/bold yellow]")
679
+ console.print(f" [bold {Theme.WARNING}]Unstaged changes:[/bold {Theme.WARNING}]")
657
680
  console.print(f"[dim]{result_unstaged.stdout.strip()}[/dim]")
658
681
  has_output = True
659
682
 
660
683
  if result_untracked.stdout.strip():
661
684
  untracked = result_untracked.stdout.strip().split("\n")
662
- console.print(f"[bold cyan]Untracked files ({len(untracked)}):[/bold cyan]")
685
+ console.print(f" [bold {Theme.ACCENT}]Untracked files ({len(untracked)}):[/bold {Theme.ACCENT}]")
663
686
  for f in untracked[:20]:
664
687
  console.print(f" [dim]+ {f}[/dim]")
665
688
  if len(untracked) > 20:
@@ -667,7 +690,7 @@ async def handle_slash_command(
667
690
  has_output = True
668
691
 
669
692
  if not has_output:
670
- console.print("[green]✓ Working tree is clean.[/green]")
693
+ console.print(f" [{Theme.SUCCESS}]✓ Working tree is clean.[/{Theme.SUCCESS}]")
671
694
 
672
695
  # Show full diff if requested
673
696
  if arg == "--full" or arg == "-f":
@@ -682,13 +705,13 @@ async def handle_slash_command(
682
705
 
683
706
  console.print()
684
707
  except subprocess.CalledProcessError:
685
- console.print("[red]Error: Not a git repository or git command failed.[/red]\n")
708
+ console.print(f" [{Theme.ERROR}]Error: Not a git repository or git command failed.[/{Theme.ERROR}]\n")
686
709
  except Exception as e:
687
- console.print(f"[red]Error showing diff: {e}[/red]\n")
710
+ console.print(f" [{Theme.ERROR}]Error showing diff: {e}[/{Theme.ERROR}]\n")
688
711
 
689
712
  else:
690
713
  console.print(
691
- f"[red]Unknown command: {cmd}[/red] — type [bold]/help[/bold] for commands.\n"
714
+ f" [{Theme.ERROR}]Unknown command: {cmd}[/{Theme.ERROR}] — type [bold {Theme.ACCENT}]/help[/bold {Theme.ACCENT}] for commands.\n"
692
715
  )
693
716
 
694
717
  return False