tsugite-cli 0.3.3__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 (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/cli/agents.py ADDED
@@ -0,0 +1,148 @@
1
+ """Agent management CLI commands."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+ agents_app = typer.Typer(help="Manage agents and agent inheritance")
11
+
12
+
13
+ @agents_app.command("list")
14
+ def agents_list(
15
+ global_only: bool = typer.Option(False, "--global", help="List only global agents"),
16
+ local_only: bool = typer.Option(False, "--local", help="List only local agents"),
17
+ ):
18
+ """List available agents."""
19
+ from tsugite.agent_inheritance import get_global_agents_paths
20
+ from tsugite.agent_utils import list_local_agents
21
+
22
+ # Show global agents
23
+ if global_only or not local_only:
24
+ console.print("[cyan]Global Agents:[/cyan]\n")
25
+
26
+ found_any = False
27
+ for global_path in get_global_agents_paths():
28
+ if not global_path.exists():
29
+ continue
30
+
31
+ agent_files = sorted(global_path.glob("*.md"))
32
+ if agent_files:
33
+ found_any = True
34
+ console.print(f"[dim]{global_path}[/dim]")
35
+ for agent_file in agent_files:
36
+ console.print(f" • {agent_file.stem}")
37
+ console.print()
38
+
39
+ if not found_any:
40
+ console.print("[yellow]No global agents found[/yellow]")
41
+ console.print("\nGlobal agent locations:")
42
+ for path in get_global_agents_paths():
43
+ console.print(f" {path}")
44
+ console.print()
45
+
46
+ # Show local agents
47
+ if local_only or not global_only:
48
+ console.print("[cyan]Local Agents:[/cyan]\n")
49
+
50
+ local_agents = list_local_agents()
51
+
52
+ if not local_agents:
53
+ console.print("[yellow]No local agents found[/yellow]")
54
+ console.print("\nLocal agent locations:")
55
+ console.print(" • Current directory (*.md)")
56
+ console.print(" • .tsugite/")
57
+ console.print(" • ./agents/")
58
+ else:
59
+ for location, agent_files in local_agents.items():
60
+ console.print(f"[dim]{location}[/dim]")
61
+ for agent_file in agent_files:
62
+ console.print(f" • {agent_file.stem}")
63
+ console.print()
64
+
65
+
66
+ @agents_app.command("show")
67
+ def agents_show(
68
+ agent_path: str = typer.Argument(help="Agent name or path to agent file"),
69
+ show_inheritance: bool = typer.Option(False, "--inheritance", help="Show inheritance chain"),
70
+ ):
71
+ """Show agent information.
72
+
73
+ Can be either a file path (e.g., 'examples/my_agent.md') or
74
+ an agent name to search globally (e.g., 'default', 'builtin-default').
75
+ """
76
+ from tsugite.agent_inheritance import find_agent_file
77
+ from tsugite.md_agents import parse_agent_file
78
+
79
+ try:
80
+ agent_file = Path(agent_path)
81
+
82
+ # If path doesn't exist, try to find it as an agent name
83
+ if not agent_file.exists():
84
+ found_path = find_agent_file(agent_path, Path.cwd())
85
+ if found_path:
86
+ agent_file = found_path
87
+ console.print(f"[dim]Found: {agent_file}[/dim]\n")
88
+ else:
89
+ console.print(f"[red]Agent not found: {agent_path}[/red]")
90
+ console.print("\nSearched in:")
91
+ console.print(" • Package agents")
92
+ console.print(" • Current directory")
93
+ console.print(" • .tsugite/")
94
+ console.print(" • ./agents/")
95
+ console.print(" • Global locations (use 'agents list --global' to see)")
96
+ raise typer.Exit(1)
97
+
98
+ agent = parse_agent_file(agent_file)
99
+ config = agent.config
100
+
101
+ console.print(f"[cyan]Agent:[/cyan] [bold]{config.name}[/bold]\n")
102
+
103
+ if config.description:
104
+ console.print(f"[bold]Description:[/bold] {config.description}\n")
105
+
106
+ if config.model:
107
+ console.print(f"[bold]Model:[/bold] {config.model}")
108
+ else:
109
+ console.print("[bold]Model:[/bold] [dim](uses default)[/dim]")
110
+
111
+ console.print(f"[bold]Max Steps:[/bold] {config.max_turns}")
112
+
113
+ if config.tools:
114
+ console.print(f"\n[bold]Tools ({len(config.tools)}):[/bold]")
115
+ for tool in config.tools:
116
+ console.print(f" • {tool}")
117
+
118
+ if config.extends:
119
+ console.print(f"\n[bold]Extends:[/bold] {config.extends}")
120
+
121
+ if show_inheritance:
122
+ from tsugite.agent_utils import build_inheritance_chain
123
+
124
+ try:
125
+ chain = build_inheritance_chain(agent_file)
126
+ console.print("\n[bold cyan]Inheritance Chain:[/bold cyan]")
127
+
128
+ if len(chain) == 1:
129
+ console.print(" [dim](no inheritance)[/dim]")
130
+ else:
131
+ for i, (agent_name, agent_path) in enumerate(chain):
132
+ is_current = i == len(chain) - 1
133
+ prefix = " └─" if is_current else " ├─"
134
+
135
+ if is_current:
136
+ console.print(f"{prefix} [bold]{agent_name}[/bold] [dim](current)[/dim]")
137
+ else:
138
+ console.print(f"{prefix} {agent_name} [dim]({agent_path.name})[/dim]")
139
+ if i < len(chain) - 1:
140
+ console.print(" │")
141
+ except Exception as e:
142
+ console.print(f"\n[yellow]Could not build inheritance chain: {e}[/yellow]")
143
+
144
+ console.print()
145
+
146
+ except Exception as e:
147
+ console.print(f"[red]Failed to load agent: {e}[/red]")
148
+ raise typer.Exit(1)
@@ -0,0 +1,193 @@
1
+ """Attachment management CLI commands."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+ attachments_app = typer.Typer(help="Manage reusable text attachments")
11
+
12
+
13
+ @attachments_app.command("add")
14
+ def attachments_add(
15
+ alias: str = typer.Argument(help="Unique name for the attachment"),
16
+ source: str = typer.Argument(help="File path, URL, or '-' for stdin"),
17
+ ):
18
+ """Add or update an attachment.
19
+
20
+ For stdin input ('-'), content is stored inline in attachments.json.
21
+ For files and URLs, only the reference is stored (content fetched on demand).
22
+ """
23
+ from tsugite.attachments import add_attachment
24
+
25
+ try:
26
+ if source == "-":
27
+ # Read from stdin - store inline
28
+ import sys
29
+
30
+ content = sys.stdin.read()
31
+ add_attachment(alias, source="inline", content=content)
32
+
33
+ console.print(f"[green]✓ Inline attachment '{alias}' saved[/green]")
34
+ console.print(" Type: Inline text")
35
+ console.print(f" Size: {len(content)} characters")
36
+ else:
37
+ # File or URL reference - validate but don't fetch
38
+ if source.startswith("http://") or source.startswith("https://"):
39
+ # URL reference
40
+ add_attachment(alias, source=source)
41
+ console.print(f"[green]✓ URL attachment '{alias}' saved[/green]")
42
+ console.print(f" Source: {source}")
43
+ console.print(" Type: URL (fetched on demand)")
44
+ else:
45
+ # File reference - validate it exists
46
+ file_path = Path(source).expanduser()
47
+ if not file_path.exists():
48
+ console.print(f"[red]File not found: {source}[/red]")
49
+ raise typer.Exit(1)
50
+
51
+ # Store absolute path for reliability
52
+ absolute_path = str(file_path.resolve())
53
+ add_attachment(alias, source=absolute_path)
54
+
55
+ console.print(f"[green]✓ File attachment '{alias}' saved[/green]")
56
+ console.print(f" Source: {absolute_path}")
57
+ console.print(" Type: File (read on demand)")
58
+
59
+ except Exception as e:
60
+ console.print(f"[red]Failed to add attachment: {e}[/red]")
61
+ raise typer.Exit(1)
62
+
63
+
64
+ @attachments_app.command("list")
65
+ def attachments_list():
66
+ """List all attachments."""
67
+ from rich.table import Table
68
+
69
+ from tsugite.attachments import list_attachments
70
+
71
+ attachments = list_attachments()
72
+
73
+ if not attachments:
74
+ console.print("[yellow]No attachments found[/yellow]")
75
+ console.print("\nAdd an attachment with: [cyan]tsugite attachments add NAME SOURCE[/cyan]")
76
+ return
77
+
78
+ table = Table(title=f"Attachments ({len(attachments)} total)")
79
+ table.add_column("Alias", style="cyan")
80
+ table.add_column("Source", style="dim")
81
+ table.add_column("Size", justify="right")
82
+ table.add_column("Updated", style="dim")
83
+
84
+ for alias, data in sorted(attachments.items()):
85
+ size = len(data.get("content", ""))
86
+ updated = data.get("updated", "unknown")[:10] # Just date part
87
+ table.add_row(alias, data.get("source", "unknown"), f"{size:,}", updated)
88
+
89
+ console.print(table)
90
+
91
+
92
+ @attachments_app.command("show")
93
+ def attachments_show(
94
+ alias: str = typer.Argument(help="Attachment alias to show"),
95
+ content: bool = typer.Option(False, "--content", help="Show full content"),
96
+ ):
97
+ """Show details of an attachment."""
98
+ from rich.panel import Panel
99
+
100
+ from tsugite.attachments import get_attachment
101
+ from tsugite.cache import get_cache_key, list_cache
102
+
103
+ result = get_attachment(alias)
104
+
105
+ if result is None:
106
+ console.print(f"[red]Attachment '{alias}' not found[/red]")
107
+ raise typer.Exit(1)
108
+
109
+ source, stored_content = result
110
+
111
+ # Determine attachment type
112
+ is_inline = source.lower() in ("inline", "text")
113
+
114
+ # Build panel content
115
+ panel_content = f"[cyan]Alias:[/cyan] {alias}\n"
116
+ panel_content += f"[cyan]Type:[/cyan] {'Inline' if is_inline else 'Reference'}\n"
117
+ panel_content += f"[cyan]Source:[/cyan] {source}\n"
118
+
119
+ # Check cache status for references
120
+ if not is_inline:
121
+ cache_entries = list_cache()
122
+ cache_key = get_cache_key(source)
123
+ if cache_key in cache_entries:
124
+ cache_info = cache_entries[cache_key]
125
+ panel_content += f"[cyan]Cached:[/cyan] Yes (size: {cache_info['size']:,} bytes)\n"
126
+ panel_content += f"[cyan]Cached at:[/cyan] {cache_info['cached_at']}\n"
127
+ else:
128
+ panel_content += "[cyan]Cached:[/cyan] No\n"
129
+
130
+ # Show content or preview
131
+ if is_inline and stored_content:
132
+ panel_content += f"[cyan]Size:[/cyan] {len(stored_content):,} characters\n"
133
+ if content:
134
+ panel_content += f"\n[cyan]Content:[/cyan]\n{stored_content}"
135
+ else:
136
+ preview = stored_content[:200]
137
+ if len(stored_content) > 200:
138
+ preview += "..."
139
+ panel_content += f"\n[cyan]Preview:[/cyan]\n{preview}"
140
+ panel_content += "\n\n[dim]Use --content to show full content[/dim]"
141
+ elif not is_inline and content:
142
+ # For references, fetch and show content if requested
143
+ from tsugite.utils import resolve_attachments
144
+
145
+ resolved = resolve_attachments([alias])
146
+ if resolved:
147
+ _, resolved_content = resolved[0]
148
+ panel_content += f"[cyan]Size:[/cyan] {len(resolved_content):,} characters\n"
149
+ panel_content += f"\n[cyan]Content:[/cyan]\n{resolved_content}"
150
+ elif not is_inline:
151
+ panel_content += "\n[dim]Use --content to fetch and show full content[/dim]"
152
+
153
+ console.print(Panel(panel_content, title=f"Attachment: {alias}", border_style="blue"))
154
+
155
+
156
+ @attachments_app.command("remove")
157
+ def attachments_remove(
158
+ alias: str = typer.Argument(help="Attachment alias to remove"),
159
+ ):
160
+ """Remove an attachment."""
161
+ from tsugite.attachments import remove_attachment
162
+
163
+ if remove_attachment(alias):
164
+ console.print(f"[green]✓ Attachment '{alias}' removed[/green]")
165
+ else:
166
+ console.print(f"[yellow]Attachment '{alias}' not found[/yellow]")
167
+
168
+
169
+ @attachments_app.command("search")
170
+ def attachments_search(
171
+ query: str = typer.Argument(help="Search term"),
172
+ ):
173
+ """Search attachments by alias or source."""
174
+ from rich.table import Table
175
+
176
+ from tsugite.attachments import search_attachments
177
+
178
+ results = search_attachments(query)
179
+
180
+ if not results:
181
+ console.print(f"[yellow]No attachments found matching '{query}'[/yellow]")
182
+ return
183
+
184
+ table = Table(title=f"Search Results for '{query}' ({len(results)} found)")
185
+ table.add_column("Alias", style="cyan")
186
+ table.add_column("Source", style="dim")
187
+ table.add_column("Size", justify="right")
188
+
189
+ for alias, data in sorted(results.items()):
190
+ size = len(data.get("content", ""))
191
+ table.add_row(alias, data.get("source", "unknown"), f"{size:,}")
192
+
193
+ console.print(table)