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.
- max_cli/__init__.py +0 -0
- max_cli/common/cache.py +145 -0
- max_cli/common/concurrent.py +83 -0
- max_cli/common/exceptions.py +40 -0
- max_cli/common/logger.py +22 -0
- max_cli/common/logging.py +24 -0
- max_cli/common/retry.py +51 -0
- max_cli/common/utils.py +40 -0
- max_cli/config.py +43 -0
- max_cli/core/ai_engine.py +541 -0
- max_cli/core/file_organizer.py +254 -0
- max_cli/core/image_processor.py +139 -0
- max_cli/core/media_engine.py +681 -0
- max_cli/core/network_engine.py +103 -0
- max_cli/core/pdf_engine.py +520 -0
- max_cli/core/system_engine.py +57 -0
- max_cli/interface/cli_ai.py +376 -0
- max_cli/interface/cli_config.py +363 -0
- max_cli/interface/cli_files.py +388 -0
- max_cli/interface/cli_images.py +176 -0
- max_cli/interface/cli_media.py +558 -0
- max_cli/interface/cli_network.py +174 -0
- max_cli/interface/cli_pdf.py +651 -0
- max_cli/interface/cli_tools.py +60 -0
- max_cli/main.py +91 -0
- max_cli/plugins/__init__.py +4 -0
- max_cli/plugins/base.py +39 -0
- max_cli/plugins/manager.py +81 -0
- max_cli-0.2.0.dist-info/METADATA +632 -0
- max_cli-0.2.0.dist-info/RECORD +34 -0
- max_cli-0.2.0.dist-info/WHEEL +5 -0
- max_cli-0.2.0.dist-info/entry_points.txt +2 -0
- max_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- max_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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}")
|