lovarch-cli 0.2.1__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.
Files changed (122) hide show
  1. lovarch_cli/__init__.py +16 -0
  2. lovarch_cli/__main__.py +10 -0
  3. lovarch_cli/ai/__init__.py +21 -0
  4. lovarch_cli/ai/gateway.py +240 -0
  5. lovarch_cli/api.py +111 -0
  6. lovarch_cli/auth/__init__.py +32 -0
  7. lovarch_cli/auth/keyring_store.py +214 -0
  8. lovarch_cli/auth/local_server.py +165 -0
  9. lovarch_cli/auth/pkce.py +57 -0
  10. lovarch_cli/auth/session.py +189 -0
  11. lovarch_cli/cli.py +262 -0
  12. lovarch_cli/clients/__init__.py +33 -0
  13. lovarch_cli/clients/factory.py +54 -0
  14. lovarch_cli/clients/local_client.py +432 -0
  15. lovarch_cli/clients/lovarch_storage.py +174 -0
  16. lovarch_cli/clients/lovarch_supabase.py +295 -0
  17. lovarch_cli/clients/persistence.py +166 -0
  18. lovarch_cli/clients/storage.py +66 -0
  19. lovarch_cli/commands/__init__.py +10 -0
  20. lovarch_cli/commands/account.py +172 -0
  21. lovarch_cli/commands/audit.py +394 -0
  22. lovarch_cli/commands/config_cmd.py +80 -0
  23. lovarch_cli/commands/consolidate.py +217 -0
  24. lovarch_cli/commands/context_cmd.py +73 -0
  25. lovarch_cli/commands/dev.py +287 -0
  26. lovarch_cli/commands/do_cmd.py +120 -0
  27. lovarch_cli/commands/init.py +218 -0
  28. lovarch_cli/commands/jobs_cmd.py +95 -0
  29. lovarch_cli/commands/login.py +202 -0
  30. lovarch_cli/commands/mcp_cmd.py +26 -0
  31. lovarch_cli/commands/run.py +375 -0
  32. lovarch_cli/commands/signup.py +185 -0
  33. lovarch_cli/commands/status.py +243 -0
  34. lovarch_cli/commands/upgrade.py +108 -0
  35. lovarch_cli/commands/verifica_cmd.py +174 -0
  36. lovarch_cli/config.py +101 -0
  37. lovarch_cli/config_store.py +111 -0
  38. lovarch_cli/credits/__init__.py +35 -0
  39. lovarch_cli/credits/base.py +84 -0
  40. lovarch_cli/credits/factory.py +36 -0
  41. lovarch_cli/credits/local.py +34 -0
  42. lovarch_cli/credits/lovarch.py +56 -0
  43. lovarch_cli/i18n/__init__.py +27 -0
  44. lovarch_cli/i18n/loader.py +121 -0
  45. lovarch_cli/i18n/translations/en.json +168 -0
  46. lovarch_cli/i18n/translations/es.json +168 -0
  47. lovarch_cli/i18n/translations/it.json +168 -0
  48. lovarch_cli/i18n/translations/pt.json +168 -0
  49. lovarch_cli/mcp/__init__.py +9 -0
  50. lovarch_cli/mcp/server.py +199 -0
  51. lovarch_cli/mcp/tools.py +372 -0
  52. lovarch_cli/sample_downloader.py +255 -0
  53. lovarch_cli/squad/README.md +206 -0
  54. lovarch_cli/squad/agents/auditor-input.md +353 -0
  55. lovarch_cli/squad/agents/bim-engineer.md +404 -0
  56. lovarch_cli/squad/agents/briefing-architect.md +249 -0
  57. lovarch_cli/squad/agents/cad-engineer.md +278 -0
  58. lovarch_cli/squad/agents/capitolato-writer.md +256 -0
  59. lovarch_cli/squad/agents/computo-engineer.md +258 -0
  60. lovarch_cli/squad/agents/concept-designer.md +399 -0
  61. lovarch_cli/squad/agents/contratto-architect.md +243 -0
  62. lovarch_cli/squad/agents/deliverable-builder.md +253 -0
  63. lovarch_cli/squad/agents/energy-prelim.md +388 -0
  64. lovarch_cli/squad/agents/pratiche-it.md +251 -0
  65. lovarch_cli/squad/agents/progetto-chief.md +768 -0
  66. lovarch_cli/squad/agents/quality-dati.md +409 -0
  67. lovarch_cli/squad/agents/quality-misure.md +418 -0
  68. lovarch_cli/squad/agents/quality-normativa.md +417 -0
  69. lovarch_cli/squad/agents/quality-output.md +436 -0
  70. lovarch_cli/squad/agents/regolatorio-it.md +278 -0
  71. lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
  72. lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
  73. lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
  74. lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
  75. lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
  76. lovarch_cli/squad/config.yaml +408 -0
  77. lovarch_cli/squad/data/CHANGELOG.md +272 -0
  78. lovarch_cli/squad/data/agents-prd.md +428 -0
  79. lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
  80. lovarch_cli/squad/data/handoff-card-template.md +231 -0
  81. lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
  82. lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
  83. lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
  84. lovarch_cli/squad/scripts/api_clients.py +206 -0
  85. lovarch_cli/squad/scripts/architect_profile.py +276 -0
  86. lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
  87. lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
  88. lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
  89. lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
  90. lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
  91. lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
  92. lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
  93. lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
  94. lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
  95. lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
  96. lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
  97. lovarch_cli/squad/scripts/validate-squad.py +383 -0
  98. lovarch_cli/squad/tasks/audit-input.md +146 -0
  99. lovarch_cli/squad/tasks/compute-metric.md +105 -0
  100. lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
  101. lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
  102. lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
  103. lovarch_cli/squad/tasks/write-capitolato.md +100 -0
  104. lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
  105. lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
  106. lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
  107. lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
  108. lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
  109. lovarch_cli/squad_loader.py +114 -0
  110. lovarch_cli/verify/__init__.py +15 -0
  111. lovarch_cli/verify/contratto.py +110 -0
  112. lovarch_cli/verify/dossier.py +97 -0
  113. lovarch_cli/verify/misure.py +83 -0
  114. lovarch_cli/verify/normativa.py +178 -0
  115. lovarch_cli/version.py +13 -0
  116. lovarch_cli/workflows/__init__.py +9 -0
  117. lovarch_cli/workflows/platform.py +212 -0
  118. lovarch_cli-0.2.1.dist-info/METADATA +232 -0
  119. lovarch_cli-0.2.1.dist-info/RECORD +122 -0
  120. lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
  121. lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
  122. lovarch_cli-0.2.1.dist-info/licenses/LICENSE +38 -0
