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.
Files changed (68) hide show
  1. zai/__init__.py +1 -0
  2. zai/__main__.py +4 -0
  3. zai/cli/__init__.py +1 -0
  4. zai/cli/common.py +16 -0
  5. zai/cli/integrations.py +319 -0
  6. zai/cli/interactive.py +518 -0
  7. zai/cli/settings.py +436 -0
  8. zai/cli/utilities.py +227 -0
  9. zai/cli/workflows.py +137 -0
  10. zai/commands/commit.md +24 -0
  11. zai/commands/explain.md +17 -0
  12. zai/commands/feature.md +34 -0
  13. zai/commands/fix.md +14 -0
  14. zai/commands/review.md +22 -0
  15. zai/config.py +307 -0
  16. zai/core/__init__.py +0 -0
  17. zai/core/agent.py +701 -0
  18. zai/core/cancellation.py +67 -0
  19. zai/core/commands.py +85 -0
  20. zai/core/context.py +299 -0
  21. zai/core/errors.py +125 -0
  22. zai/core/fallback.py +171 -0
  23. zai/core/hooks.py +115 -0
  24. zai/core/memory.py +57 -0
  25. zai/core/process.py +204 -0
  26. zai/core/repomap.py +381 -0
  27. zai/core/runtime.py +29 -0
  28. zai/core/security.py +33 -0
  29. zai/core/session.py +425 -0
  30. zai/core/storage.py +193 -0
  31. zai/core/streaming.py +157 -0
  32. zai/core/tool_schema.py +133 -0
  33. zai/core/undo.py +443 -0
  34. zai/core/watch.py +80 -0
  35. zai/main.py +210 -0
  36. zai/mcp/__init__.py +0 -0
  37. zai/mcp/client.py +431 -0
  38. zai/mcp/manager.py +118 -0
  39. zai/plugins/__init__.py +2 -0
  40. zai/plugins/base.py +49 -0
  41. zai/plugins/loader.py +404 -0
  42. zai/providers/__init__.py +22 -0
  43. zai/providers/anthropic.py +131 -0
  44. zai/providers/base.py +67 -0
  45. zai/providers/cerebras.py +57 -0
  46. zai/providers/gemini.py +119 -0
  47. zai/providers/groq.py +116 -0
  48. zai/providers/ollama.py +62 -0
  49. zai/providers/openai.py +124 -0
  50. zai/providers/openrouter.py +63 -0
  51. zai/providers/qwen.py +47 -0
  52. zai/skills/__init__.py +0 -0
  53. zai/skills/registry.py +52 -0
  54. zai/tools/__init__.py +0 -0
  55. zai/tools/browser.py +224 -0
  56. zai/tools/code_runner.py +49 -0
  57. zai/tools/files.py +53 -0
  58. zai/tools/git.py +38 -0
  59. zai/tools/search.py +157 -0
  60. zai/tools/vision.py +128 -0
  61. zai/ui/__init__.py +0 -0
  62. zai/ui/input.py +199 -0
  63. zai_cli-0.1.0.dist-info/METADATA +722 -0
  64. zai_cli-0.1.0.dist-info/RECORD +68 -0
  65. zai_cli-0.1.0.dist-info/WHEEL +5 -0
  66. zai_cli-0.1.0.dist-info/entry_points.txt +2 -0
  67. zai_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  68. 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
@@ -0,0 +1,4 @@
1
+ from zai.main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
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]
@@ -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)