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_tui/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Autobots TUI - A textual-based terminal user interface."""
2
+
3
+ __version__ = "0.1.0"
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()