kiwi-code 0.0.4__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.
- kiwi_code-0.0.4.dist-info/METADATA +234 -0
- kiwi_code-0.0.4.dist-info/RECORD +24 -0
- kiwi_code-0.0.4.dist-info/WHEEL +4 -0
- kiwi_code-0.0.4.dist-info/entry_points.txt +4 -0
- kiwi_runtime/__init__.py +3 -0
- kiwi_runtime/__main__.py +5 -0
- kiwi_runtime/main.py +989 -0
- kiwi_tui/__init__.py +3 -0
- kiwi_tui/auth.py +125 -0
- kiwi_tui/cli.py +243 -0
- kiwi_tui/client.py +539 -0
- kiwi_tui/commands.py +434 -0
- kiwi_tui/config.py +79 -0
- kiwi_tui/logger.py +32 -0
- kiwi_tui/main.py +337 -0
- kiwi_tui/models.py +85 -0
- kiwi_tui/runtime_manager.py +130 -0
- kiwi_tui/screens/__init__.py +9 -0
- kiwi_tui/screens/actions.py +271 -0
- kiwi_tui/screens/autobots.py +216 -0
- kiwi_tui/screens/dashboard.py +608 -0
- kiwi_tui/screens/login.py +320 -0
- kiwi_tui/screens/runtime_logs.py +96 -0
- kiwi_tui/widgets.py +197 -0
kiwi_tui/__init__.py
ADDED
kiwi_tui/auth.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Authentication and token management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from .models import AuthTokens
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenManager:
|
|
12
|
+
"""Manages authentication tokens with secure storage."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, token_path: Optional[Path] = None):
|
|
15
|
+
"""Initialize token manager.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
token_path: Path to token file, defaults to ~/.autobots-tui/tokens.json
|
|
19
|
+
"""
|
|
20
|
+
if token_path is None:
|
|
21
|
+
token_path = Path.home() / ".autobots-tui" / "tokens.json"
|
|
22
|
+
|
|
23
|
+
self.token_path = token_path
|
|
24
|
+
self._tokens: Optional[AuthTokens] = None
|
|
25
|
+
|
|
26
|
+
def save_tokens(self, tokens: AuthTokens) -> None:
|
|
27
|
+
"""Save tokens to secure storage.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
tokens: Authentication tokens to save
|
|
31
|
+
"""
|
|
32
|
+
self._tokens = tokens
|
|
33
|
+
self.token_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(self.token_path, "w") as f:
|
|
37
|
+
json.dump(tokens.model_dump(mode="json"), f, indent=2, default=str)
|
|
38
|
+
|
|
39
|
+
# Set file permissions to user read/write only (0600)
|
|
40
|
+
self.token_path.chmod(0o600)
|
|
41
|
+
|
|
42
|
+
logger.info("Authentication tokens saved securely")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Failed to save tokens: {e}")
|
|
45
|
+
raise
|
|
46
|
+
|
|
47
|
+
def load_tokens(self) -> Optional[AuthTokens]:
|
|
48
|
+
"""Load tokens from storage.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
AuthTokens if found and valid, None otherwise
|
|
52
|
+
"""
|
|
53
|
+
if not self.token_path.exists():
|
|
54
|
+
logger.debug("No saved tokens found")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with open(self.token_path, "r") as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
|
|
61
|
+
self._tokens = AuthTokens(**data)
|
|
62
|
+
logger.info("Authentication tokens loaded")
|
|
63
|
+
return self._tokens
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Failed to load tokens: {e}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def clear_tokens(self) -> None:
|
|
69
|
+
"""Clear stored tokens."""
|
|
70
|
+
self._tokens = None
|
|
71
|
+
|
|
72
|
+
if self.token_path.exists():
|
|
73
|
+
try:
|
|
74
|
+
self.token_path.unlink()
|
|
75
|
+
logger.info("Authentication tokens cleared")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Failed to clear tokens: {e}")
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def tokens(self) -> Optional[AuthTokens]:
|
|
81
|
+
"""Get current tokens.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Current AuthTokens or None
|
|
85
|
+
"""
|
|
86
|
+
if self._tokens is None:
|
|
87
|
+
self._tokens = self.load_tokens()
|
|
88
|
+
return self._tokens
|
|
89
|
+
|
|
90
|
+
def is_authenticated(self) -> bool:
|
|
91
|
+
"""Check if user is authenticated with valid tokens.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if authenticated with non-expired tokens
|
|
95
|
+
"""
|
|
96
|
+
tokens = self.tokens
|
|
97
|
+
if not tokens:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Check if token is expired
|
|
101
|
+
if tokens.is_expired():
|
|
102
|
+
logger.warning("Access token is expired")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def get_access_token(self) -> Optional[str]:
|
|
108
|
+
"""Get the current access token.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Access token string or None
|
|
112
|
+
"""
|
|
113
|
+
tokens = self.tokens
|
|
114
|
+
if tokens and not tokens.is_expired():
|
|
115
|
+
return tokens.access_token
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def get_refresh_token(self) -> Optional[str]:
|
|
119
|
+
"""Get the current refresh token.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Refresh token string or None
|
|
123
|
+
"""
|
|
124
|
+
tokens = self.tokens
|
|
125
|
+
return tokens.refresh_token if tokens else None
|
kiwi_tui/cli.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Typer CLI for Autobots — thin wrapper over commands.py."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
logger.remove() # Suppress loguru console output in CLI mode
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from autobots_client import AuthenticatedClient
|
|
10
|
+
|
|
11
|
+
from .config import ConfigManager
|
|
12
|
+
from .auth import TokenManager
|
|
13
|
+
from . import commands
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(name="kiwi", help="Kiwi — interact with the Kiwi AI platform.")
|
|
16
|
+
|
|
17
|
+
actions_app = typer.Typer(name="actions", help="Manage actions.")
|
|
18
|
+
action_runs_app = typer.Typer(name="runs", help="Manage action runs (results).")
|
|
19
|
+
graphs_app = typer.Typer(name="graphs", help="Manage action graphs.")
|
|
20
|
+
graph_runs_app = typer.Typer(name="graph-runs", help="Manage action graph runs (results).")
|
|
21
|
+
runtime_app = typer.Typer(name="runtime", help="Manage the kiwi-runtime process.")
|
|
22
|
+
|
|
23
|
+
app.add_typer(actions_app)
|
|
24
|
+
app.add_typer(action_runs_app)
|
|
25
|
+
app.add_typer(graphs_app)
|
|
26
|
+
app.add_typer(graph_runs_app)
|
|
27
|
+
app.add_typer(runtime_app)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_client() -> AuthenticatedClient:
|
|
31
|
+
config = ConfigManager().config
|
|
32
|
+
tm = TokenManager()
|
|
33
|
+
if not tm.is_authenticated():
|
|
34
|
+
typer.echo("Not authenticated. Run `kiwi login` first.", err=True)
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
return AuthenticatedClient(base_url=config.backend_url, token=tm.get_access_token(), raise_on_unexpected_status=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _print(lines: list[str]) -> None:
|
|
40
|
+
for line in lines:
|
|
41
|
+
typer.echo(line)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# -- actions ----------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
@actions_app.command("list")
|
|
47
|
+
def actions_list_cmd(
|
|
48
|
+
name: Optional[str] = typer.Option(None, help="Filter by name"),
|
|
49
|
+
limit: int = typer.Option(20, help="Max results"),
|
|
50
|
+
offset: int = typer.Option(0, help="Offset"),
|
|
51
|
+
):
|
|
52
|
+
"""List actions."""
|
|
53
|
+
_print(commands.actions_list(_get_client(), name=name, limit=limit, offset=offset))
|
|
54
|
+
|
|
55
|
+
@actions_app.command("get")
|
|
56
|
+
def actions_get_cmd(id: str = typer.Argument(help="Action ID")):
|
|
57
|
+
"""Get action details."""
|
|
58
|
+
_print(commands.actions_get(_get_client(), id=id))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -- runs -------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
@action_runs_app.command("list")
|
|
64
|
+
def runs_list_cmd(
|
|
65
|
+
action_id: Optional[str] = typer.Option(None, help="Filter by action ID"),
|
|
66
|
+
action_name: Optional[str] = typer.Option(None, help="Filter by action name"),
|
|
67
|
+
status: Optional[str] = typer.Option(None, help="Filter by status"),
|
|
68
|
+
limit: int = typer.Option(20, help="Max results"),
|
|
69
|
+
offset: int = typer.Option(0, help="Offset"),
|
|
70
|
+
):
|
|
71
|
+
"""List action runs (results)."""
|
|
72
|
+
_print(commands.runs_list(_get_client(), action_id=action_id, action_name=action_name, status=status, limit=limit, offset=offset))
|
|
73
|
+
|
|
74
|
+
@action_runs_app.command("get")
|
|
75
|
+
def runs_get_cmd(id: str = typer.Argument(help="Action run ID")):
|
|
76
|
+
"""Get action run details."""
|
|
77
|
+
_print(commands.runs_get(_get_client(), id=id))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# -- graphs -----------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
@graphs_app.command("list")
|
|
83
|
+
def graphs_list_cmd(
|
|
84
|
+
name: Optional[str] = typer.Option(None, help="Filter by name"),
|
|
85
|
+
limit: int = typer.Option(20, help="Max results"),
|
|
86
|
+
offset: int = typer.Option(0, help="Offset"),
|
|
87
|
+
):
|
|
88
|
+
"""List action graphs."""
|
|
89
|
+
_print(commands.graphs_list(_get_client(), name=name, limit=limit, offset=offset))
|
|
90
|
+
|
|
91
|
+
@graphs_app.command("get")
|
|
92
|
+
def graphs_get_cmd(id: str = typer.Argument(help="Action graph ID")):
|
|
93
|
+
"""Get action graph details."""
|
|
94
|
+
_print(commands.graphs_get(_get_client(), id=id))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# -- graph-runs -------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
@graph_runs_app.command("list")
|
|
100
|
+
def graph_runs_list_cmd(
|
|
101
|
+
graph_id: Optional[str] = typer.Option(None, "--graph-id", help="Filter by graph ID"),
|
|
102
|
+
graph_name: Optional[str] = typer.Option(None, "--graph-name", help="Filter by graph name"),
|
|
103
|
+
status: Optional[str] = typer.Option(None, help="Filter by status"),
|
|
104
|
+
limit: int = typer.Option(20, help="Max results"),
|
|
105
|
+
offset: int = typer.Option(0, help="Offset"),
|
|
106
|
+
):
|
|
107
|
+
"""List action graph runs (results)."""
|
|
108
|
+
_print(commands.graph_runs_list(_get_client(), action_graph_id=graph_id, action_graph_name=graph_name, status=status, limit=limit, offset=offset))
|
|
109
|
+
|
|
110
|
+
@graph_runs_app.command("get")
|
|
111
|
+
def graph_runs_get_cmd(id: str = typer.Argument(help="Graph run ID")):
|
|
112
|
+
"""Get action graph run details."""
|
|
113
|
+
_print(commands.graph_runs_get(_get_client(), id=id))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# -- runtime ----------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
@runtime_app.command("status")
|
|
119
|
+
def runtime_status_cmd():
|
|
120
|
+
"""Show whether a kiwi-runtime is running for the current directory."""
|
|
121
|
+
from . import runtime_manager
|
|
122
|
+
import os
|
|
123
|
+
|
|
124
|
+
pid = runtime_manager.get_running_pid()
|
|
125
|
+
cwd = os.getcwd()
|
|
126
|
+
if pid:
|
|
127
|
+
typer.echo(f"Runtime is running (PID {pid}) for {cwd}")
|
|
128
|
+
else:
|
|
129
|
+
typer.echo(f"No runtime running for {cwd}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@runtime_app.command("stop")
|
|
133
|
+
def runtime_stop_cmd(
|
|
134
|
+
all_: bool = typer.Option(False, "--all", help="Stop all running runtimes"),
|
|
135
|
+
):
|
|
136
|
+
"""Stop the kiwi-runtime for the current directory (or all with --all)."""
|
|
137
|
+
from . import runtime_manager
|
|
138
|
+
|
|
139
|
+
if all_:
|
|
140
|
+
count = runtime_manager.stop_all()
|
|
141
|
+
typer.echo(f"Stopped {count} runtime(s).")
|
|
142
|
+
else:
|
|
143
|
+
if runtime_manager.stop_runtime():
|
|
144
|
+
typer.echo("Runtime stopped.")
|
|
145
|
+
else:
|
|
146
|
+
typer.echo("No runtime running for this directory.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@runtime_app.command("list")
|
|
150
|
+
def runtime_list_cmd():
|
|
151
|
+
"""List all known kiwi-runtime processes."""
|
|
152
|
+
from . import runtime_manager
|
|
153
|
+
|
|
154
|
+
runtimes = runtime_manager.list_all()
|
|
155
|
+
if not runtimes:
|
|
156
|
+
typer.echo("No runtimes found.")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
for rt in runtimes:
|
|
160
|
+
status = f"PID {rt['pid']}" if rt["running"] else "stopped"
|
|
161
|
+
typer.echo(f" [{status}] {rt['cwd']}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@runtime_app.command("logs")
|
|
165
|
+
def runtime_logs_cmd():
|
|
166
|
+
"""Tail the kiwi-runtime log file for the current directory."""
|
|
167
|
+
from . import runtime_manager
|
|
168
|
+
import time
|
|
169
|
+
|
|
170
|
+
rt_log_path = runtime_manager.log_path()
|
|
171
|
+
if not rt_log_path.exists():
|
|
172
|
+
typer.echo("No runtime log file found for this directory.")
|
|
173
|
+
raise typer.Exit(code=1)
|
|
174
|
+
|
|
175
|
+
typer.echo(f"Tailing {rt_log_path} (Ctrl+C to stop)\n")
|
|
176
|
+
with open(rt_log_path, "r", encoding="utf-8", errors="replace") as fp:
|
|
177
|
+
size = rt_log_path.stat().st_size
|
|
178
|
+
if size > 65536:
|
|
179
|
+
fp.seek(size - 65536)
|
|
180
|
+
try:
|
|
181
|
+
while True:
|
|
182
|
+
chunk = fp.read()
|
|
183
|
+
if chunk:
|
|
184
|
+
typer.echo(chunk, nl=False)
|
|
185
|
+
else:
|
|
186
|
+
time.sleep(0.2)
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
typer.echo("\nDone.")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# -- top-level --------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@app.command()
|
|
194
|
+
def login(
|
|
195
|
+
username: str = typer.Option(..., prompt=True, help="Email"),
|
|
196
|
+
password: str = typer.Option(..., prompt=True, hide_input=True, help="Password"),
|
|
197
|
+
):
|
|
198
|
+
"""Login and save authentication tokens."""
|
|
199
|
+
from .client import AutobotsClientWrapper
|
|
200
|
+
from .models import LoginCredentials
|
|
201
|
+
|
|
202
|
+
config = ConfigManager().config
|
|
203
|
+
token_manager = TokenManager()
|
|
204
|
+
wrapper = AutobotsClientWrapper(base_url=config.backend_url)
|
|
205
|
+
success, tokens, message = wrapper.login(LoginCredentials(username=username, password=password))
|
|
206
|
+
|
|
207
|
+
if success and tokens:
|
|
208
|
+
token_manager.save_tokens(tokens)
|
|
209
|
+
typer.echo("Logged in successfully.")
|
|
210
|
+
else:
|
|
211
|
+
typer.echo(f"Login failed: {message}", err=True)
|
|
212
|
+
raise typer.Exit(code=1)
|
|
213
|
+
|
|
214
|
+
@app.command()
|
|
215
|
+
def logout():
|
|
216
|
+
"""Logout and clear saved authentication tokens."""
|
|
217
|
+
tm = TokenManager()
|
|
218
|
+
tm.clear_tokens()
|
|
219
|
+
typer.echo("Logged out successfully.")
|
|
220
|
+
|
|
221
|
+
@app.command()
|
|
222
|
+
def whoami():
|
|
223
|
+
"""Show current authentication status."""
|
|
224
|
+
tm = TokenManager()
|
|
225
|
+
config = ConfigManager().config
|
|
226
|
+
if tm.is_authenticated():
|
|
227
|
+
typer.echo(f"Authenticated\nServer: {config.backend_url}")
|
|
228
|
+
else:
|
|
229
|
+
typer.echo("Not authenticated. Run `kiwi login` to sign in.")
|
|
230
|
+
|
|
231
|
+
@app.command()
|
|
232
|
+
def tui():
|
|
233
|
+
"""Launch the interactive TUI."""
|
|
234
|
+
from .main import main
|
|
235
|
+
main()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def cli():
|
|
239
|
+
"""Entry point for the CLI."""
|
|
240
|
+
app()
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
cli()
|