max-cli 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,376 @@
1
+ import typer
2
+ import subprocess
3
+ import shlex
4
+ from rich.panel import Panel
5
+ from rich.prompt import Confirm, Prompt
6
+ from rich.markdown import Markdown
7
+ from pathlib import Path
8
+ import requests
9
+ from typing import Optional
10
+
11
+ from max_cli.core.ai_engine import AIEngine
12
+ from max_cli.common.logger import console, log_error, log_success
13
+
14
+ app = typer.Typer()
15
+ engine = AIEngine()
16
+
17
+ # We need a reference to the main Typer app to generate docs.
18
+ # We will set this in main.py
19
+ MAIN_APP_REF: Optional[typer.Typer] = None
20
+
21
+
22
+ @app.command("ask")
23
+ def ask_ai(
24
+ prompt: str = typer.Argument(..., help="What do you want to do?"),
25
+ explain: bool = typer.Option(
26
+ False, "--explain", "-e", help="Explain the command logic."
27
+ ),
28
+ ):
29
+ """
30
+ Natural Language Interface.
31
+ Example: max ai ask "Compress all PDFs in Documents folder"
32
+ """
33
+ if MAIN_APP_REF is None:
34
+ log_error("Internal Error: Main App reference not linked.")
35
+ raise typer.Exit(1)
36
+
37
+ console.print(f"[dim]Analyzing request: '{prompt}'...[/dim]")
38
+
39
+ with console.status("[bold cyan]Consulting AI...[/bold cyan]"):
40
+ try:
41
+ result = engine.interpret_intent(prompt, MAIN_APP_REF)
42
+ except Exception as e:
43
+ log_error(str(e))
44
+ raise typer.Exit(1)
45
+
46
+ # Handle AI Rejection
47
+ if "error" in result:
48
+ console.print(
49
+ Panel(result["error"], title="[red]AI Error[/red]", border_style="red")
50
+ )
51
+ return
52
+
53
+ # Handle Success
54
+ cmd_str = result.get("command", "")
55
+ reason = result.get("thought", "")
56
+ is_dangerous = result.get("dangerous", False)
57
+
58
+ # Display Proposal
59
+ console.print(
60
+ Panel(
61
+ f"[dim]{reason}[/dim]\n\n[bold green]> {cmd_str}[/bold green]",
62
+ title="[cyan]Max Suggests[/cyan]",
63
+ border_style="green" if not is_dangerous else "yellow",
64
+ )
65
+ )
66
+
67
+ if explain and result.get("explanation"):
68
+ console.print(
69
+ Panel(
70
+ result["explanation"],
71
+ title="[dim]How it works[/dim]",
72
+ border_style="blue",
73
+ )
74
+ )
75
+
76
+ # Confirmation
77
+ msg = "Run this command?"
78
+ if is_dangerous:
79
+ msg = "[bold red]⚠ This command modifies files. Proceed?[/bold red]"
80
+
81
+ if Confirm.ask(msg):
82
+ console.print("\n[dim]Executing...[/dim]")
83
+ # Execute safely using subprocess
84
+ # We split the string safely to handle quotes properly
85
+ try:
86
+ args = shlex.split(cmd_str)
87
+ subprocess.run(args, check=True)
88
+ except Exception as e:
89
+ log_error(f"Execution failed: {e}")
90
+ else:
91
+ console.print("[yellow]Aborted.[/yellow]")
92
+
93
+
94
+ @app.command("analyze")
95
+ def analyze_image(
96
+ target: Path = typer.Argument(..., help="Path to the image."),
97
+ prompt: str = typer.Option(
98
+ "Describe this image in detail.",
99
+ "--prompt",
100
+ "-p",
101
+ help="Specific question about the image.",
102
+ ),
103
+ ):
104
+ """
105
+ Use AI Vision to describe an image or extract data from it.
106
+ Example: max ai analyze invoice.png -p "Extract the total amount and date"
107
+ """
108
+ if not target.exists():
109
+ log_error(f"Image file not found: {target}")
110
+ raise typer.Exit(1)
111
+
112
+ console.print(f"[dim]Uploading '{target.name}' to AI...[/dim]")
113
+
114
+ with console.status("[bold magenta]Analyzing Vision Data...[/bold magenta]"):
115
+ try:
116
+ # Call the new engine method
117
+ result_text = engine.analyze_image_content(target, prompt)
118
+
119
+ # Render result
120
+ console.print("\n")
121
+ console.print(
122
+ Panel(
123
+ Markdown(result_text),
124
+ title=f"[cyan]Analysis: {target.name}[/cyan]",
125
+ border_style="magenta",
126
+ )
127
+ )
128
+
129
+ except Exception as e:
130
+ log_error(str(e))
131
+ raise typer.Exit(1)
132
+
133
+
134
+ @app.command("create")
135
+ def create_image(
136
+ prompt: str = typer.Argument(..., help="Description of the image to create."),
137
+ output: Optional[Path] = typer.Option(None, "-o", "--output", help="Save path."),
138
+ model: str = typer.Option("gemini-2.5-flash-image", help="Override image model."),
139
+ ):
140
+ """
141
+ Generate an image from text (Nano Banana).
142
+ """
143
+ console.print(f"[cyan]Painting: [bold]{prompt}[/bold]...[/cyan]")
144
+
145
+ with console.status("[bold green]Nano Banana is generating...[/bold green]"):
146
+ try:
147
+ url = engine.generate_image(prompt, model=model)
148
+ _handle_image_result(url, output, "created_image.png")
149
+ except Exception as e:
150
+ log_error(str(e))
151
+
152
+
153
+ @app.command("edit")
154
+ def edit_image(
155
+ target: Path = typer.Argument(..., help="Path to original image."),
156
+ prompt: str = typer.Argument(
157
+ ..., help="Instruction (e.g., 'Turn the sky purple')."
158
+ ),
159
+ output: Optional[Path] = typer.Option(None, "-o", help="Save path."),
160
+ model: str = typer.Option("gemini-2.5-flash-image", help="Override image model."),
161
+ ):
162
+ """
163
+ Edit an existing image using AI instructions.
164
+ """
165
+ if not target.exists():
166
+ log_error(f"File not found: {target}")
167
+ raise typer.Exit(1)
168
+
169
+ console.print(f"[cyan]Editing [bold]{target.name}[/bold]...[/cyan]")
170
+
171
+ with console.status("[bold green]Applying AI changes...[/bold green]"):
172
+ try:
173
+ url = engine.edit_image(target, prompt, model=model)
174
+ _handle_image_result(url, output, f"edited_{target.name}")
175
+ except Exception as e:
176
+ log_error(str(e))
177
+
178
+
179
+ def _handle_image_result(url: str, output_path: Optional[Path], default_name: str):
180
+ """Helper to display URL and download image."""
181
+ console.print("\n[green]✨ Image Ready![/green]")
182
+ console.print(f"🔗 [link={url}]View Online[/link]")
183
+
184
+ # Auto-download
185
+ final_path = output_path or Path.cwd() / default_name
186
+
187
+ try:
188
+ with console.status(f"[dim]Downloading to {final_path.name}...[/dim]"):
189
+ r = requests.get(url, stream=True)
190
+ r.raise_for_status()
191
+ with open(final_path, "wb") as f:
192
+ for chunk in r.iter_content(chunk_size=8192):
193
+ f.write(chunk)
194
+ log_success(f"Saved to: [bold]{final_path}[/bold]")
195
+ except Exception as e:
196
+ console.print(f"[yellow]Could not auto-download: {e}[/yellow]")
197
+
198
+
199
+ @app.command("chat")
200
+ def chat_session(
201
+ clear: bool = typer.Option(False, "--clear", help="Clear conversation history."),
202
+ export: Optional[Path] = typer.Option(
203
+ None, "--export", "-e", help="Export conversation to JSON file."
204
+ ),
205
+ import_file: Optional[Path] = typer.Option(
206
+ None, "--import", "-i", help="Import conversation from JSON file."
207
+ ),
208
+ ):
209
+ """
210
+ Start an interactive session with Max. He remembers what you said.
211
+
212
+ Use --clear to reset history, --export to save, --import to load previous chats.
213
+ """
214
+ if clear:
215
+ engine.clear_history()
216
+ console.print("[green]Conversation history cleared.[/green]")
217
+ return
218
+
219
+ if export:
220
+ engine.export_history(export)
221
+ log_success(f"Conversation exported to: {export}")
222
+ return
223
+
224
+ if import_file:
225
+ if not import_file.exists():
226
+ log_error(f"File not found: {import_file}")
227
+ raise typer.Exit(1)
228
+ engine.import_history(import_file)
229
+ log_success(f"Conversation imported from: {import_file}")
230
+ return
231
+
232
+ console.print(
233
+ Panel(
234
+ "[bold cyan]Max Interactive Session[/bold cyan]\nType 'exit' to quit, 'help' for suggestions.",
235
+ border_style="cyan",
236
+ )
237
+ )
238
+
239
+ if engine.history:
240
+ console.print(
241
+ f"[dim]Loaded {len(engine.history)} messages from previous session[/dim]"
242
+ )
243
+
244
+ while True:
245
+ suggestions = engine.get_suggestions()
246
+ user_input = Prompt.ask(
247
+ "[bold green]User[/bold green]",
248
+ choices=suggestions + ["help", "exit", "quit"],
249
+ show_choices=False,
250
+ )
251
+ if user_input.lower() in ["exit", "quit"]:
252
+ engine._save_history()
253
+ break
254
+ if user_input.lower() == "help":
255
+ console.print("[bold cyan]Suggestions:[/bold cyan]")
256
+ for i, s in enumerate(suggestions, 1):
257
+ console.print(f" {i}. {s}")
258
+ console.print(" [dim]Or type your own command[/dim]")
259
+ continue
260
+
261
+ with console.status("[dim]Thinking...[/dim]"):
262
+ try:
263
+ result = engine.interpret_intent(user_input, MAIN_APP_REF)
264
+
265
+ if "error" in result:
266
+ console.print(f"[red]Max:[/red] {result['error']}")
267
+ continue
268
+
269
+ thought = result.get("thought")
270
+ cmd = result.get("command")
271
+
272
+ if thought and not cmd:
273
+ console.print(f"[cyan]Max:[/cyan] {thought}")
274
+
275
+ elif cmd:
276
+ console.print(
277
+ f"[cyan]Max Suggests:[/cyan] [bold white]{cmd}[/bold white]"
278
+ )
279
+ if thought:
280
+ console.print(f"[dim]Reason: {thought}[/dim]")
281
+
282
+ if Confirm.ask("Execute?"):
283
+ args = shlex.split(cmd)
284
+ subprocess.run(args)
285
+
286
+ except Exception as e:
287
+ log_error(str(e))
288
+
289
+ engine._save_history()
290
+ console.print("[cyan]Goodbye![/cyan]")
291
+
292
+
293
+ @app.command("search")
294
+ def semantic_search_cmd(
295
+ query: str = typer.Argument(..., help="Search query in natural language."),
296
+ path: Path = typer.Argument(".", help="Folder to search in."),
297
+ extensions: str = typer.Option(
298
+ "txt,md,py,json,yaml",
299
+ "--ext",
300
+ help="File extensions to search (comma-separated).",
301
+ ),
302
+ ):
303
+ """
304
+ Search files by content using AI (semantic search).
305
+ """
306
+
307
+ if not path.is_dir():
308
+ log_error(f"Folder not found: {path}")
309
+ raise typer.Exit(1)
310
+
311
+ exts = [f".{e.strip()}" for e in extensions.split(",")]
312
+ files = [f for f in path.rglob("*") if f.is_file() and f.suffix.lower() in exts]
313
+
314
+ if not files:
315
+ console.print("[yellow]No matching files found.[/yellow]")
316
+ return
317
+
318
+ console.print(f"[cyan]Searching {len(files)} files for: '{query}'...[/cyan]")
319
+
320
+ try:
321
+ results = engine.semantic_search(query, files)
322
+
323
+ if not results:
324
+ console.print("[yellow]No matches found.[/yellow]")
325
+ else:
326
+ console.print(f"[green]Found {len(results)} matching file(s):[/green]\n")
327
+ for r in results:
328
+ console.print(f" [bold]{r['file']}[/bold]")
329
+ console.print(f" [dim]{r.get('reasoning', '')}[/dim]\n")
330
+ except Exception as e:
331
+ log_error(f"Search failed: {e}")
332
+
333
+
334
+ @app.command("extract")
335
+ def extract_data_cmd(
336
+ target: Path = typer.Argument(..., help="Image file to extract data from."),
337
+ schema: str = typer.Option(
338
+ ...,
339
+ "--schema",
340
+ "-s",
341
+ help="Schema as field:description (can specify multiple).",
342
+ ),
343
+ output: Optional[Path] = typer.Option(None, "-o", help="Output JSON file."),
344
+ ):
345
+ """
346
+ Extract structured data from images (receipts, invoices, etc.).
347
+
348
+ Example: max ai extract receipt.jpg -s "total:Total amount" -s "date:Date"
349
+ """
350
+ if not target.exists():
351
+ log_error(f"File not found: {target}")
352
+ raise typer.Exit(1)
353
+
354
+ schema_dict = {}
355
+ for s in schema:
356
+ if ":" in s:
357
+ field, desc = s.split(":", 1)
358
+ schema_dict[field.strip()] = desc.strip()
359
+ else:
360
+ schema_dict[s.strip()] = ""
361
+
362
+ console.print(f"[cyan]Extracting data from {target.name}...[/cyan]")
363
+
364
+ try:
365
+ result = engine.extract_structured_data(target, schema_dict)
366
+
367
+ console.print("\n[bold green]Extracted Data:[/bold green]")
368
+ import json
369
+
370
+ console.print(json.dumps(result, indent=2))
371
+
372
+ if output:
373
+ output.write_text(json.dumps(result, indent=2))
374
+ log_success(f"Saved to: {output}")
375
+ except Exception as e:
376
+ log_error(f"Extraction failed: {e}")