zai-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.
- zai/__init__.py +1 -0
- zai/__main__.py +4 -0
- zai/cli/__init__.py +1 -0
- zai/cli/common.py +16 -0
- zai/cli/integrations.py +319 -0
- zai/cli/interactive.py +518 -0
- zai/cli/settings.py +436 -0
- zai/cli/utilities.py +227 -0
- zai/cli/workflows.py +137 -0
- zai/commands/commit.md +24 -0
- zai/commands/explain.md +17 -0
- zai/commands/feature.md +34 -0
- zai/commands/fix.md +14 -0
- zai/commands/review.md +22 -0
- zai/config.py +307 -0
- zai/core/__init__.py +0 -0
- zai/core/agent.py +701 -0
- zai/core/cancellation.py +67 -0
- zai/core/commands.py +85 -0
- zai/core/context.py +299 -0
- zai/core/errors.py +125 -0
- zai/core/fallback.py +171 -0
- zai/core/hooks.py +115 -0
- zai/core/memory.py +57 -0
- zai/core/process.py +204 -0
- zai/core/repomap.py +381 -0
- zai/core/runtime.py +29 -0
- zai/core/security.py +33 -0
- zai/core/session.py +425 -0
- zai/core/storage.py +193 -0
- zai/core/streaming.py +157 -0
- zai/core/tool_schema.py +133 -0
- zai/core/undo.py +443 -0
- zai/core/watch.py +80 -0
- zai/main.py +210 -0
- zai/mcp/__init__.py +0 -0
- zai/mcp/client.py +431 -0
- zai/mcp/manager.py +118 -0
- zai/plugins/__init__.py +2 -0
- zai/plugins/base.py +49 -0
- zai/plugins/loader.py +404 -0
- zai/providers/__init__.py +22 -0
- zai/providers/anthropic.py +131 -0
- zai/providers/base.py +67 -0
- zai/providers/cerebras.py +57 -0
- zai/providers/gemini.py +119 -0
- zai/providers/groq.py +116 -0
- zai/providers/ollama.py +62 -0
- zai/providers/openai.py +124 -0
- zai/providers/openrouter.py +63 -0
- zai/providers/qwen.py +47 -0
- zai/skills/__init__.py +0 -0
- zai/skills/registry.py +52 -0
- zai/tools/__init__.py +0 -0
- zai/tools/browser.py +224 -0
- zai/tools/code_runner.py +49 -0
- zai/tools/files.py +53 -0
- zai/tools/git.py +38 -0
- zai/tools/search.py +157 -0
- zai/tools/vision.py +128 -0
- zai/ui/__init__.py +0 -0
- zai/ui/input.py +199 -0
- zai_cli-0.1.0.dist-info/METADATA +722 -0
- zai_cli-0.1.0.dist-info/RECORD +68 -0
- zai_cli-0.1.0.dist-info/WHEEL +5 -0
- zai_cli-0.1.0.dist-info/entry_points.txt +2 -0
- zai_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- zai_cli-0.1.0.dist-info/top_level.txt +1 -0
zai/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
zai/__main__.py
ADDED
zai/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Typer command modules for the zai CLI."""
|
zai/cli/common.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def closest_command(value: str, choices, cutoff: float = 0.72) -> str | None:
|
|
5
|
+
"""Return one unambiguous close command match."""
|
|
6
|
+
normalized = value.lower().strip()
|
|
7
|
+
candidates = sorted({str(choice).lower() for choice in choices})
|
|
8
|
+
matches = difflib.get_close_matches(normalized, candidates, n=2, cutoff=cutoff)
|
|
9
|
+
if not matches:
|
|
10
|
+
return None
|
|
11
|
+
if len(matches) > 1:
|
|
12
|
+
first = difflib.SequenceMatcher(None, normalized, matches[0]).ratio()
|
|
13
|
+
second = difflib.SequenceMatcher(None, normalized, matches[1]).ratio()
|
|
14
|
+
if first - second < 0.08:
|
|
15
|
+
return None
|
|
16
|
+
return matches[0]
|
zai/cli/integrations.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Confirm
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ..core.hooks import VALID_EVENTS, add_hook, load_hooks, remove_hook
|
|
13
|
+
from ..mcp.manager import (
|
|
14
|
+
KNOWN_SERVERS,
|
|
15
|
+
add_server,
|
|
16
|
+
build_server_command,
|
|
17
|
+
list_servers,
|
|
18
|
+
remove_server,
|
|
19
|
+
)
|
|
20
|
+
from ..plugins import loader as plugin_loader
|
|
21
|
+
from ..plugins.loader import (
|
|
22
|
+
disable_plugin,
|
|
23
|
+
enable_plugin,
|
|
24
|
+
local_plugin_path,
|
|
25
|
+
revoke_plugin_trust,
|
|
26
|
+
scaffold_plugin,
|
|
27
|
+
trust_plugin,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def hook(
|
|
34
|
+
action: str = typer.Argument("list", help="list, add, remove"),
|
|
35
|
+
event: Optional[str] = typer.Argument(None),
|
|
36
|
+
command: Optional[str] = typer.Argument(None),
|
|
37
|
+
hook_id: Optional[int] = typer.Option(None, "--id"),
|
|
38
|
+
):
|
|
39
|
+
"""Manage hooks: run commands on events."""
|
|
40
|
+
if action == "list":
|
|
41
|
+
hooks = load_hooks()
|
|
42
|
+
if not hooks:
|
|
43
|
+
console.print("[dim]No hooks configured.[/dim]")
|
|
44
|
+
return
|
|
45
|
+
table = Table(title="Hooks")
|
|
46
|
+
table.add_column("ID", style="dim")
|
|
47
|
+
table.add_column("Event", style="cyan")
|
|
48
|
+
table.add_column("Command")
|
|
49
|
+
for item in hooks:
|
|
50
|
+
table.add_row(str(item["id"]), item["event"], item["command"])
|
|
51
|
+
console.print(table)
|
|
52
|
+
elif action == "add" and event and command:
|
|
53
|
+
if event not in VALID_EVENTS:
|
|
54
|
+
console.print(f"[red]Invalid event. Choose: {', '.join(VALID_EVENTS)}[/red]")
|
|
55
|
+
return
|
|
56
|
+
created = add_hook(event, command)
|
|
57
|
+
console.print(f"[green]Hook added (id={created['id']})[/green]")
|
|
58
|
+
elif action == "remove" and hook_id:
|
|
59
|
+
if remove_hook(hook_id):
|
|
60
|
+
console.print(f"[green]Hook {hook_id} removed.[/green]")
|
|
61
|
+
else:
|
|
62
|
+
console.print(f"[red]Hook {hook_id} not found.[/red]")
|
|
63
|
+
else:
|
|
64
|
+
console.print(
|
|
65
|
+
"[red]Usage: zai hook list | zai hook add <event> <command> | "
|
|
66
|
+
"zai hook remove --id <n>[/red]"
|
|
67
|
+
)
|
|
68
|
+
console.print(f"Events: {', '.join(VALID_EVENTS)}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def mcp(
|
|
72
|
+
action: str = typer.Argument("list", help="list, add, remove, available"),
|
|
73
|
+
name: Optional[str] = typer.Argument(None),
|
|
74
|
+
):
|
|
75
|
+
"""Manage MCP servers (optional integrations)."""
|
|
76
|
+
if action == "available":
|
|
77
|
+
table = Table(title="Available MCP Servers")
|
|
78
|
+
table.add_column("Name", style="cyan")
|
|
79
|
+
table.add_column("Description")
|
|
80
|
+
table.add_column("Requires", style="dim")
|
|
81
|
+
for server_name, data in KNOWN_SERVERS.items():
|
|
82
|
+
table.add_row(
|
|
83
|
+
server_name,
|
|
84
|
+
data["description"],
|
|
85
|
+
data.get("env_key") or "nothing",
|
|
86
|
+
)
|
|
87
|
+
console.print(table)
|
|
88
|
+
elif action == "list":
|
|
89
|
+
servers = list_servers()
|
|
90
|
+
if not servers:
|
|
91
|
+
console.print("[dim]No MCP servers connected. Try: zai mcp available[/dim]")
|
|
92
|
+
return
|
|
93
|
+
for server in servers:
|
|
94
|
+
console.print(
|
|
95
|
+
f"[green]+[/green] [cyan]{server['name']}[/cyan] — "
|
|
96
|
+
f"{server['description']}"
|
|
97
|
+
)
|
|
98
|
+
elif action == "add" and name:
|
|
99
|
+
result = add_server(name)
|
|
100
|
+
if result is None:
|
|
101
|
+
console.print(f"[red]Unknown MCP server: {name}[/red]")
|
|
102
|
+
console.print("Run: zai mcp available")
|
|
103
|
+
elif result.get("already"):
|
|
104
|
+
console.print(f"[yellow]{name} already in config.[/yellow]")
|
|
105
|
+
else:
|
|
106
|
+
console.print(f"[green]MCP server '{name}' saved![/green]")
|
|
107
|
+
if result.get("env_key"):
|
|
108
|
+
console.print(
|
|
109
|
+
f"[yellow]Set {result['env_key']} in ~/.zai/.env then restart zai[/yellow]"
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
console.print("[dim]Restart zai to connect (needs: npx)[/dim]")
|
|
113
|
+
elif action == "connect" and name:
|
|
114
|
+
from ..mcp.client import connect
|
|
115
|
+
|
|
116
|
+
configured = next(
|
|
117
|
+
(server for server in list_servers() if server.get("name") == name),
|
|
118
|
+
None,
|
|
119
|
+
)
|
|
120
|
+
server_info = configured or (
|
|
121
|
+
{"name": name, **KNOWN_SERVERS[name]}
|
|
122
|
+
if name in KNOWN_SERVERS else None
|
|
123
|
+
)
|
|
124
|
+
if not server_info:
|
|
125
|
+
console.print(f"[red]Unknown MCP server: {name}[/red]")
|
|
126
|
+
return
|
|
127
|
+
try:
|
|
128
|
+
command, env = build_server_command(server_info, os.getcwd())
|
|
129
|
+
except ValueError as error:
|
|
130
|
+
console.print(f"[red]{error}[/red]")
|
|
131
|
+
return
|
|
132
|
+
console.print(f"[dim]Connecting to {name}...[/dim]")
|
|
133
|
+
if not connect(name, command, env):
|
|
134
|
+
console.print(
|
|
135
|
+
"[red]Failed. Check the server command, runtime, and credentials.[/red]"
|
|
136
|
+
)
|
|
137
|
+
elif action == "tools":
|
|
138
|
+
from ..mcp.client import get_all_tools
|
|
139
|
+
|
|
140
|
+
tools = get_all_tools()
|
|
141
|
+
if not tools:
|
|
142
|
+
console.print("[dim]No MCP servers connected. Try: zai mcp connect <name>[/dim]")
|
|
143
|
+
else:
|
|
144
|
+
for server, tool_list in tools.items():
|
|
145
|
+
console.print(f"\n[cyan]{server}[/cyan]")
|
|
146
|
+
for tool in tool_list:
|
|
147
|
+
console.print(
|
|
148
|
+
f" [dim]{tool['name']}[/dim] — "
|
|
149
|
+
f"{tool.get('description', '')[:60]}"
|
|
150
|
+
)
|
|
151
|
+
elif action == "remove" and name:
|
|
152
|
+
if remove_server(name):
|
|
153
|
+
from ..mcp.client import disconnect
|
|
154
|
+
|
|
155
|
+
disconnect(name)
|
|
156
|
+
console.print(f"[green]MCP server '{name}' removed.[/green]")
|
|
157
|
+
else:
|
|
158
|
+
console.print(f"[red]{name} not found.[/red]")
|
|
159
|
+
else:
|
|
160
|
+
console.print(
|
|
161
|
+
"[red]Usage: zai mcp list | zai mcp available | "
|
|
162
|
+
"zai mcp add <name> | zai mcp connect <name> | "
|
|
163
|
+
"zai mcp tools | zai mcp remove <name>[/red]"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def plugin(
|
|
168
|
+
action: str = typer.Argument(
|
|
169
|
+
"list",
|
|
170
|
+
help="list, new, install, remove, enable, disable, trust, untrust",
|
|
171
|
+
),
|
|
172
|
+
name: Optional[str] = typer.Argument(None, help="Plugin name"),
|
|
173
|
+
):
|
|
174
|
+
"""Manage plugins: extend zai with custom tools, skills, and commands."""
|
|
175
|
+
if action == "list":
|
|
176
|
+
plugin_loader.load_all()
|
|
177
|
+
plugins = plugin_loader.get_loaded()
|
|
178
|
+
errors = plugin_loader.get_errors()
|
|
179
|
+
|
|
180
|
+
if action == "list":
|
|
181
|
+
if not plugins and not errors:
|
|
182
|
+
console.print("[dim]No plugins loaded.[/dim]")
|
|
183
|
+
console.print(
|
|
184
|
+
"[dim]Drop a .py file in ~/.zai/plugins/ or run: "
|
|
185
|
+
"zai plugin new <name>[/dim]"
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
if plugins:
|
|
189
|
+
table = Table(title="Loaded Plugins")
|
|
190
|
+
table.add_column("Name", style="cyan")
|
|
191
|
+
table.add_column("Description")
|
|
192
|
+
table.add_column("Tools", justify="right")
|
|
193
|
+
table.add_column("Skills", justify="right")
|
|
194
|
+
table.add_column("Commands", justify="right")
|
|
195
|
+
table.add_column("Version", style="dim")
|
|
196
|
+
for plugin_name, loaded_plugin in plugins.items():
|
|
197
|
+
table.add_row(
|
|
198
|
+
plugin_name,
|
|
199
|
+
loaded_plugin.description,
|
|
200
|
+
str(len(loaded_plugin.get_tools())),
|
|
201
|
+
str(len(loaded_plugin.get_skills())),
|
|
202
|
+
str(len(loaded_plugin.get_commands())),
|
|
203
|
+
loaded_plugin.version,
|
|
204
|
+
)
|
|
205
|
+
console.print(table)
|
|
206
|
+
if errors:
|
|
207
|
+
console.print("\n[red]Load errors:[/red]")
|
|
208
|
+
for filename, error in errors.items():
|
|
209
|
+
console.print(f" [dim]{filename}[/dim]: [red]{error}[/red]")
|
|
210
|
+
elif action == "new":
|
|
211
|
+
if not name:
|
|
212
|
+
console.print("[red]Usage: zai plugin new <name>[/red]")
|
|
213
|
+
return
|
|
214
|
+
path = scaffold_plugin(name)
|
|
215
|
+
console.print(f"[green]Plugin scaffolded:[/green] {path}")
|
|
216
|
+
console.print("[dim]Edit the file, then restart zai to load it.[/dim]")
|
|
217
|
+
elif action == "install":
|
|
218
|
+
if not name:
|
|
219
|
+
console.print("[red]Usage: zai plugin install <name>[/red]")
|
|
220
|
+
return
|
|
221
|
+
package = name if name.startswith("zai-plugin-") else f"zai-plugin-{name}"
|
|
222
|
+
if not Confirm.ask(
|
|
223
|
+
f"Install external plugin package '{package}'?",
|
|
224
|
+
default=False,
|
|
225
|
+
):
|
|
226
|
+
console.print("[yellow]Plugin installation cancelled.[/yellow]")
|
|
227
|
+
return
|
|
228
|
+
console.print(f"[cyan]Installing {package}...[/cyan]")
|
|
229
|
+
result = subprocess.run(
|
|
230
|
+
[sys.executable, "-m", "pip", "install", package],
|
|
231
|
+
capture_output=True,
|
|
232
|
+
text=True,
|
|
233
|
+
)
|
|
234
|
+
if result.returncode == 0:
|
|
235
|
+
console.print(
|
|
236
|
+
"[green]Installed.[/green] Review the package, then run: "
|
|
237
|
+
f"[cyan]zai plugin trust {name}[/cyan]"
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
console.print(f"[red]Install failed:[/red]\n{result.stderr.strip()}")
|
|
241
|
+
elif action == "remove":
|
|
242
|
+
if not name:
|
|
243
|
+
console.print("[red]Usage: zai plugin remove <name>[/red]")
|
|
244
|
+
return
|
|
245
|
+
plugin_file = local_plugin_path(name)
|
|
246
|
+
if plugin_file and plugin_file.exists():
|
|
247
|
+
plugin_file.unlink()
|
|
248
|
+
revoke_plugin_trust(name)
|
|
249
|
+
console.print(f"[green]Removed:[/green] {plugin_file}")
|
|
250
|
+
elif plugin_file:
|
|
251
|
+
package = name if name.startswith("zai-plugin-") else f"zai-plugin-{name}"
|
|
252
|
+
result = subprocess.run(
|
|
253
|
+
[sys.executable, "-m", "pip", "uninstall", package, "-y"],
|
|
254
|
+
capture_output=True,
|
|
255
|
+
text=True,
|
|
256
|
+
)
|
|
257
|
+
if result.returncode == 0:
|
|
258
|
+
revoke_plugin_trust(name)
|
|
259
|
+
console.print(f"[green]Uninstalled {package}[/green]")
|
|
260
|
+
else:
|
|
261
|
+
console.print(
|
|
262
|
+
f"[red]Plugin '{name}' not found "
|
|
263
|
+
"(checked ~/.zai/plugins/ and pip)[/red]"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
console.print("[red]Invalid plugin name.[/red]")
|
|
267
|
+
elif action == "disable":
|
|
268
|
+
if not name:
|
|
269
|
+
console.print("[red]Usage: zai plugin disable <name>[/red]")
|
|
270
|
+
return
|
|
271
|
+
disable_plugin(name)
|
|
272
|
+
console.print(f"[yellow]Plugin '{name}' disabled.[/yellow]")
|
|
273
|
+
elif action == "enable":
|
|
274
|
+
if not name:
|
|
275
|
+
console.print("[red]Usage: zai plugin enable <name>[/red]")
|
|
276
|
+
return
|
|
277
|
+
if not Confirm.ask(
|
|
278
|
+
f"Enable plugin '{name}' with its declared permissions?",
|
|
279
|
+
default=False,
|
|
280
|
+
):
|
|
281
|
+
console.print("[yellow]Plugin enable cancelled.[/yellow]")
|
|
282
|
+
return
|
|
283
|
+
if enable_plugin(name):
|
|
284
|
+
console.print(
|
|
285
|
+
f"[green]Plugin '{name}' enabled. Restart zai to load it.[/green]"
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
console.print(f"[yellow]Plugin '{name}' was not disabled.[/yellow]")
|
|
289
|
+
elif action == "trust":
|
|
290
|
+
if not name:
|
|
291
|
+
console.print("[red]Usage: zai plugin trust <name>[/red]")
|
|
292
|
+
return
|
|
293
|
+
if trust_plugin(name):
|
|
294
|
+
console.print(
|
|
295
|
+
f"[green]Trusted current code for '{name}'.[/green] "
|
|
296
|
+
"Restart zai to load it."
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
console.print(f"[red]Plugin '{name}' not found.[/red]")
|
|
300
|
+
elif action == "untrust":
|
|
301
|
+
if not name:
|
|
302
|
+
console.print("[red]Usage: zai plugin untrust <name>[/red]")
|
|
303
|
+
return
|
|
304
|
+
if revoke_plugin_trust(name):
|
|
305
|
+
console.print(f"[yellow]Trust revoked for '{name}'.[/yellow]")
|
|
306
|
+
else:
|
|
307
|
+
console.print(f"[yellow]Plugin '{name}' was not trusted.[/yellow]")
|
|
308
|
+
else:
|
|
309
|
+
console.print(
|
|
310
|
+
"[red]Usage: zai plugin list | new <name> | install <name> | "
|
|
311
|
+
"remove <name> | disable <name> | enable <name> | "
|
|
312
|
+
"trust <name> | untrust <name>[/red]"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def register_integration_commands(app: typer.Typer) -> None:
|
|
317
|
+
app.command()(hook)
|
|
318
|
+
app.command()(mcp)
|
|
319
|
+
app.command("plugin")(plugin)
|