wagner-cli 0.1.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.
wagner_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Wagner CLI — AI coding orchestrator with self-improving specialists."""
2
+
3
+ __version__ = "0.1.0"
wagner_cli/client.py ADDED
@@ -0,0 +1,108 @@
1
+ """Wagner API client."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import httpx
8
+
9
+
10
+ DEFAULT_SERVER = "https://wagner.razzyshmazzy.com"
11
+ CONFIG_PATH = Path.home() / ".wagner" / "config.json"
12
+
13
+
14
+ class WagnerClient:
15
+ """Client for the Wagner orchestration API."""
16
+
17
+ def __init__(self, server_url: str | None = None, api_key: str | None = None,
18
+ anthropic_key: str | None = None, timeout: float = 300.0):
19
+ config = self._load_config()
20
+ self.server_url = (server_url or config.get("server_url", DEFAULT_SERVER)).rstrip("/")
21
+ self.api_key = api_key or config.get("api_key", "")
22
+ # BYOK: the user's own Anthropic key. Sent only on conduct requests via
23
+ # the X-Anthropic-Key header — never on health/status/faces.
24
+ self.anthropic_key = anthropic_key or config.get("anthropic_key", "")
25
+ self.timeout = timeout
26
+ self._client = httpx.Client(
27
+ base_url=self.server_url,
28
+ timeout=timeout,
29
+ headers=self._headers(),
30
+ )
31
+
32
+ def _headers(self) -> dict:
33
+ h = {"Content-Type": "application/json"}
34
+ if self.api_key:
35
+ h["Authorization"] = f"Bearer {self.api_key}"
36
+ return h
37
+
38
+ @staticmethod
39
+ def _load_config() -> dict:
40
+ if CONFIG_PATH.exists():
41
+ try:
42
+ return json.loads(CONFIG_PATH.read_text())
43
+ except Exception:
44
+ return {}
45
+ return {}
46
+
47
+ @staticmethod
48
+ def save_config(server_url: str, api_key: str = "", anthropic_key: str = ""):
49
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
50
+ CONFIG_PATH.write_text(json.dumps({
51
+ "server_url": server_url,
52
+ "api_key": api_key,
53
+ "anthropic_key": anthropic_key,
54
+ }, indent=2))
55
+
56
+ def _conduct_headers(self) -> dict:
57
+ """Extra headers sent ONLY on conduct requests (BYOK key)."""
58
+ if self.anthropic_key:
59
+ return {"X-Anthropic-Key": self.anthropic_key}
60
+ return {}
61
+
62
+ def health(self) -> dict:
63
+ r = self._client.get("/api/health")
64
+ r.raise_for_status()
65
+ return r.json()
66
+
67
+ def conduct(self, prompt: str, synthesize: bool = True,
68
+ skip_clarification: bool = False, max_revisions: int = 1,
69
+ context: dict[str, str] | None = None) -> dict:
70
+ payload = {
71
+ "prompt": prompt,
72
+ "synthesize": synthesize,
73
+ "skip_clarification": skip_clarification,
74
+ "max_revisions": max_revisions,
75
+ }
76
+ if context:
77
+ payload["context"] = context
78
+ r = self._client.post("/api/conduct", json=payload, headers=self._conduct_headers())
79
+ r.raise_for_status()
80
+ return r.json()
81
+
82
+ def conduct_dry(self, prompt: str, context: dict[str, str] | None = None) -> dict:
83
+ payload = {"prompt": prompt}
84
+ if context:
85
+ payload["context"] = context
86
+ r = self._client.post("/api/conduct/dry", json=payload, headers=self._conduct_headers())
87
+ r.raise_for_status()
88
+ return r.json()
89
+
90
+ def faces(self) -> list[dict]:
91
+ r = self._client.get("/api/faces")
92
+ r.raise_for_status()
93
+ return r.json()
94
+
95
+ def status(self) -> dict:
96
+ r = self._client.get("/api/status")
97
+ r.raise_for_status()
98
+ return r.json()
99
+
100
+ def performances(self, limit: int = 10) -> list[dict]:
101
+ r = self._client.get(f"/api/performances?limit={limit}")
102
+ r.raise_for_status()
103
+ return r.json()
104
+
105
+ def gaps(self) -> dict:
106
+ r = self._client.get("/api/gaps")
107
+ r.raise_for_status()
108
+ return r.json()
wagner_cli/display.py ADDED
@@ -0,0 +1,175 @@
1
+ """Rich console rendering for Wagner CLI."""
2
+
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
7
+ from rich.syntax import Syntax
8
+ from rich.text import Text
9
+ from rich.columns import Columns
10
+ from rich import box
11
+
12
+ console = Console()
13
+
14
+
15
+ def print_banner():
16
+ banner = Text("WAGNER", style="bold yellow")
17
+ console.print(Panel(
18
+ banner,
19
+ subtitle="AI Coding Orchestrator",
20
+ border_style="dim yellow",
21
+ padding=(0, 2),
22
+ ))
23
+
24
+
25
+ def print_status(status: dict):
26
+ table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
27
+ table.add_column("Key", style="dim")
28
+ table.add_column("Value", style="bold")
29
+ table.add_row("Server", status.get("server", ""))
30
+ table.add_row("Faces", str(status.get("total_faces", 0)))
31
+ table.add_row("Performances", str(status.get("total_performances", 0)))
32
+ table.add_row("API", "connected" if status.get("api_connected") else "no key")
33
+ console.print(table)
34
+
35
+
36
+ def print_faces(faces: list[dict]):
37
+ table = Table(title="Specialist Faces", box=box.ROUNDED, border_style="dim")
38
+ table.add_column("Name", style="bold")
39
+ table.add_column("Mode", style="cyan", width=6)
40
+ table.add_column("Examples", justify="right")
41
+ table.add_column("Queries", justify="right")
42
+ table.add_column("Progress", width=12)
43
+
44
+ for f in sorted(faces, key=lambda x: x.get("examples", 0), reverse=True):
45
+ progress = f.get("graduation_progress", "0%")
46
+ bar_pct = min(100, int(progress.replace("%", "") or 0))
47
+ bar_filled = int(bar_pct / 5)
48
+ bar = "[yellow]" + "█" * bar_filled + "[dim]" + "░" * (20 - bar_filled)
49
+
50
+ table.add_row(
51
+ f.get("name", "?"),
52
+ f.get("mode", "rag").upper(),
53
+ str(f.get("examples", 0)),
54
+ str(f.get("total_queries", 0)),
55
+ bar,
56
+ )
57
+
58
+ console.print(table)
59
+
60
+
61
+ def print_conduct_result(result: dict, show_synthesis: bool = True):
62
+ # Header
63
+ console.print()
64
+ console.print(Panel(
65
+ f'[bold]{result.get("prompt", "")[:100]}[/bold]',
66
+ title="Orchestration Complete",
67
+ border_style="green",
68
+ ))
69
+
70
+ # Stats row
71
+ stats = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
72
+ stats.add_column("", style="dim")
73
+ stats.add_column("", style="bold")
74
+ stats.add_row("Subtasks", str(result.get("subtasks", 0)))
75
+ stats.add_row("Specialists", str(result.get("specialists_used", 0)))
76
+ stats.add_row("Latency", f'{result.get("total_latency_ms", 0):.0f}ms')
77
+ stats.add_row("API calls", str(result.get("total_api_calls", 0)))
78
+
79
+ gaps = result.get("gaps_detected", [])
80
+ if gaps:
81
+ stats.add_row("Gaps", ", ".join(gaps))
82
+
83
+ revised = result.get("revised", False)
84
+ if revised:
85
+ stats.add_row("Revised", "[green]Yes — critic triggered revision[/green]")
86
+
87
+ critique = result.get("critique")
88
+ if critique:
89
+ score = critique.get("score", "?")
90
+ stats.add_row("Critic score", f"{score}/10")
91
+
92
+ console.print(stats)
93
+
94
+ # Specialist outputs
95
+ outputs = result.get("specialist_outputs", [])
96
+ if outputs:
97
+ table = Table(title="Specialists", box=box.SIMPLE_HEAVY, border_style="dim")
98
+ table.add_column("Face", style="bold")
99
+ table.add_column("Task")
100
+ table.add_column("Conf", justify="right", style="cyan")
101
+ table.add_column("ms", justify="right", style="dim")
102
+
103
+ for o in outputs:
104
+ conf = o.get("confidence", 0)
105
+ conf_style = "green" if conf > 0.5 else "yellow" if conf > 0.3 else "red"
106
+ table.add_row(
107
+ o.get("face_name", "?"),
108
+ (o.get("task", "")[:60] + "...") if len(o.get("task", "")) > 60 else o.get("task", ""),
109
+ f"[{conf_style}]{conf:.0%}[/{conf_style}]",
110
+ f'{o.get("latency_ms", 0):.0f}',
111
+ )
112
+ console.print(table)
113
+
114
+ # Clarification
115
+ clarification = result.get("clarification")
116
+ if clarification and clarification.get("needs_clarification"):
117
+ console.print()
118
+ console.print(Panel(
119
+ "\n".join(f" {i+1}. {q}" for i, q in enumerate(clarification.get("questions", []))),
120
+ title="[yellow]Clarification Needed[/yellow]",
121
+ border_style="yellow",
122
+ ))
123
+ return
124
+
125
+ # Synthesis
126
+ if show_synthesis and result.get("synthesis"):
127
+ console.print()
128
+ console.print("[dim]─── Synthesis ───[/dim]")
129
+ console.print(result["synthesis"])
130
+
131
+
132
+ def print_files_written(paths: list[str]):
133
+ console.print()
134
+ for p in paths:
135
+ console.print(f" [green]✓[/green] {p}")
136
+ console.print(f"\n [bold]{len(paths)} files written[/bold]")
137
+
138
+
139
+ def print_file_preview(previews: list[dict]):
140
+ table = Table(title="Generated Files", box=box.ROUNDED, border_style="dim")
141
+ table.add_column("File", style="bold")
142
+ table.add_column("Lines", justify="right")
143
+ table.add_column("Size", justify="right", style="dim")
144
+
145
+ for p in previews:
146
+ size = f'{p["chars"]}' if p["chars"] < 1024 else f'{p["chars"]/1024:.1f}K'
147
+ table.add_row(p["path"], str(p["lines"]), size)
148
+
149
+ console.print(table)
150
+
151
+
152
+ def spinner(message: str = "Orchestrating..."):
153
+ return Progress(
154
+ SpinnerColumn(style="yellow"),
155
+ TextColumn("[bold]{task.description}"),
156
+ TimeElapsedColumn(),
157
+ console=console,
158
+ transient=True,
159
+ )
160
+
161
+
162
+ def print_error(msg: str):
163
+ console.print(f"[red]Error:[/red] {msg}")
164
+
165
+
166
+ def print_clarification_prompt(questions: list[str]) -> list[str]:
167
+ """Display clarification questions and collect answers."""
168
+ console.print()
169
+ console.print("[yellow]Wagner needs clarification before proceeding:[/yellow]")
170
+ answers = []
171
+ for i, q in enumerate(questions):
172
+ console.print(f"\n [bold]{i+1}.[/bold] {q}")
173
+ answer = console.input(" [dim]→[/dim] ")
174
+ answers.append(answer)
175
+ return answers
wagner_cli/main.py ADDED
@@ -0,0 +1,302 @@
1
+ """Wagner CLI — main entry point."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+ import httpx
8
+
9
+ from wagner_cli import __version__
10
+ from wagner_cli.client import WagnerClient, DEFAULT_SERVER
11
+ from wagner_cli.writer import parse_files, write_files, preview_files
12
+ from wagner_cli.display import (
13
+ console, print_banner, print_status, print_faces, print_conduct_result,
14
+ print_files_written, print_file_preview, print_error, spinner,
15
+ print_clarification_prompt,
16
+ )
17
+
18
+
19
+ def _get_client(server: str | None = None) -> WagnerClient:
20
+ try:
21
+ return WagnerClient(server_url=server)
22
+ except Exception as e:
23
+ print_error(f"Cannot create client: {e}")
24
+ sys.exit(1)
25
+
26
+
27
+ def _check_health(client: WagnerClient):
28
+ try:
29
+ client.health()
30
+ except httpx.ConnectError:
31
+ print_error(f"Cannot connect to {client.server_url}")
32
+ console.print("[dim]Run 'wagner config <url>' to set your server[/dim]")
33
+ sys.exit(1)
34
+ except Exception as e:
35
+ print_error(f"Server error: {e}")
36
+ sys.exit(1)
37
+
38
+
39
+ @click.group()
40
+ @click.version_option(__version__, "-v", "--version")
41
+ def cli():
42
+ """Wagner — AI coding orchestrator with self-improving specialists.
43
+
44
+ \b
45
+ Quick start:
46
+ wagner config https://wagner.razzyshmazzy.com
47
+ wagner run "build a REST API with auth"
48
+ wagner run "build a CLI tool" --write
49
+ """
50
+ pass
51
+
52
+
53
+ @cli.command()
54
+ @click.argument("prompt", nargs=-1, required=True)
55
+ @click.option("--server", "-s", default=None, help="Wagner server URL")
56
+ @click.option("--write", "-w", is_flag=True, help="Write generated files to disk")
57
+ @click.option("--output", "-o", default=".", help="Output directory for generated files")
58
+ @click.option("--overwrite", is_flag=True, help="Overwrite existing files")
59
+ @click.option("--dry", is_flag=True, help="Dry run — decompose without synthesis")
60
+ @click.option("--no-clarify", is_flag=True, help="Skip clarification step")
61
+ @click.option("--no-critic", is_flag=True, help="Skip critic/revision step")
62
+ @click.option("--context", "-c", multiple=True, help="Source files to include as context (repeatable)")
63
+ def run(prompt, server, write, output, overwrite, dry, no_clarify, no_critic, context):
64
+ """Run Wagner orchestration on a coding task.
65
+
66
+ \b
67
+ Examples:
68
+ wagner run "build a FastAPI app with auth"
69
+ wagner run "add pagination" --context app/routes.py app/models.py --write
70
+ wagner run "build a chat app" --dry
71
+ """
72
+ prompt_text = " ".join(prompt)
73
+ client = _get_client(server)
74
+ _check_health(client)
75
+
76
+ print_banner()
77
+
78
+ # Read context files from disk
79
+ context_dict = None
80
+ if context:
81
+ context_dict = {}
82
+ for filepath in context:
83
+ p = Path(filepath)
84
+ if not p.exists():
85
+ print_error(f"Context file not found: {filepath}")
86
+ sys.exit(1)
87
+ try:
88
+ context_dict[str(p)] = p.read_text(encoding="utf-8", errors="ignore")
89
+ console.print(f" [dim]context:[/dim] {p} ({len(p.read_text())} chars)")
90
+ except Exception as e:
91
+ print_error(f"Cannot read {filepath}: {e}")
92
+ sys.exit(1)
93
+ console.print()
94
+
95
+ if dry:
96
+ with spinner() as progress:
97
+ progress.add_task("Decomposing...", total=None)
98
+ result = client.conduct_dry(prompt_text, context=context_dict)
99
+ print_conduct_result(result, show_synthesis=False)
100
+ return
101
+
102
+ # Full conduct
103
+ with spinner() as progress:
104
+ progress.add_task("Orchestrating...", total=None)
105
+ result = client.conduct(
106
+ prompt_text,
107
+ synthesize=True,
108
+ skip_clarification=no_clarify,
109
+ max_revisions=0 if no_critic else 1,
110
+ context=context_dict,
111
+ )
112
+
113
+ # Handle clarification
114
+ clarification = result.get("clarification")
115
+ if clarification and clarification.get("needs_clarification"):
116
+ questions = clarification.get("questions", [])
117
+ answers = print_clarification_prompt(questions)
118
+
119
+ # Re-submit with answers
120
+ augmented = prompt_text + "\n\nClarifications:\n"
121
+ for q, a in zip(questions, answers):
122
+ augmented += f"- {q}: {a}\n"
123
+
124
+ with spinner() as progress:
125
+ progress.add_task("Orchestrating with clarifications...", total=None)
126
+ result = client.conduct(
127
+ augmented,
128
+ synthesize=True,
129
+ skip_clarification=True,
130
+ max_revisions=0 if no_critic else 1,
131
+ context=context_dict,
132
+ )
133
+
134
+ print_conduct_result(result)
135
+
136
+ # Parse and optionally write files
137
+ synthesis = result.get("synthesis", "")
138
+ if synthesis:
139
+ files = parse_files(synthesis)
140
+ if files:
141
+ previews = preview_files(files)
142
+ print_file_preview(previews)
143
+
144
+ if write:
145
+ written = write_files(files, Path(output), overwrite=overwrite)
146
+ print_files_written(written)
147
+ else:
148
+ console.print("\n[dim]Use --write to save files to disk[/dim]")
149
+
150
+
151
+ @cli.command()
152
+ @click.option("--server", "-s", default=None, help="Wagner server URL")
153
+ def status(server):
154
+ """Show Wagner server status."""
155
+ try:
156
+ client = _get_client(server)
157
+ data = client.status()
158
+ health = client.health()
159
+ data["server"] = client.server_url
160
+ data["api_connected"] = health.get("api_connected", False)
161
+ print_banner()
162
+ print_status(data)
163
+ except httpx.ConnectError:
164
+ print_error("Cannot connect to server")
165
+ except Exception as e:
166
+ print_error(str(e))
167
+
168
+
169
+ @cli.command()
170
+ @click.option("--server", "-s", default=None, help="Wagner server URL")
171
+ def faces(server):
172
+ """List all specialist faces."""
173
+ try:
174
+ client = _get_client(server)
175
+ data = client.faces()
176
+ print_banner()
177
+ print_faces(data)
178
+ except httpx.ConnectError:
179
+ print_error("Cannot connect to server")
180
+ except Exception as e:
181
+ print_error(str(e))
182
+
183
+
184
+ @cli.command()
185
+ @click.option("--server", "-s", default=None, help="Wagner server URL")
186
+ @click.option("--limit", "-n", default=10, help="Number of recent performances")
187
+ def history(server, limit):
188
+ """Show recent orchestration history."""
189
+ try:
190
+ client = _get_client(server)
191
+ data = client.performances(limit)
192
+ print_banner()
193
+
194
+ from rich.table import Table
195
+ from rich import box
196
+
197
+ table = Table(title=f"Recent Performances (last {limit})", box=box.ROUNDED, border_style="dim")
198
+ table.add_column("Prompt", style="bold", max_width=50)
199
+ table.add_column("Tasks", justify="right")
200
+ table.add_column("Specs", justify="right")
201
+ table.add_column("Gaps", justify="right")
202
+ table.add_column("Latency", justify="right", style="dim")
203
+ table.add_column("Time", style="dim")
204
+
205
+ for p in data:
206
+ table.add_row(
207
+ p.get("prompt", "")[:50],
208
+ str(p.get("subtasks", 0)),
209
+ str(p.get("specialists", 0)),
210
+ str(p.get("gaps", 0)),
211
+ f'{p.get("latency_ms", 0):.0f}ms',
212
+ p.get("timestamp", "")[:16],
213
+ )
214
+
215
+ console.print(table)
216
+ except httpx.ConnectError:
217
+ print_error("Cannot connect to server")
218
+ except Exception as e:
219
+ print_error(str(e))
220
+
221
+
222
+ @cli.command()
223
+ @click.option("--server", "-s", default=None, help="Wagner server URL")
224
+ def gaps(server):
225
+ """Show specialist gaps detected during orchestration."""
226
+ try:
227
+ client = _get_client(server)
228
+ data = client.gaps()
229
+ print_banner()
230
+
231
+ if not data:
232
+ console.print("[dim]No gaps detected yet. Run more prompts.[/dim]")
233
+ return
234
+
235
+ from rich.table import Table
236
+ from rich import box
237
+
238
+ table = Table(title="Specialist Gaps", box=box.ROUNDED, border_style="dim")
239
+ table.add_column("Missing Specialist", style="bold")
240
+ table.add_column("Requests", justify="right", style="yellow")
241
+
242
+ for name, count in sorted(data.items(), key=lambda x: -x[1]):
243
+ table.add_row(name, str(count))
244
+
245
+ console.print(table)
246
+ except Exception as e:
247
+ print_error(str(e))
248
+
249
+
250
+ @cli.command()
251
+ @click.argument("api_key", required=False)
252
+ @click.option("--server", "-s", default=None, help="Wagner server URL")
253
+ @click.option("--anthropic-key", default=None,
254
+ help="Your own Anthropic API key (BYOK). Sent per-request, never stored server-side.")
255
+ def config(api_key, server, anthropic_key):
256
+ """Configure Wagner.
257
+
258
+ \b
259
+ Bring Your Own Key (BYOK) — Wagner runs all LLM calls on your Anthropic key:
260
+ wagner config --anthropic-key sk-ant-your-key-here
261
+
262
+ \b
263
+ Combined with a custom server:
264
+ wagner config --anthropic-key sk-ant-xxx --server https://wagner.razzyshmazzy.com
265
+
266
+ \b
267
+ Set the Wagner access key (positional, optional):
268
+ wagner config wgn_your_api_key_here
269
+
270
+ Any flag you omit keeps its current value.
271
+ """
272
+ # Merge with existing config so a partial update never clears other fields.
273
+ existing = WagnerClient._load_config()
274
+ new_server = server or existing.get("server_url") or DEFAULT_SERVER
275
+ new_api_key = api_key if api_key is not None else existing.get("api_key", "")
276
+ new_anthropic = anthropic_key if anthropic_key is not None else existing.get("anthropic_key", "")
277
+
278
+ WagnerClient.save_config(new_server, new_api_key, new_anthropic)
279
+
280
+ def _mask(s: str) -> str:
281
+ if not s:
282
+ return "(none)"
283
+ return f"{s[:8]}{'*' * max(0, len(s) - 8)}"
284
+
285
+ console.print(f"[green]✓[/green] Configured Wagner")
286
+ console.print(f" Server: {new_server}")
287
+ console.print(f" Access key: {_mask(new_api_key)}")
288
+ console.print(f" Anthropic key (BYOK): {_mask(new_anthropic)}")
289
+
290
+ # Test connection
291
+ try:
292
+ client = WagnerClient(server_url=new_server, api_key=new_api_key,
293
+ anthropic_key=new_anthropic)
294
+ health = client.health()
295
+ byok = " · byok supported" if health.get("byok") else ""
296
+ console.print(f" Status: [green]connected[/green] ({health.get('faces', 0)} faces{byok})")
297
+ except Exception as e:
298
+ console.print(f" Status: [red]cannot connect[/red] ({e})")
299
+
300
+
301
+ if __name__ == "__main__":
302
+ cli()
wagner_cli/writer.py ADDED
@@ -0,0 +1,65 @@
1
+ """Parse Wagner synthesis output into files and write to disk."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ def parse_files(synthesis: str) -> list[dict]:
9
+ """Parse ### FILE: markers from Wagner synthesis into file entries."""
10
+ if not synthesis:
11
+ return []
12
+
13
+ files = []
14
+ # Match ### FILE: path/to/file.ext followed by a code block
15
+ pattern = r'###\s*FILE:\s*(.+?)\n\s*```\w*\n(.*?)```'
16
+ matches = re.finditer(pattern, synthesis, re.DOTALL)
17
+
18
+ for match in matches:
19
+ filepath = match.group(1).strip()
20
+ code = match.group(2).strip()
21
+ if filepath and code:
22
+ files.append({"path": filepath, "code": code})
23
+
24
+ # Fallback: if no ### FILE markers, try to extract any code blocks
25
+ if not files:
26
+ code_blocks = re.findall(r'```\w*\n(.*?)```', synthesis, re.DOTALL)
27
+ for i, block in enumerate(code_blocks):
28
+ block = block.strip()
29
+ if len(block) > 50:
30
+ files.append({"path": f"output_{i}.txt", "code": block})
31
+
32
+ return files
33
+
34
+
35
+ def write_files(files: list[dict], output_dir: Path, overwrite: bool = False) -> list[str]:
36
+ """Write parsed files to disk. Returns list of written paths."""
37
+ written = []
38
+ output_dir = Path(output_dir)
39
+
40
+ for entry in files:
41
+ filepath = output_dir / entry["path"]
42
+
43
+ if filepath.exists() and not overwrite:
44
+ # Add .wagner suffix to avoid clobbering
45
+ filepath = filepath.with_suffix(filepath.suffix + ".wagner")
46
+
47
+ filepath.parent.mkdir(parents=True, exist_ok=True)
48
+ filepath.write_text(entry["code"] + "\n", encoding="utf-8")
49
+ written.append(str(filepath))
50
+
51
+ return written
52
+
53
+
54
+ def preview_files(files: list[dict], max_lines: int = 5) -> list[dict]:
55
+ """Return a preview of each file (first N lines + total line count)."""
56
+ previews = []
57
+ for entry in files:
58
+ lines = entry["code"].split("\n")
59
+ previews.append({
60
+ "path": entry["path"],
61
+ "lines": len(lines),
62
+ "chars": len(entry["code"]),
63
+ "preview": "\n".join(lines[:max_lines]),
64
+ })
65
+ return previews
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: wagner-cli
3
+ Version: 0.1.0
4
+ Summary: AI coding orchestrator with self-improving specialist agents
5
+ Author-email: razzyshmazzy <razzyshmazzy@gmail.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://wagner.razzyshmazzy.com
8
+ Project-URL: Dashboard, https://wagner.razzyshmazzy.com/api/health
9
+ Keywords: ai,coding,orchestration,agents,anthropic,claude,cli
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Code Generators
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.25.0
23
+ Requires-Dist: rich>=13.0.0
24
+ Requires-Dist: click>=8.1.0
25
+
26
+ # Wagner CLI
27
+
28
+ AI coding orchestrator with self-improving specialist agents.
29
+
30
+ Wagner decomposes complex coding tasks across specialist agents, each trained on
31
+ domain-specific patterns. The more you use it, the smarter it gets.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install wagner-cli
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ Wagner runs on a **bring-your-own-key** model: orchestration (decomposition,
42
+ routing, specialist faces) runs on the Wagner server, while every model call
43
+ runs on **your** Anthropic key. Your key is sent per request and is never
44
+ stored or logged server-side.
45
+
46
+ ```bash
47
+ # 1. Point Wagner at your own Anthropic key (stored in ~/.wagner/config.json)
48
+ wagner config --anthropic-key sk-ant-...
49
+
50
+ # 2. Orchestrate a coding task
51
+ wagner run "build a REST API with JWT auth and PostgreSQL"
52
+
53
+ # 3. Write the generated files to disk
54
+ wagner run "build a CLI tool that watches files" --write
55
+ ```
56
+
57
+ More examples:
58
+
59
+ ```bash
60
+ # Dry run — see the decomposition without synthesis (no synthesis cost)
61
+ wagner run "build a chat app" --dry
62
+
63
+ # Skip clarification / critic passes
64
+ wagner run "build a FastAPI CRUD app" --no-clarify --no-critic
65
+
66
+ # Edit an existing codebase — pass relevant files as context
67
+ wagner run "add pagination to the list endpoint" \
68
+ --context app/routes.py app/models.py --write
69
+ ```
70
+
71
+ ## Commands
72
+
73
+ ```
74
+ wagner run "prompt" Orchestrate a coding task
75
+ wagner config Configure your Anthropic key / server
76
+ wagner status Show server status and face count
77
+ wagner faces List all specialist faces
78
+ wagner history Show recent orchestrations
79
+ wagner gaps Show missing specialist domains
80
+ ```
81
+
82
+ ## `wagner run` options
83
+
84
+ ```
85
+ --write, -w Write generated files to the output directory
86
+ --output, -o DIR Output directory (default: current directory)
87
+ --overwrite Overwrite existing files
88
+ --dry Decompose only, no synthesis
89
+ --no-clarify Skip clarification questions
90
+ --no-critic Skip the critic/revision pass
91
+ --context, -c PATH Source file to include as context (repeatable)
92
+ --server, -s URL Override the server for this command
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ ```bash
98
+ # Set your Anthropic key (BYOK)
99
+ wagner config --anthropic-key sk-ant-...
100
+
101
+ # Use a custom server
102
+ wagner config --anthropic-key sk-ant-... --server https://wagner.razzyshmazzy.com
103
+ ```
104
+
105
+ Any flag you omit keeps its current value. Config lives in `~/.wagner/config.json`.
106
+
107
+ ## How It Works
108
+
109
+ 1. You type a prompt
110
+ 2. Wagner's **Clarifier** checks if the prompt is ambiguous
111
+ 3. The **Decomposer** breaks it into specialist subtasks
112
+ 4. Each subtask is **dispatched** to the best specialist Face
113
+ 5. The **Synthesizer** combines outputs into structured files
114
+ 6. The **Critic** reviews and triggers revision if needed
115
+ 7. Files are written to disk with `--write`
116
+
117
+ Every prompt improves the system. Faces accumulate examples and get smarter over time.
@@ -0,0 +1,10 @@
1
+ wagner_cli/__init__.py,sha256=OVjw4OW8kgzWvH1ZJarJsv7_0LWxcHmqyIAxcu9LaoE,100
2
+ wagner_cli/client.py,sha256=9F0etR3OJ5nc-aXiiU_NSqxR_8S8r0sou2wfWJH5-W4,3653
3
+ wagner_cli/display.py,sha256=iNGuGbFtFReRE9FnoM9_8PXn3aC19rWbK3SuG4LSHCo,6005
4
+ wagner_cli/main.py,sha256=0bHJcWg86rkEx83FGgKf7DO8PzABJbhKKY2kh2xjzp8,10402
5
+ wagner_cli/writer.py,sha256=N_OlA3SZ3m2GDqR3pD3UDxQzOEVUyLbolwDXSaskTL4,2155
6
+ wagner_cli-0.1.0.dist-info/METADATA,sha256=uvHhl13s3dCZmOiS5kJgZhE1JQBgsCXPJp5aoEhale0,3877
7
+ wagner_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ wagner_cli-0.1.0.dist-info/entry_points.txt,sha256=OePlIrKbh2sVquH6a3CTKklxilbjMNKHKvx_XBD0RIo,47
9
+ wagner_cli-0.1.0.dist-info/top_level.txt,sha256=S6-2BghhyM95Ing387bkRBFEUCR6g1mJ7VRfeuzWE3Q,11
10
+ wagner_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wagner = wagner_cli.main:cli
@@ -0,0 +1 @@
1
+ wagner_cli