tarang 4.4.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.
tarang/ws/handlers.py ADDED
@@ -0,0 +1,590 @@
1
+ """
2
+ Message Handlers for WebSocket Events.
3
+
4
+ Handles different event types from the backend:
5
+ - UI updates (thinking, progress, milestones)
6
+ - Tool requests and approvals
7
+ - Completion and errors
8
+
9
+ Integrates with Rich console for beautiful output.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Callable, Dict, List, Optional
16
+
17
+ from rich.console import Console, Group
18
+ from rich.live import Live
19
+ from rich.panel import Panel
20
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskID, TimeElapsedColumn
21
+ from rich.table import Table
22
+ from rich.syntax import Syntax
23
+
24
+ from tarang.ws.client import EventType, WSEvent
25
+ from tarang.ws.executor import ToolExecutor
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ @dataclass
31
+ class ExecutionState:
32
+ """Tracks execution state for UI."""
33
+ current_phase: int = 0
34
+ total_phases: int = 0
35
+ phase_name: str = ""
36
+ milestones: List[str] = field(default_factory=list)
37
+ completed_milestones: List[str] = field(default_factory=list)
38
+ in_progress_milestone: str = ""
39
+ files_changed: List[str] = field(default_factory=list)
40
+ error: Optional[str] = None
41
+ job_id: Optional[str] = None
42
+ thinking_message: str = ""
43
+
44
+
45
+ # Type for approval UI callback
46
+ ApprovalUICallback = Callable[[str, str, Dict[str, Any]], bool]
47
+
48
+
49
+ class MessageHandlers:
50
+ """
51
+ Handles WebSocket messages and updates UI.
52
+
53
+ Usage:
54
+ handlers = MessageHandlers(
55
+ console=console,
56
+ executor=executor,
57
+ on_approval=lambda tool, desc, args: ui.confirm(desc),
58
+ )
59
+
60
+ async for event in ws_client.execute(instruction, cwd):
61
+ await handlers.handle(event, ws_client)
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ console: Console,
67
+ executor: ToolExecutor,
68
+ on_approval: Optional[ApprovalUICallback] = None,
69
+ verbose: bool = False,
70
+ auto_approve: bool = False,
71
+ ):
72
+ self.console = console
73
+ self.executor = executor
74
+ self.on_approval = on_approval
75
+ self.verbose = verbose
76
+ self.auto_approve = auto_approve
77
+
78
+ self.state = ExecutionState()
79
+ self._progress: Optional[Progress] = None
80
+ self._phase_task_id: Optional[TaskID] = None
81
+ self._milestone_task_id: Optional[TaskID] = None
82
+ self._live: Optional[Live] = None
83
+
84
+ def _create_progress_display(self) -> Progress:
85
+ """Create a progress display with phase and milestone tracking."""
86
+ return Progress(
87
+ SpinnerColumn(),
88
+ TextColumn("[bold blue]{task.fields[phase_name]}[/bold blue]"),
89
+ BarColumn(bar_width=30),
90
+ TextColumn("{task.percentage:.0f}%"),
91
+ TextColumn("[dim]{task.fields[milestone]}[/dim]"),
92
+ TimeElapsedColumn(),
93
+ console=self.console,
94
+ transient=False,
95
+ )
96
+
97
+ def _build_status_panel(self) -> Panel:
98
+ """Build a status panel showing current progress."""
99
+ if not self.state.phase_name:
100
+ return Panel(
101
+ f"[dim cyan]{self.state.thinking_message or 'Initializing...'}[/dim cyan]",
102
+ title="[bold] Status[/bold]",
103
+ border_style="blue",
104
+ )
105
+
106
+ # Build milestone list with checkboxes
107
+ milestone_lines = []
108
+ for m in self.state.milestones:
109
+ if m in self.state.completed_milestones:
110
+ milestone_lines.append(f" [green][/green] {m}")
111
+ elif m == self.state.in_progress_milestone:
112
+ milestone_lines.append(f" [yellow][/yellow] {m}...")
113
+ else:
114
+ milestone_lines.append(f" [dim][ ][/dim] {m}")
115
+
116
+ phase_progress = f"Phase {self.state.current_phase}/{self.state.total_phases}"
117
+ completed = len(self.state.completed_milestones)
118
+ total = len(self.state.milestones)
119
+
120
+ content = f"[bold]{self.state.phase_name}[/bold] ({phase_progress})\n"
121
+ content += "\n".join(milestone_lines) if milestone_lines else ""
122
+
123
+ if self.state.files_changed:
124
+ content += f"\n\n[dim]Files: {len(self.state.files_changed)}[/dim]"
125
+
126
+ return Panel(
127
+ content,
128
+ title=f"[bold blue] {self.state.phase_name}[/bold blue]",
129
+ border_style="blue",
130
+ subtitle=f"[dim]{completed}/{total} milestones[/dim]",
131
+ )
132
+
133
+ async def handle(self, event: WSEvent, ws_client) -> bool:
134
+ """
135
+ Handle a WebSocket event.
136
+
137
+ Args:
138
+ event: The event to handle
139
+ ws_client: WebSocket client for sending responses
140
+
141
+ Returns:
142
+ True if execution should continue, False to stop
143
+ """
144
+ handler = getattr(self, f"_handle_{event.type.value}", None)
145
+
146
+ if handler:
147
+ return await handler(event, ws_client)
148
+ else:
149
+ if self.verbose:
150
+ logger.debug(f"Unhandled event type: {event.type}")
151
+ return True
152
+
153
+ async def _handle_connected(self, event: WSEvent, ws_client) -> bool:
154
+ """Handle connection established."""
155
+ session_id = event.data.get("session_id", "")
156
+ if self.verbose:
157
+ self.console.print(f"[dim]Connected: {session_id}[/dim]")
158
+ return True
159
+
160
+ async def _handle_thinking(self, event: WSEvent, ws_client) -> bool:
161
+ """Handle thinking/processing status."""
162
+ message = event.data.get("message", "Thinking...")
163
+ self.state.thinking_message = message
164
+ self.console.print(f"[dim cyan]{message}[/dim cyan]")
165
+ return True
166
+
167
+ async def _handle_phase_start(self, event: WSEvent, ws_client) -> bool:
168
+ """Handle new phase starting."""
169
+ phase = event.data.get("phase", 0)
170
+ total = event.data.get("total_phases", 1)
171
+ name = event.data.get("name", "")
172
+ milestones = event.data.get("milestones", [])
173
+
174
+ self.state.current_phase = phase
175
+ self.state.total_phases = total
176
+ self.state.phase_name = name
177
+ self.state.milestones = milestones
178
+ self.state.completed_milestones = []
179
+ self.state.in_progress_milestone = ""
180
+
181
+ # Calculate overall progress
182
+ phases_done = phase - 1
183
+ progress_percent = int((phases_done / total) * 100) if total > 0 else 0
184
+
185
+ self.console.print()
186
+
187
+ # Draw progress bar
188
+ bar_width = 30
189
+ filled = int(bar_width * phases_done / total) if total > 0 else 0
190
+ bar = "" * filled + "" * (bar_width - filled)
191
+
192
+ self.console.print(
193
+ f"[bold blue]Phase {phase}/{total}[/bold blue] [dim]{bar}[/dim] {progress_percent}%"
194
+ )
195
+ self.console.print(
196
+ Panel(
197
+ f"[bold]{name}[/bold]",
198
+ border_style="blue",
199
+ )
200
+ )
201
+
202
+ if milestones:
203
+ for m in milestones:
204
+ self.console.print(f" [dim][ ][/dim] {m}")
205
+
206
+ return True
207
+
208
+ async def _handle_milestone_update(self, event: WSEvent, ws_client) -> bool:
209
+ """Handle milestone status change."""
210
+ milestone = event.data.get("milestone", "")
211
+ status = event.data.get("status", "")
212
+
213
+ if status == "completed":
214
+ if milestone not in self.state.completed_milestones:
215
+ self.state.completed_milestones.append(milestone)
216
+ if self.state.in_progress_milestone == milestone:
217
+ self.state.in_progress_milestone = ""
218
+ self.console.print(f" [green][/green] {milestone}")
219
+ elif status == "in_progress":
220
+ self.state.in_progress_milestone = milestone
221
+ self.console.print(f" [yellow][/yellow] {milestone}...")
222
+ elif status == "failed":
223
+ self.state.in_progress_milestone = ""
224
+ self.console.print(f" [red][/red] {milestone}")
225
+
226
+ return True
227
+
228
+ async def _handle_progress(self, event: WSEvent, ws_client) -> bool:
229
+ """Handle progress update."""
230
+ percent = event.data.get("percent", 0)
231
+ message = event.data.get("message", "")
232
+ phase = event.data.get("phase", 0)
233
+ total = event.data.get("total_phases", 1)
234
+
235
+ if self.verbose:
236
+ self.console.print(
237
+ f"[dim]Progress: {percent}% - {message}[/dim]"
238
+ )
239
+
240
+ return True
241
+
242
+ async def _handle_tool_request(self, event: WSEvent, ws_client) -> bool:
243
+ """Handle tool execution request from backend."""
244
+ request_id = event.request_id or event.data.get("request_id", "")
245
+ tool = event.data.get("tool", "")
246
+ args = event.data.get("args", {})
247
+
248
+ # Show tool call with icon
249
+ tool_icons = {
250
+ "read_file": "",
251
+ "list_files": "",
252
+ "search_files": "",
253
+ "write_file": "",
254
+ "edit_file": "",
255
+ "delete_file": "",
256
+ "shell": "",
257
+ "get_file_info": "",
258
+ }
259
+ icon = tool_icons.get(tool, "")
260
+
261
+ # Build display info
262
+ if tool == "read_file":
263
+ display = f"{args.get('file_path', '')}"
264
+ elif tool == "list_files":
265
+ path = args.get('path', '.')
266
+ pattern = args.get('pattern', '')
267
+ display = f"{path}" + (f" ({pattern})" if pattern else "")
268
+ elif tool == "search_files":
269
+ display = f"'{args.get('pattern', '')}'"
270
+ elif tool == "write_file":
271
+ display = f"{args.get('file_path', '')}"
272
+ elif tool == "edit_file":
273
+ display = f"{args.get('file_path', '')}"
274
+ elif tool == "shell":
275
+ cmd = args.get('command', '')[:50]
276
+ display = f"`{cmd}`"
277
+ else:
278
+ display = ""
279
+
280
+ self.console.print(f" [dim cyan]{icon} {tool}[/dim cyan] {display}")
281
+
282
+ try:
283
+ # Execute tool locally
284
+ result = await self.executor.execute(tool, args)
285
+
286
+ # Send result back
287
+ await ws_client.send_tool_result(request_id, result)
288
+
289
+ # Track file changes
290
+ if tool in ("write_file", "edit_file") and result.get("success"):
291
+ file_path = result.get("file_path", args.get("file_path", ""))
292
+ if file_path and file_path not in self.state.files_changed:
293
+ self.state.files_changed.append(file_path)
294
+
295
+ # Show result summary for verbose mode
296
+ if self.verbose:
297
+ if result.get("error"):
298
+ self.console.print(f" [red]Error: {result['error']}[/red]")
299
+ elif tool == "read_file":
300
+ lines = result.get("lines_returned", 0)
301
+ self.console.print(f" [dim]Read {lines} lines[/dim]")
302
+ elif tool == "list_files":
303
+ count = len(result.get("files", []))
304
+ self.console.print(f" [dim]Found {count} files[/dim]")
305
+ elif tool == "search_files":
306
+ count = result.get("total_matches", 0)
307
+ self.console.print(f" [dim]Found {count} matches[/dim]")
308
+
309
+ except Exception as e:
310
+ logger.exception(f"Tool execution error: {tool}")
311
+ self.console.print(f" [red]Error: {e}[/red]")
312
+ await ws_client.send_tool_error(request_id, str(e))
313
+
314
+ return True
315
+
316
+ async def _handle_approval_request(self, event: WSEvent, ws_client) -> bool:
317
+ """Handle approval request for destructive operations."""
318
+ request_id = event.request_id or event.data.get("request_id", "")
319
+ tool = event.data.get("tool", "")
320
+ args = event.data.get("args", {})
321
+ description = event.data.get("description", "")
322
+
323
+ # Show what's being requested
324
+ self._show_approval_request(tool, args, description)
325
+
326
+ # Auto-approve if flag is set
327
+ if self.auto_approve:
328
+ approved = True
329
+ self.console.print("[dim green]Auto-approved[/dim green]")
330
+ elif self.on_approval:
331
+ approved = self.on_approval(tool, description, args)
332
+ else:
333
+ # Default: ask via console
334
+ self.console.print("[yellow]Approve this operation?[/yellow] (y/n): ", end="")
335
+ response = input().strip().lower()
336
+ approved = response in ("y", "yes")
337
+
338
+ if approved:
339
+ # Execute and send result
340
+ try:
341
+ result = await self.executor.execute(tool, args)
342
+ await ws_client.send_tool_result(request_id, result)
343
+
344
+ # Track file changes
345
+ if tool in ("write_file", "edit_file") and result.get("success"):
346
+ file_path = result.get("file_path", args.get("file_path", ""))
347
+ if file_path and file_path not in self.state.files_changed:
348
+ self.state.files_changed.append(file_path)
349
+ self.console.print(f" [green] Applied: {file_path}[/green]")
350
+
351
+ except Exception as e:
352
+ await ws_client.send_tool_error(request_id, str(e))
353
+ else:
354
+ # Send rejection
355
+ await ws_client.send_approval(request_id, False)
356
+ self.console.print(" [yellow] Skipped[/yellow]")
357
+
358
+ return True
359
+
360
+ def _get_language_from_path(self, file_path: str) -> str:
361
+ """Detect language from file extension for syntax highlighting."""
362
+ ext_map = {
363
+ ".py": "python",
364
+ ".js": "javascript",
365
+ ".jsx": "jsx",
366
+ ".ts": "typescript",
367
+ ".tsx": "tsx",
368
+ ".json": "json",
369
+ ".yaml": "yaml",
370
+ ".yml": "yaml",
371
+ ".md": "markdown",
372
+ ".html": "html",
373
+ ".css": "css",
374
+ ".scss": "scss",
375
+ ".sql": "sql",
376
+ ".sh": "bash",
377
+ ".bash": "bash",
378
+ ".zsh": "bash",
379
+ ".go": "go",
380
+ ".rs": "rust",
381
+ ".rb": "ruby",
382
+ ".java": "java",
383
+ ".kt": "kotlin",
384
+ ".swift": "swift",
385
+ ".c": "c",
386
+ ".cpp": "cpp",
387
+ ".h": "c",
388
+ ".hpp": "cpp",
389
+ }
390
+ import os
391
+ _, ext = os.path.splitext(file_path)
392
+ return ext_map.get(ext.lower(), "text")
393
+
394
+ def _show_approval_request(
395
+ self,
396
+ tool: str,
397
+ args: Dict[str, Any],
398
+ description: str,
399
+ ):
400
+ """Display approval request with details and syntax highlighting."""
401
+ self.console.print()
402
+
403
+ if tool == "write_file":
404
+ file_path = args.get("file_path", "")
405
+ content = args.get("content", "")
406
+ language = self._get_language_from_path(file_path)
407
+
408
+ self.console.print(f"[bold cyan]╭─ ✏️ Create: {file_path}[/bold cyan]")
409
+ if description:
410
+ self.console.print(f"[bold cyan]│[/bold cyan] [dim]{description}[/dim]")
411
+
412
+ # Show syntax-highlighted preview
413
+ lines = content.split("\n")
414
+ preview_lines = lines[:20]
415
+ preview = "\n".join(preview_lines)
416
+
417
+ try:
418
+ syntax = Syntax(
419
+ preview,
420
+ language,
421
+ theme="monokai",
422
+ line_numbers=True,
423
+ word_wrap=True,
424
+ )
425
+ self.console.print(Panel(
426
+ syntax,
427
+ border_style="green",
428
+ title="[green]+ New File[/green]",
429
+ subtitle=f"[dim]{len(lines)} lines[/dim]" if len(lines) > 20 else None,
430
+ ))
431
+ except Exception:
432
+ # Fallback to simple display
433
+ for line in preview_lines:
434
+ self.console.print(f" [green]+ {line}[/green]")
435
+
436
+ if len(lines) > 20:
437
+ self.console.print(f" [dim]... and {len(lines) - 20} more lines[/dim]")
438
+
439
+ elif tool == "edit_file":
440
+ file_path = args.get("file_path", "")
441
+ search = args.get("search", "")
442
+ replace = args.get("replace", "")
443
+ language = self._get_language_from_path(file_path)
444
+
445
+ self.console.print(f"[bold cyan]╭─ ✏️ Edit: {file_path}[/bold cyan]")
446
+ if description:
447
+ self.console.print(f"[bold cyan]│[/bold cyan] [dim]{description}[/dim]")
448
+
449
+ # Build unified diff display
450
+ search_lines = search.split("\n")
451
+ replace_lines = replace.split("\n")
452
+
453
+ # Show removal
454
+ if search_lines:
455
+ self.console.print("[bold cyan]│[/bold cyan]")
456
+ self.console.print("[bold cyan]│[/bold cyan] [red]Remove:[/red]")
457
+ for line in search_lines[:10]:
458
+ self.console.print(f"[bold cyan]│[/bold cyan] [red]- {line}[/red]")
459
+ if len(search_lines) > 10:
460
+ self.console.print(f"[bold cyan]│[/bold cyan] [dim]... ({len(search_lines)} lines total)[/dim]")
461
+
462
+ # Show addition
463
+ if replace_lines:
464
+ self.console.print("[bold cyan]│[/bold cyan]")
465
+ self.console.print("[bold cyan]│[/bold cyan] [green]Add:[/green]")
466
+ for line in replace_lines[:10]:
467
+ self.console.print(f"[bold cyan]│[/bold cyan] [green]+ {line}[/green]")
468
+ if len(replace_lines) > 10:
469
+ self.console.print(f"[bold cyan]│[/bold cyan] [dim]... ({len(replace_lines)} lines total)[/dim]")
470
+
471
+ self.console.print("[bold cyan]╰─[/bold cyan]")
472
+
473
+ elif tool == "delete_file":
474
+ file_path = args.get("file_path", "")
475
+ self.console.print(f"[bold red]╭─ 🗑️ Delete: {file_path}[/bold red]")
476
+ if description:
477
+ self.console.print(f"[bold red]│[/bold red] [dim]{description}[/dim]")
478
+ self.console.print("[bold red]╰─ This action cannot be undone![/bold red]")
479
+
480
+ elif tool == "shell":
481
+ command = args.get("command", "")
482
+ cwd = args.get("cwd", "")
483
+ timeout = args.get("timeout", 60)
484
+
485
+ self.console.print(f"[bold yellow]╭─ 💻 Shell Command[/bold yellow]")
486
+ if description:
487
+ self.console.print(f"[bold yellow]│[/bold yellow] [dim]{description}[/dim]")
488
+ self.console.print(f"[bold yellow]│[/bold yellow]")
489
+
490
+ try:
491
+ syntax = Syntax(command, "bash", theme="monokai")
492
+ self.console.print(Panel(
493
+ syntax,
494
+ border_style="yellow",
495
+ title="[yellow]Command[/yellow]",
496
+ ))
497
+ except Exception:
498
+ self.console.print(f"[bold yellow]│[/bold yellow] $ {command}")
499
+
500
+ if cwd:
501
+ self.console.print(f"[bold yellow]│[/bold yellow] [dim]Directory: {cwd}[/dim]")
502
+ self.console.print(f"[bold yellow]╰─[/bold yellow] [dim]Timeout: {timeout}s[/dim]")
503
+
504
+ else:
505
+ self.console.print(f"[bold]╭─ {tool}[/bold]")
506
+ if description:
507
+ self.console.print(f"[bold]│[/bold] [dim]{description}[/dim]")
508
+ self.console.print(f"[bold]╰─[/bold]")
509
+
510
+ async def _handle_complete(self, event: WSEvent, ws_client) -> bool:
511
+ """Handle execution completed."""
512
+ summary = event.data.get("summary", "Completed")
513
+ files = event.data.get("files_changed", [])
514
+ phases = event.data.get("phases_completed", 0)
515
+ milestones = event.data.get("milestones_completed", 0)
516
+
517
+ self.console.print()
518
+ self.console.print(
519
+ Panel(
520
+ f"[green]{summary}[/green]\n\n"
521
+ f"[dim]Files changed: {len(files)}[/dim]\n"
522
+ f"[dim]Phases: {phases} | Milestones: {milestones}[/dim]",
523
+ title="[bold green] Complete[/bold green]",
524
+ border_style="green",
525
+ )
526
+ )
527
+
528
+ if files:
529
+ for f in files[:10]:
530
+ self.console.print(f" [dim]{f}[/dim]")
531
+ if len(files) > 10:
532
+ self.console.print(f" [dim]... and {len(files) - 10} more[/dim]")
533
+
534
+ return False # Stop iteration
535
+
536
+ async def _handle_error(self, event: WSEvent, ws_client) -> bool:
537
+ """Handle error event."""
538
+ message = event.data.get("message", "Unknown error")
539
+ recoverable = event.data.get("recoverable", True)
540
+
541
+ self.state.error = message
542
+
543
+ self.console.print()
544
+ self.console.print(
545
+ Panel(
546
+ f"[red]{message}[/red]",
547
+ title="[bold red] Error[/bold red]",
548
+ border_style="red",
549
+ )
550
+ )
551
+
552
+ return False # Stop iteration
553
+
554
+ async def _handle_paused(self, event: WSEvent, ws_client) -> bool:
555
+ """Handle job paused (e.g., disconnect)."""
556
+ job_id = event.data.get("job_id", "")
557
+ resume_cmd = event.data.get("resume_command", "")
558
+ phase = event.data.get("phase", 0)
559
+ milestone = event.data.get("milestone", "")
560
+
561
+ self.console.print()
562
+ self.console.print(
563
+ Panel(
564
+ f"[yellow]Job paused at phase {phase}[/yellow]\n"
565
+ f"[dim]Milestone: {milestone}[/dim]\n\n"
566
+ f"[cyan]Resume with:[/cyan]\n"
567
+ f" {resume_cmd or f'tarang resume {job_id}'}",
568
+ title="[bold yellow] Paused[/bold yellow]",
569
+ border_style="yellow",
570
+ )
571
+ )
572
+
573
+ return False # Stop iteration
574
+
575
+ async def _handle_heartbeat(self, event: WSEvent, ws_client) -> bool:
576
+ """Handle heartbeat - just acknowledge."""
577
+ return True
578
+
579
+ async def _handle_pong(self, event: WSEvent, ws_client) -> bool:
580
+ """Handle pong response to our ping."""
581
+ return True
582
+
583
+ def get_summary(self) -> Dict[str, Any]:
584
+ """Get execution summary."""
585
+ return {
586
+ "files_changed": self.state.files_changed,
587
+ "phases_completed": self.state.current_phase,
588
+ "milestones_completed": len(self.state.completed_milestones),
589
+ "error": self.state.error,
590
+ }
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: tarang
3
+ Version: 4.4.0
4
+ Summary: Tarang - AI Coding Agent (Hybrid WebSocket Architecture)
5
+ Author-email: Tarang Team <hello@devtarang.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://devtarang.ai
8
+ Project-URL: Documentation, https://docs.devtarang.ai
9
+ Project-URL: Repository, https://github.com/tarang-ai/tarang-cli
10
+ Keywords: ai,coding,assistant,llm,developer-tools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
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: click>=8.0.0
23
+ Requires-Dist: httpx>=0.25.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Requires-Dist: python-dotenv>=1.0.0
26
+ Requires-Dist: rich>=13.0.0
27
+ Requires-Dist: websockets>=12.0
28
+ Requires-Dist: prompt_toolkit>=3.0.0
29
+ Requires-Dist: rank-bm25>=0.2.2
30
+ Requires-Dist: tree-sitter>=0.23.0
31
+ Requires-Dist: tree-sitter-python>=0.23.0
32
+ Requires-Dist: tree-sitter-javascript>=0.23.0
33
+ Requires-Dist: tree-sitter-sql>=0.3.0
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
36
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
39
+
40
+ # Tarang CLI
41
+
42
+ AI-powered coding assistant with ManagerAgent architecture.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install tarang
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```bash
53
+ # Start interactive session
54
+ tarang run
55
+
56
+ # Run a single instruction
57
+ tarang run "create a hello world app"
58
+
59
+ # Run and exit
60
+ tarang run "fix linter errors" --once
61
+ ```
62
+
63
+ ## Commands
64
+
65
+ - `tarang run [instruction]` - Start coding session (interactive or single)
66
+ - `tarang init <project>` - Initialize a new project
67
+ - `tarang chat` - Interactive chat mode
68
+ - `tarang status` - Show project status
69
+ - `tarang resume` - Resume interrupted execution
70
+ - `tarang reset` - Reset execution state
71
+ - `tarang clean` - Remove all Tarang state
72
+ - `tarang check` - Verify configuration
73
+
74
+ ## Options
75
+
76
+ - `--project-dir, -p` - Project directory (default: current)
77
+ - `--config, -c` - Agent config (coder, explorer, orchestrator)
78
+ - `--verbose, -v` - Enable verbose output
79
+ - `--once` - Run single instruction and exit
80
+
81
+ ## Configuration
82
+
83
+ Tarang requires an OpenRouter API key:
84
+
85
+ ```bash
86
+ export OPENROUTER_API_KEY=your_key
87
+ ```
88
+
89
+ ## Project State
90
+
91
+ Tarang stores execution state in `.tarang/` directory:
92
+ - `state.json` - Current execution state
93
+ - Supports resume after interruption
94
+
95
+ ## Links
96
+
97
+ - Website: [devtarang.ai](https://devtarang.ai)
98
+ - Documentation: [docs.devtarang.ai](https://docs.devtarang.ai)
99
+
100
+ ## License
101
+
102
+ MIT