zenus-cli 0.6.1__tar.gz
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.
zenus_cli-0.6.1/PKG-INFO
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zenus-cli
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Zenus - Command Line Interface
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Zenus Team
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Dist: anthropic (>=0.47.0,<0.48.0)
|
|
16
|
+
Requires-Dist: openai (>=1.0.0,<2.0.0)
|
|
17
|
+
Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
|
|
18
|
+
Requires-Dist: rich (>=13.7.0,<14.0.0)
|
|
19
|
+
Requires-Dist: zenus-core (>=0.6.1,<0.7.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Zenus CLI
|
|
23
|
+
|
|
24
|
+
Command-line interface for Zenus.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install zenus-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
See main [Zenus documentation](../../README.md) for details.
|
|
33
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "zenus-cli"
|
|
3
|
+
version = "0.6.1"
|
|
4
|
+
description = "Zenus - Command Line Interface"
|
|
5
|
+
authors = ["Zenus Team"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
packages = [{include = "zenus_cli", from = "src"}]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
zenus-core = "^0.6.1"
|
|
13
|
+
rich = "^13.7.0"
|
|
14
|
+
# LLM provider dependencies (needed for runtime)
|
|
15
|
+
openai = "^1.0.0"
|
|
16
|
+
anthropic = "^0.47.0"
|
|
17
|
+
pyyaml = "^6.0.3"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.scripts]
|
|
20
|
+
zenus = "zenus_cli.main:main"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.group.dev.dependencies]
|
|
23
|
+
pytest = "^8.0.0"
|
|
24
|
+
pytest-cov = "^4.1.0"
|
|
25
|
+
prompt-toolkit = "^3.0.52"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["poetry-core"]
|
|
29
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Command Router
|
|
3
|
+
|
|
4
|
+
Responsible for:
|
|
5
|
+
- Parsing CLI arguments
|
|
6
|
+
- Routing to appropriate handlers (interactive/direct/help)
|
|
7
|
+
- Maintaining clear separation between parse, intent, execute
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Optional, List
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CLICommand:
|
|
16
|
+
"""Represents a parsed CLI command"""
|
|
17
|
+
mode: str # 'interactive', 'direct', 'help', 'version', 'status', 'model'
|
|
18
|
+
input_text: Optional[str] = None
|
|
19
|
+
flags: dict = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CommandRouter:
|
|
23
|
+
"""Routes CLI invocations to appropriate handlers"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.version = "0.1.0-alpha"
|
|
27
|
+
|
|
28
|
+
def parse(self, args: List[str]) -> CLICommand:
|
|
29
|
+
"""
|
|
30
|
+
Parse command line arguments into a CLICommand
|
|
31
|
+
|
|
32
|
+
Modes:
|
|
33
|
+
- No args or 'shell' -> interactive REPL
|
|
34
|
+
- 'help' / '--help' / '-h' -> help message
|
|
35
|
+
- 'version' / '--version' / '-v' -> version info
|
|
36
|
+
- 'rollback' -> rollback commands
|
|
37
|
+
- 'history' -> history commands
|
|
38
|
+
- '--dry-run <text>' -> show plan but do not execute
|
|
39
|
+
- Direct text -> immediate intent execution
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Check for flags
|
|
43
|
+
dry_run = False
|
|
44
|
+
iterative = False
|
|
45
|
+
force_provider: Optional[str] = None
|
|
46
|
+
force_model: Optional[str] = None
|
|
47
|
+
filtered_args = []
|
|
48
|
+
i = 0
|
|
49
|
+
while i < len(args):
|
|
50
|
+
arg = args[i]
|
|
51
|
+
if arg == "--dry-run":
|
|
52
|
+
dry_run = True
|
|
53
|
+
elif arg == "--iterative":
|
|
54
|
+
iterative = True
|
|
55
|
+
elif arg in ("--provider", "-p") and i + 1 < len(args):
|
|
56
|
+
force_provider = args[i + 1]
|
|
57
|
+
i += 1
|
|
58
|
+
elif arg.startswith("--provider="):
|
|
59
|
+
force_provider = arg.split("=", 1)[1]
|
|
60
|
+
elif arg == "--model" and i + 1 < len(args):
|
|
61
|
+
force_model = args[i + 1]
|
|
62
|
+
i += 1
|
|
63
|
+
elif arg.startswith("--model="):
|
|
64
|
+
force_model = arg.split("=", 1)[1]
|
|
65
|
+
else:
|
|
66
|
+
filtered_args.append(arg)
|
|
67
|
+
i += 1
|
|
68
|
+
|
|
69
|
+
args = filtered_args
|
|
70
|
+
|
|
71
|
+
if not args or args[0] == "shell":
|
|
72
|
+
return CLICommand(mode="interactive")
|
|
73
|
+
|
|
74
|
+
if args[0] in ("help", "--help", "-h"):
|
|
75
|
+
return CLICommand(mode="help")
|
|
76
|
+
|
|
77
|
+
if args[0] in ("version", "--version", "-v"):
|
|
78
|
+
return CLICommand(mode="version")
|
|
79
|
+
|
|
80
|
+
if args[0] == "status":
|
|
81
|
+
return CLICommand(mode="status")
|
|
82
|
+
|
|
83
|
+
if args[0] == "model":
|
|
84
|
+
return CLICommand(
|
|
85
|
+
mode="model",
|
|
86
|
+
input_text=" ".join(args[1:]) if len(args) > 1 else "status",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if args[0] == "rollback":
|
|
90
|
+
return CLICommand(
|
|
91
|
+
mode="rollback",
|
|
92
|
+
input_text=" ".join(args[1:]) if len(args) > 1 else None,
|
|
93
|
+
flags={"dry_run": dry_run}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if args[0] == "history":
|
|
97
|
+
return CLICommand(
|
|
98
|
+
mode="history",
|
|
99
|
+
input_text=" ".join(args[1:]) if len(args) > 1 else None
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Everything else is a direct command
|
|
103
|
+
input_text = " ".join(args)
|
|
104
|
+
return CLICommand(
|
|
105
|
+
mode="direct",
|
|
106
|
+
input_text=input_text,
|
|
107
|
+
flags={
|
|
108
|
+
"dry_run": dry_run,
|
|
109
|
+
"iterative": iterative,
|
|
110
|
+
"force_provider": force_provider,
|
|
111
|
+
"force_model": force_model,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def show_help(self):
|
|
116
|
+
"""Display help message"""
|
|
117
|
+
help_text = f"""
|
|
118
|
+
Zenus v{self.version}
|
|
119
|
+
A developer-centric, CLI-first, agent-driven operating layer
|
|
120
|
+
|
|
121
|
+
USAGE:
|
|
122
|
+
zenus [OPTIONS] [COMMAND]
|
|
123
|
+
|
|
124
|
+
COMMANDS:
|
|
125
|
+
shell Start interactive REPL (default)
|
|
126
|
+
status Show system status and active LLM config
|
|
127
|
+
model [status] Show current provider/model configuration
|
|
128
|
+
model list List all available models per provider
|
|
129
|
+
model set <p> [model] Set default provider (and optionally model)
|
|
130
|
+
rollback [N] Rollback last N actions (default: 1)
|
|
131
|
+
history Show command history and failures
|
|
132
|
+
help Show this help message
|
|
133
|
+
version Show version information
|
|
134
|
+
<direct command> Execute command immediately
|
|
135
|
+
|
|
136
|
+
OPTIONS:
|
|
137
|
+
--dry-run Show plan but do not execute
|
|
138
|
+
--iterative Use iterative ReAct loop (for complex tasks)
|
|
139
|
+
--provider <name> Use specific provider for this command only
|
|
140
|
+
(anthropic, openai, deepseek, ollama)
|
|
141
|
+
--model <id> Use specific model for this command only
|
|
142
|
+
|
|
143
|
+
EXAMPLES:
|
|
144
|
+
zenus # Start interactive shell
|
|
145
|
+
zenus "list files in ~/Documents" # Direct execution
|
|
146
|
+
zenus --dry-run "delete all tmp files" # Preview without executing
|
|
147
|
+
zenus --provider deepseek "summarize this" # Override provider once
|
|
148
|
+
zenus --model claude-opus-4-6 "refactor src" # Override model once
|
|
149
|
+
zenus model set anthropic claude-opus-4-6 # Change default
|
|
150
|
+
zenus --iterative "read project and improve" # Multi-step ReAct loop
|
|
151
|
+
|
|
152
|
+
INTERACTIVE SHELL SHORTCUTS:
|
|
153
|
+
@deepseek: your command # Use deepseek for this command
|
|
154
|
+
use claude: your command # Use anthropic for this command
|
|
155
|
+
--provider openai <cmd> # Override provider in shell mode
|
|
156
|
+
model set deepseek # Change default inside shell
|
|
157
|
+
status # Show status inside shell
|
|
158
|
+
|
|
159
|
+
PROVIDER ALIASES (for inline overrides):
|
|
160
|
+
anthropic, claude, sonnet, haiku, opus → anthropic
|
|
161
|
+
openai, gpt, chatgpt, o1, o3 → openai
|
|
162
|
+
deepseek → deepseek
|
|
163
|
+
ollama, local, llama → ollama
|
|
164
|
+
|
|
165
|
+
CREDENTIALS:
|
|
166
|
+
ANTHROPIC_API_KEY Anthropic (Claude) API key
|
|
167
|
+
OPENAI_API_KEY OpenAI API key
|
|
168
|
+
DEEPSEEK_API_KEY DeepSeek API key
|
|
169
|
+
(or set in config.yaml / .env)
|
|
170
|
+
|
|
171
|
+
LOGS:
|
|
172
|
+
Audit logs: ~/.zenus/logs/
|
|
173
|
+
|
|
174
|
+
For more information, visit: https://github.com/Guillhermm/zenus
|
|
175
|
+
""".strip()
|
|
176
|
+
print(help_text)
|
|
177
|
+
|
|
178
|
+
def show_version(self):
|
|
179
|
+
"""Display version information"""
|
|
180
|
+
print(f"Zenus v{self.version}")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zenus - Main Entry Point
|
|
3
|
+
|
|
4
|
+
CLI-first, agent-driven operating layer
|
|
5
|
+
Routes commands through: CLI to Intent to Plan to Execute
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from zenus_cli.router import CommandRouter
|
|
10
|
+
from zenus_core.orchestrator import Orchestrator, OrchestratorError
|
|
11
|
+
from zenus_core.rollback import get_rollback_engine, RollbackError
|
|
12
|
+
from zenus_core.memory.action_tracker import get_action_tracker
|
|
13
|
+
from zenus_core.memory.failure_logger import get_failure_logger
|
|
14
|
+
from zenus_core.output.console import console
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
"""Main entry point for Zenus"""
|
|
19
|
+
|
|
20
|
+
router = CommandRouter()
|
|
21
|
+
orchestrator = Orchestrator()
|
|
22
|
+
|
|
23
|
+
# Parse CLI arguments (skip program name)
|
|
24
|
+
args = sys.argv[1:]
|
|
25
|
+
command = router.parse(args)
|
|
26
|
+
|
|
27
|
+
# Route to appropriate handler
|
|
28
|
+
try:
|
|
29
|
+
if command.mode == "help":
|
|
30
|
+
router.show_help()
|
|
31
|
+
|
|
32
|
+
elif command.mode == "version":
|
|
33
|
+
router.show_version()
|
|
34
|
+
|
|
35
|
+
elif command.mode == "status":
|
|
36
|
+
from zenus_core.shell.commands import handle_status_command
|
|
37
|
+
handle_status_command(orchestrator)
|
|
38
|
+
|
|
39
|
+
elif command.mode == "model":
|
|
40
|
+
from zenus_core.shell.commands import handle_model_command
|
|
41
|
+
parts = (command.input_text or "status").split()
|
|
42
|
+
subcommand = parts[0]
|
|
43
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
44
|
+
handle_model_command(subcommand, args)
|
|
45
|
+
|
|
46
|
+
elif command.mode == "interactive":
|
|
47
|
+
orchestrator.interactive_shell()
|
|
48
|
+
|
|
49
|
+
elif command.mode == "rollback":
|
|
50
|
+
# Rollback command
|
|
51
|
+
rollback_engine = get_rollback_engine()
|
|
52
|
+
dry_run = command.flags.get("dry_run", False)
|
|
53
|
+
|
|
54
|
+
# Parse number of actions to rollback
|
|
55
|
+
n = 1
|
|
56
|
+
if command.input_text:
|
|
57
|
+
try:
|
|
58
|
+
n = int(command.input_text)
|
|
59
|
+
except ValueError:
|
|
60
|
+
console.print("[red]Error: rollback argument must be a number[/red]")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
result = rollback_engine.rollback_last_n_actions(n, dry_run=dry_run)
|
|
65
|
+
if result["success"]:
|
|
66
|
+
if result.get("dry_run"):
|
|
67
|
+
console.print("\n[green]Dry run complete - no changes made[/green]")
|
|
68
|
+
else:
|
|
69
|
+
console.print(f"\n[green]✓ Successfully rolled back {result['actions_rolled_back']} action(s)[/green]")
|
|
70
|
+
else:
|
|
71
|
+
console.print(f"\n[yellow]⚠ Partial rollback: {result['actions_rolled_back']} succeeded, {result['actions_failed']} failed[/yellow]")
|
|
72
|
+
for error in result["errors"]:
|
|
73
|
+
console.print(f" [red]• {error}[/red]")
|
|
74
|
+
except RollbackError as e:
|
|
75
|
+
console.print(f"[red]Rollback error: {e}[/red]")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
elif command.mode == "history":
|
|
79
|
+
# History command
|
|
80
|
+
action_tracker = get_action_tracker()
|
|
81
|
+
failure_logger = get_failure_logger()
|
|
82
|
+
|
|
83
|
+
# Check if --failures flag
|
|
84
|
+
show_failures = command.input_text == "--failures"
|
|
85
|
+
|
|
86
|
+
if show_failures:
|
|
87
|
+
# Show failure history
|
|
88
|
+
stats = failure_logger.get_failure_stats()
|
|
89
|
+
console.print("\n[cyan]Failure History:[/cyan]")
|
|
90
|
+
console.print(f" Total failures: {stats['total_failures']}")
|
|
91
|
+
console.print(f" Recent (7 days): {stats['recent_7_days']}")
|
|
92
|
+
|
|
93
|
+
if stats['by_tool']:
|
|
94
|
+
console.print("\n[cyan]By tool:[/cyan]")
|
|
95
|
+
for tool, count in sorted(stats['by_tool'].items(), key=lambda x: x[1], reverse=True)[:10]:
|
|
96
|
+
console.print(f" {tool}: {count}")
|
|
97
|
+
|
|
98
|
+
if stats['by_error_type']:
|
|
99
|
+
console.print("\n[cyan]By error type:[/cyan]")
|
|
100
|
+
for error_type, count in sorted(stats['by_error_type'].items(), key=lambda x: x[1], reverse=True):
|
|
101
|
+
console.print(f" {error_type}: {count}")
|
|
102
|
+
|
|
103
|
+
console.print(f"\n[cyan]Patterns with suggestions: {stats['patterns_with_suggestions']}[/cyan]")
|
|
104
|
+
else:
|
|
105
|
+
# Show transaction history
|
|
106
|
+
transactions = action_tracker.get_recent_transactions(limit=10)
|
|
107
|
+
console.print("\n[cyan]Recent Transactions:[/cyan]")
|
|
108
|
+
for tx in transactions:
|
|
109
|
+
status_color = "green" if tx["status"] == "completed" else "red"
|
|
110
|
+
rollback_info = " (rolled back)" if tx.get("rollback_status") else ""
|
|
111
|
+
console.print(f" [{status_color}]{tx['id']}[/{status_color}]: {tx['user_input']}{rollback_info}")
|
|
112
|
+
console.print(f" Goal: {tx['intent_goal']}")
|
|
113
|
+
console.print(f" Status: {tx['status']}")
|
|
114
|
+
console.print()
|
|
115
|
+
|
|
116
|
+
elif command.mode == "direct":
|
|
117
|
+
# Direct execution
|
|
118
|
+
dry_run = command.flags.get("dry_run", False)
|
|
119
|
+
iterative = command.flags.get("iterative", False)
|
|
120
|
+
force_provider = command.flags.get("force_provider")
|
|
121
|
+
force_model = command.flags.get("force_model")
|
|
122
|
+
|
|
123
|
+
# If --model given without --provider, embed it as an inline directive
|
|
124
|
+
# so the orchestrator's provider_override parser can infer the provider
|
|
125
|
+
input_text = command.input_text
|
|
126
|
+
if force_model and not force_provider:
|
|
127
|
+
input_text = f"--model {force_model} {input_text}"
|
|
128
|
+
force_provider = None # let the parser infer it
|
|
129
|
+
|
|
130
|
+
if iterative:
|
|
131
|
+
result = orchestrator.execute_iterative(
|
|
132
|
+
input_text,
|
|
133
|
+
max_iterations=10,
|
|
134
|
+
dry_run=dry_run,
|
|
135
|
+
force_provider=force_provider,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
result = orchestrator.execute_command(
|
|
139
|
+
input_text,
|
|
140
|
+
dry_run=dry_run,
|
|
141
|
+
force_provider=force_provider,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if result:
|
|
145
|
+
print(result)
|
|
146
|
+
|
|
147
|
+
except OrchestratorError as e:
|
|
148
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
except KeyboardInterrupt:
|
|
151
|
+
print("\nInterrupted")
|
|
152
|
+
sys.exit(130)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|