@@ -0,0 +1,218 @@
1
+ """lovarch init — Create a new project directory.
2
+
3
+ Creates ~/.lovarch/projects/<project>/ with input/ and output/ subdirs and
4
+ a project.yaml metadata file. With --sample copies the bundled villa-chianti
5
+ sample-input as a starter pack the user can run end-to-end with no extra work.
6
+
7
+ Project layout after `lovarch init my-villa --sample`:
8
+
9
+ ~/.lovarch/projects/my-villa/
10
+ ├── project.yaml # name, created_at, workflow, mode
11
+ ├── input/ # 18 input files (briefing, photos, DXF, etc.)
12
+ │ ├── architetto-info.json
13
+ │ ├── briefing-cliente.md
14
+ │ ├── foto/
15
+ │ ├── pinterest-references/
16
+ │ ├── README.md
17
+ │ ├── stato-attuale.dxf
18
+ │ ├── stato-attuale.png
19
+ │ └── visura-catastale.pdf
20
+ └── output/ # populated by `lovarch run`
21
+
22
+ The sample comes from the bundled squad payload (lovarch_cli/squad/), so it
23
+ ships with the pip package — no separate download needed.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ import shutil
29
+ import sys
30
+ from datetime import datetime, timezone
31
+ from pathlib import Path
32
+ from typing import Annotated
33
+
34
+ import typer
35
+ import yaml
36
+ from rich.console import Console
37
+ from rich.panel import Panel
38
+ from rich.text import Text
39
+
40
+ from lovarch_cli.config import DEFAULT_HOME
41
+ from lovarch_cli.i18n import current_lang, set_current_lang, t
42
+
43
+ console = Console()
44
+ err_console = Console(stderr=True)
45
+
46
+ # Valid project name pattern: lowercase letters, digits, hyphens.
47
+ # Forbids path-traversal characters and shell-unfriendly chars.
48
+ PROJECT_NAME_RX = re.compile(r"^[a-z0-9][a-z0-9-]{1,62}$")
49
+
50
+ # Sample-input villa-chianti is NOT bundled in the wheel (49MB of photos/DXF/
51
+ # PDF — too heavy). It ships as a GitHub Releases asset and lovarch_cli.
52
+ # sample_downloader.resolve_sample_source() handles bundled→cache→download
53
+ # resolution transparently.
54
+
55
+
56
+ def _project_dir(home: Path, name: str) -> Path:
57
+ return home / "projects" / name
58
+
59
+
60
+ def init_command(
61
+ project: Annotated[
62
+ str,
63
+ typer.Argument(
64
+ help="Project name (lowercase letters/digits/hyphens, 2-63 chars).",
65
+ ),
66
+ ],
67
+ sample: Annotated[
68
+ bool,
69
+ typer.Option(
70
+ "--sample",
71
+ help="Copy the bundled villa-chianti sample-input as a starter.",
72
+ ),
73
+ ] = False,
74
+ workflow: Annotated[
75
+ str,
76
+ typer.Option(
77
+ "--workflow",
78
+ help="Target workflow this project will run against.",
79
+ ),
80
+ ] = "dal-brief-al-cantiere",
81
+ force: Annotated[
82
+ bool,
83
+ typer.Option(
84
+ "--force",
85
+ help="Overwrite an existing project directory (dangerous).",
86
+ ),
87
+ ] = False,
88
+ lang_flag: Annotated[
89
+ str | None,
90
+ typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
91
+ ] = None,
92
+ home_override: Annotated[
93
+ Path | None,
94
+ typer.Option(
95
+ "--home",
96
+ help="Override $HOME/.lovarch root (mainly for tests).",
97
+ hidden=True,
98
+ ),
99
+ ] = None,
100
+ squad_src: Annotated[
101
+ Path | None,
102
+ typer.Option(
103
+ "--squad-src",
104
+ help=(
105
+ "Path to a squad-architettura-progetto source dir. Also reads "
106
+ "$LOVARCH_SQUAD_SRC. When set, --sample reads the villa-"
107
+ "chianti sample from this path instead of downloading."
108
+ ),
109
+ ),
110
+ ] = None,
111
+ ) -> None:
112
+ """Initialize a new project directory."""
113
+ if lang_flag is not None:
114
+ set_current_lang(lang_flag)
115
+ lang = current_lang()
116
+
117
+ # ─── Validate project name ───────────────────────────────────────────
118
+ if not PROJECT_NAME_RX.match(project):
119
+ err_console.print(
120
+ f"\n[red]✗ {t('init.invalid_name', lang=lang, name=project)}[/red]\n"
121
+ )
122
+ sys.exit(2)
123
+
124
+ home = home_override or DEFAULT_HOME
125
+ proj_dir = _project_dir(home, project)
126
+
127
+ # ─── Handle existing project ─────────────────────────────────────────
128
+ if proj_dir.exists():
129
+ if not force:
130
+ err_console.print(
131
+ f"\n[yellow]{t('init.already_exists', lang=lang, name=project, path=str(proj_dir))}[/yellow]\n"
132
+ )
133
+ sys.exit(1)
134
+ shutil.rmtree(proj_dir)
135
+
136
+ # ─── Create dir tree ─────────────────────────────────────────────────
137
+ input_dir = proj_dir / "input"
138
+ output_dir = proj_dir / "output"
139
+ input_dir.mkdir(parents=True)
140
+ output_dir.mkdir()
141
+
142
+ # ─── Optional: resolve + copy sample-input (lazy download if needed) ─
143
+ sample_copied_count = 0
144
+ sample_origin: str | None = None
145
+ if sample:
146
+ from lovarch_cli.sample_downloader import (
147
+ SampleDownloadError,
148
+ resolve_sample_source,
149
+ )
150
+
151
+ try:
152
+ resolved = resolve_sample_source(
153
+ console=console,
154
+ lang=lang,
155
+ home=home,
156
+ squad_src=squad_src,
157
+ )
158
+ src = resolved.path
159
+ sample_origin = resolved.origin
160
+ except SampleDownloadError as exc:
161
+ err_console.print(f"\n[red]✗ {exc}[/red]")
162
+ err_console.print(
163
+ f"[dim]{t('init.sample_hint', lang=lang)}[/dim]\n"
164
+ )
165
+ # Don't bail: project dir exists, user can still add own input.
166
+ else:
167
+ for item in src.iterdir():
168
+ if item.is_dir():
169
+ shutil.copytree(item, input_dir / item.name)
170
+ else:
171
+ shutil.copy2(item, input_dir / item.name)
172
+ sample_copied_count = sum(1 for _ in input_dir.rglob("*") if _.is_file())
173
+
174
+ # ─── Write project.yaml metadata ─────────────────────────────────────
175
+ metadata = {
176
+ "name": project,
177
+ "created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
178
+ "workflow": workflow,
179
+ "sample": sample,
180
+ "cli_version": _cli_version(),
181
+ }
182
+ (proj_dir / "project.yaml").write_text(
183
+ yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True),
184
+ encoding="utf-8",
185
+ )
186
+
187
+ # ─── Success panel ───────────────────────────────────────────────────
188
+ body_lines: list[str] = [
189
+ t("init.created", lang=lang, path=str(proj_dir)),
190
+ ]
191
+ if sample and sample_copied_count > 0:
192
+ body_lines.append(
193
+ t("init.sample_copied", lang=lang, count=sample_copied_count)
194
+ )
195
+ if sample_origin in {"cache", "download"}:
196
+ body_lines.append(
197
+ f"[dim]{t(f'init.sample_origin_{sample_origin}', lang=lang)}[/dim]"
198
+ )
199
+ body_lines.append("")
200
+ body_lines.append(t("init.next_steps", lang=lang, name=project))
201
+
202
+ console.print()
203
+ console.print(
204
+ Panel(
205
+ Text.from_markup("\n".join(body_lines)),
206
+ title=f"[bold gold1]🌱 {t('init.title', lang=lang, name=project)}[/bold gold1]",
207
+ border_style="gold1",
208
+ padding=(1, 2),
209
+ )
210
+ )
211
+ console.print()
212
+
213
+
214
+ def _cli_version() -> str:
215
+ """Read version lazily — avoids circular import at module load."""
216
+ from lovarch_cli.version import __version__
217
+
218
+ return __version__
@@ -0,0 +1,95 @@
1
+ """`lovarch jobs` — async platform jobs (video, Shotstack export, upscale).
2
+
3
+ lovarch jobs list
4
+ lovarch jobs status <id>
5
+
6
+ Reads content_video_jobs via the user's own session (owner RLS). Costs are
7
+ shown only as the user's credits.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ console = Console()
18
+ err_console = Console(stderr=True)
19
+
20
+ jobs_app = typer.Typer(
21
+ help="Job asincroni della piattaforma (video, export, upscale).",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ _STATUS_ICON = {"done": "✅", "completed": "✅", "failed": "❌", "error": "❌",
26
+ "running": "⏳", "pending": "⏳", "queued": "⏳"}
27
+
28
+
29
+ def _workflows():
30
+ from lovarch_cli.auth.session import LovarchSession
31
+ from lovarch_cli.workflows import PlatformWorkflows
32
+
33
+ session = LovarchSession.load()
34
+ if session is None:
35
+ err_console.print("[red]✗ Non autenticato. Esegui `lovarch login --premium`.[/red]")
36
+ raise typer.Exit(1)
37
+ return PlatformWorkflows(session)
38
+
39
+
40
+ @jobs_app.command("list")
41
+ def list_command(
42
+ limit: int = typer.Option(10, "--limit", "-n", help="Quanti job mostrare."),
43
+ ) -> None:
44
+ """Ultimi job asincroni dell'account."""
45
+ from lovarch_cli.workflows import WorkflowError
46
+
47
+ try:
48
+ jobs = asyncio.run(_workflows().jobs_list(limit=limit))
49
+ except WorkflowError as exc:
50
+ err_console.print(f"[red]✗ {exc}[/red]")
51
+ raise typer.Exit(1)
52
+ if not jobs:
53
+ console.print("[dim]Nessun job trovato.[/dim]")
54
+ return
55
+ table = Table(title="Job asincroni", header_style="bold gold1")
56
+ for col in ("Stato", "Engine", "Modello", "Crediti", "Creato", "ID"):
57
+ table.add_column(col)
58
+ for j in jobs:
59
+ status = str(j.get("status", "?"))
60
+ table.add_row(
61
+ f"{_STATUS_ICON.get(status, '·')} {status}",
62
+ str(j.get("engine", "—")), str(j.get("model", "—")),
63
+ str(j.get("credits_charged", "—")),
64
+ str(j.get("created_at", ""))[:16], str(j.get("id", ""))[:8],
65
+ )
66
+ console.print(table)
67
+
68
+
69
+ @jobs_app.command("status")
70
+ def status_command(
71
+ job_id: str = typer.Argument(..., help="ID del job."),
72
+ ) -> None:
73
+ """Stato dettagliato di un job."""
74
+ from lovarch_cli.workflows import WorkflowError
75
+
76
+ try:
77
+ j = asyncio.run(_workflows().job_status(job_id))
78
+ except WorkflowError as exc:
79
+ err_console.print(f"[red]✗ {exc}[/red]")
80
+ raise typer.Exit(1)
81
+ status = str(j.get("status", "?"))
82
+ console.print(f"{_STATUS_ICON.get(status, '·')} [bold]{status}[/bold] · {j.get('engine')} · {j.get('model')}")
83
+ if j.get("prompt"):
84
+ console.print(f"[dim]{str(j['prompt'])[:120]}[/dim]")
85
+ if j.get("output_url"):
86
+ console.print(f"[green]Output:[/green] {j['output_url']}")
87
+ if j.get("error_message"):
88
+ console.print(f"[red]Errore:[/red] {j['error_message']}")
89
+ charged = j.get("credits_charged")
90
+ refunded = j.get("credits_refunded")
91
+ if charged is not None:
92
+ line = f"Crediti: {charged}"
93
+ if refunded:
94
+ line += f" (rimborsati: {refunded})"
95
+ console.print(f"[dim]{line}[/dim]")
@@ -0,0 +1,202 @@
1
+ """arch login — Authentication entry point (Free or Premium).
2
+
3
+ Subcommand structure:
4
+ arch login → interactive: choose Free or Premium
5
+ arch login --free → redirects to arch signup (free has no separate login)
6
+ arch login --premium → PKCE flow with Lovarch web
7
+
8
+ Premium PKCE flow:
9
+ 1. Generate verifier + challenge + state via PkceParams.generate()
10
+ 2. Spin up local HTTP server on 127.0.0.1:RANDOM/callback
11
+ 3. Open browser to https://lovarch.com/cli-auth?...
12
+ 4. Show "Waiting for browser..." spinner with timeout 5min
13
+ 5. On callback: validate state matches, POST cli-auth-exchange EF
14
+ 6. Save tokens via keyring_store.save_premium_session
15
+ 7. Show success panel with user info
16
+
17
+ Localized in 4 languages — keys live in lovarch_cli/i18n/translations/*.json.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import sys
23
+ import webbrowser
24
+ from typing import Annotated
25
+ from urllib.parse import urlencode
26
+
27
+ import typer
28
+ from rich.console import Console
29
+ from rich.panel import Panel
30
+ from rich.prompt import Prompt
31
+ from rich.text import Text
32
+
33
+ from lovarch_cli.api import ApiClient, LovarchApiError
34
+ from lovarch_cli.auth import PkceParams, save_premium_session
35
+ from lovarch_cli.auth.local_server import AuthServer
36
+ from lovarch_cli.config import DEFAULT_API_URL
37
+ from lovarch_cli.i18n import current_lang, set_current_lang, t
38
+
39
+ console = Console()
40
+ err_console = Console(stderr=True)
41
+
42
+ # https://lovarch.com host — ALWAYS the web app for /cli-auth, NOT the
43
+ # Supabase API URL (which is for EF calls). This is hardcoded because the
44
+ # /cli-auth React page only exists on lovarch.com.
45
+ LOVARCH_WEB_BASE = "https://lovarch.com"
46
+ PKCE_TIMEOUT_SECONDS = 300.0 # 5 minutes — matches DB code TTL
47
+
48
+
49
+ def login_command(
50
+ free: Annotated[
51
+ bool,
52
+ typer.Option("--free", help="Free mode (redirects to arch signup)."),
53
+ ] = False,
54
+ premium: Annotated[
55
+ bool,
56
+ typer.Option(
57
+ "--premium",
58
+ help="Premium mode (PKCE flow with Lovarch login).",
59
+ ),
60
+ ] = False,
61
+ lang_flag: Annotated[
62
+ str | None,
63
+ typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
64
+ ] = None,
65
+ ) -> None:
66
+ """Login al CLI (Free o Premium)."""
67
+ if lang_flag is not None:
68
+ set_current_lang(lang_flag)
69
+ lang = current_lang()
70
+
71
+ # Mutually exclusive flags
72
+ if free and premium:
73
+ err_console.print(f"[red]{t('login.modes_mutex', lang=lang)}[/red]")
74
+ sys.exit(2)
75
+
76
+ if not free and not premium:
77
+ # Interactive choice
78
+ choice = Prompt.ask(
79
+ f"[bold]{t('login.choose_mode_prompt', lang=lang)}[/bold]",
80
+ choices=["free", "premium"],
81
+ default="free",
82
+ )
83
+ free = choice == "free"
84
+ premium = choice == "premium"
85
+
86
+ if free:
87
+ console.print()
88
+ console.print(t("login.free_redirect", lang=lang))
89
+ console.print()
90
+ sys.exit(0)
91
+
92
+ # ─── Premium PKCE flow ───────────────────────────────────────────────
93
+ pkce = PkceParams.generate()
94
+ server = AuthServer(port=0)
95
+ server.start()
96
+
97
+ auth_url = (
98
+ f"{LOVARCH_WEB_BASE}/cli-auth?"
99
+ + urlencode(
100
+ {
101
+ "state": pkce.state,
102
+ "code_challenge": pkce.code_challenge,
103
+ "code_challenge_method": pkce.code_challenge_method,
104
+ "redirect_uri": server.callback_url,
105
+ "lang": lang,
106
+ }
107
+ )
108
+ )
109
+
110
+ console.print()
111
+ console.print(f"[gold1]→[/gold1] {t('login.opening_browser', lang=lang)}")
112
+ console.print(f" [dim]{t('login.manual_url_hint', lang=lang)}[/dim]")
113
+ console.print(f" [dim cyan]{auth_url}[/dim cyan]")
114
+ console.print()
115
+
116
+ webbrowser.open(auth_url, new=1, autoraise=True)
117
+
118
+ with console.status(
119
+ Text.from_markup(f"[gold1]{t('login.waiting_callback', lang=lang)}[/gold1]"),
120
+ spinner="dots",
121
+ ):
122
+ result = server.wait_for_callback(PKCE_TIMEOUT_SECONDS)
123
+
124
+ server.shutdown()
125
+
126
+ # ─── Validate callback ───────────────────────────────────────────────
127
+ if result.error:
128
+ if result.error == "timeout":
129
+ err_console.print(
130
+ f"\n[red]✗ {t('login.callback_timeout', lang=lang)}[/red]"
131
+ )
132
+ elif result.error == "access_denied":
133
+ err_console.print(
134
+ f"\n[yellow]✗ {t('login.auth_denied', lang=lang)}[/yellow]"
135
+ )
136
+ else:
137
+ err_console.print(
138
+ f"\n[red]✗ {result.error}: {result.error_description or ''}[/red]"
139
+ )
140
+ sys.exit(1)
141
+
142
+ if not result.code or result.state != pkce.state:
143
+ err_console.print(f"\n[red]✗ {t('login.state_mismatch', lang=lang)}[/red]")
144
+ sys.exit(1)
145
+
146
+ # ─── Exchange code for tokens ────────────────────────────────────────
147
+ api = ApiClient(base_url=DEFAULT_API_URL)
148
+ try:
149
+ response = asyncio.run(
150
+ api.invoke_ef(
151
+ "cli-auth-exchange",
152
+ {
153
+ "code": result.code,
154
+ "code_verifier": pkce.code_verifier,
155
+ "language": lang,
156
+ },
157
+ )
158
+ )
159
+ except LovarchApiError as exc:
160
+ err_console.print(f"\n[red]✗ {exc}[/red]")
161
+ if exc.error_code:
162
+ err_console.print(f"[dim](error_code: {exc.error_code})[/dim]")
163
+ sys.exit(1)
164
+
165
+ # ─── Save session ────────────────────────────────────────────────────
166
+ user = response.get("user", {})
167
+ expires_in = int(response.get("expires_in", 3600))
168
+ from datetime import datetime, timedelta, timezone
169
+
170
+ expires_at = (
171
+ datetime.now(timezone.utc) + timedelta(seconds=expires_in)
172
+ ).isoformat(timespec="seconds")
173
+
174
+ used_keyring, location = save_premium_session(
175
+ user_id=user.get("id", ""),
176
+ email=user.get("email", ""),
177
+ full_name=user.get("full_name"),
178
+ access_token=response["access_token"],
179
+ refresh_token=response["refresh_token"],
180
+ expires_at=expires_at,
181
+ language=lang,
182
+ )
183
+
184
+ body = Text.assemble(
185
+ ("Account: ", "dim"),
186
+ (f"{user.get('email','')}\n", "bold"),
187
+ ("User ID: ", "dim"),
188
+ (f"{user.get('id','')}\n", "bold dim"),
189
+ ("Expires: ", "dim"),
190
+ (f"{expires_at}\n", "bold"),
191
+ ("Storage: ", "dim"),
192
+ (location, "bold" if used_keyring else "italic yellow"),
193
+ )
194
+ console.print()
195
+ console.print(
196
+ Panel(
197
+ body,
198
+ title=f"[bold green]{t('login.login_success', lang=lang)}[/bold green]",
199
+ border_style="green",
200
+ padding=(1, 2),
201
+ )
202
+ )
@@ -0,0 +1,26 @@
1
+ """`lovarch mcp` — run the Lovarch MCP server.
2
+
3
+ The heavy ``mcp`` SDK import is lazy (inside the command) so that
4
+ ``lovarch --help`` and every other command keep working even when the optional
5
+ ``[mcp]`` extra is not installed.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import typer
10
+
11
+ mcp_app = typer.Typer(
12
+ help="Server MCP di Lovarch (per Claude Code / IDE). Richiede l'extra [mcp].",
13
+ no_args_is_help=True,
14
+ )
15
+
16
+
17
+ @mcp_app.command("serve")
18
+ def serve_command() -> None:
19
+ """Avvia il server MCP su stdio.
20
+
21
+ Registra in Claude Code con:
22
+ claude mcp add lovarch -- lovarch mcp serve
23
+ """
24
+ from lovarch_cli.mcp.server import serve
25
+
26
+ serve()