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/commands.py ADDED
@@ -0,0 +1,434 @@
1
+ """Command handlers for Autobots — shared by both CLI and TUI.
2
+
3
+ Each command function takes an AuthenticatedClient and keyword args,
4
+ and returns a list of strings (output lines).
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from autobots_client import AuthenticatedClient
10
+ from autobots_client.api.actions import (
11
+ list_actions_v1_actions_get,
12
+ get_action_v1_actions_id_get,
13
+ )
14
+ from autobots_client.api.action_results import (
15
+ list_action_result_v1_action_results_get,
16
+ get_action_result_v1_action_results_id_get,
17
+ )
18
+ from autobots_client.api.action_graphs import (
19
+ list_action_graphs_v1_action_graphs_get,
20
+ get_action_graph_v1_action_graphs_id_get,
21
+ )
22
+ from autobots_client.api.action_graphs_results import (
23
+ list_action_graph_result_v1_action_graphs_results_get,
24
+ get_action_graph_result_v1_action_graphs_results_id_get,
25
+ )
26
+ from autobots_client.types import UNSET
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Helpers
31
+ # ---------------------------------------------------------------------------
32
+
33
+ def _fmt_date(dt) -> str:
34
+ if dt is None or isinstance(dt, type(UNSET)):
35
+ return "-"
36
+ try:
37
+ return dt.strftime("%Y-%m-%d %H:%M")
38
+ except Exception:
39
+ return str(dt)
40
+
41
+
42
+ def _val(v) -> str:
43
+ if v is None or isinstance(v, type(UNSET)):
44
+ return ""
45
+ return str(v)
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # actions
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def actions_list(client: AuthenticatedClient, *, name: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
53
+ """List actions. Returns output lines."""
54
+ resp = list_actions_v1_actions_get.sync_detailed(
55
+ client=client,
56
+ name=name if name else UNSET,
57
+ limit=limit,
58
+ offset=offset,
59
+ )
60
+ if resp.status_code != 200:
61
+ return [f"Error: HTTP {resp.status_code}"]
62
+
63
+ result = resp.parsed
64
+ lines = [
65
+ f"Actions ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}):",
66
+ "",
67
+ f"{'ID':<28} {'Name':<30} {'Type':<25} {'Ver':<6} {'Created'}",
68
+ "-" * 105,
69
+ ]
70
+ for doc in result.docs:
71
+ lines.append(
72
+ f"{doc.field_id:<28} {_val(doc.name):<30} {_val(doc.type_.value):<25} "
73
+ f"{_val(doc.version):<6} {_fmt_date(doc.created_at)}"
74
+ )
75
+ return lines
76
+
77
+
78
+ def actions_get(client: AuthenticatedClient, *, id: str) -> list[str]:
79
+ """Get action details. Returns output lines."""
80
+ resp = get_action_v1_actions_id_get.sync_detailed(id=id, client=client)
81
+ if resp.status_code != 200:
82
+ return [f"Error: HTTP {resp.status_code}"]
83
+
84
+ doc = resp.parsed
85
+ return [
86
+ f"ID: {doc.field_id}",
87
+ f"Name: {doc.name}",
88
+ f"Type: {doc.type_.value}",
89
+ f"Version: {_val(doc.version)}",
90
+ f"Description: {_val(doc.description)}",
91
+ f"Published: {_val(doc.is_published)}",
92
+ f"Saved: {_val(doc.is_saved)}",
93
+ f"Created: {_fmt_date(doc.created_at)}",
94
+ f"Updated: {_fmt_date(doc.updated_at)}",
95
+ ]
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # runs (action results)
100
+ # ---------------------------------------------------------------------------
101
+
102
+ def runs_list(client: AuthenticatedClient, *, action_id: Optional[str] = None, action_name: Optional[str] = None,
103
+ status: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
104
+ """List action runs. Returns output lines."""
105
+ from autobots_client.models.event_result_status import EventResultStatus
106
+
107
+ status_enum = UNSET
108
+ if status:
109
+ try:
110
+ status_enum = EventResultStatus(status.lower())
111
+ except ValueError:
112
+ return [f"Invalid status: {status}. Valid: processing, success, error, stuck, waiting"]
113
+
114
+ resp = list_action_result_v1_action_results_get.sync_detailed(
115
+ client=client,
116
+ action_id=action_id if action_id else UNSET,
117
+ action_name=action_name if action_name else UNSET,
118
+ status=status_enum,
119
+ limit=limit,
120
+ offset=offset,
121
+ )
122
+ if resp.status_code != 200:
123
+ return [f"Error: HTTP {resp.status_code}"]
124
+
125
+ result = resp.parsed
126
+ lines = [
127
+ f"Action Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}):",
128
+ "",
129
+ f"{'ID':<28} {'Status':<12} {'Name':<30} {'Created':<18} {'Updated'}",
130
+ "-" * 105,
131
+ ]
132
+ for doc in result.docs:
133
+ lines.append(
134
+ f"{doc.field_id:<28} {doc.status.value:<12} {_val(doc.name):<30} "
135
+ f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
136
+ )
137
+ return lines
138
+
139
+
140
+ def runs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
141
+ """Get action run details. Returns output lines."""
142
+ resp = get_action_result_v1_action_results_id_get.sync_detailed(id=id, client=client)
143
+ if resp.status_code != 200:
144
+ return [f"Error: HTTP {resp.status_code}"]
145
+
146
+ doc = resp.parsed
147
+ lines = [
148
+ f"ID: {doc.field_id}",
149
+ f"Status: {doc.status.value}",
150
+ f"Name: {_val(doc.name)}",
151
+ f"Type: {doc.type_.value}",
152
+ f"Saved: {_val(doc.is_saved)}",
153
+ f"Created: {_fmt_date(doc.created_at)}",
154
+ f"Updated: {_fmt_date(doc.updated_at)}",
155
+ ]
156
+ if doc.result and not isinstance(doc.result, type(UNSET)):
157
+ lines += [
158
+ "",
159
+ "Action:",
160
+ f" Name: {doc.result.name}",
161
+ f" Type: {doc.result.type_.value}",
162
+ f" ID: {doc.result.field_id}",
163
+ ]
164
+ return lines
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # graphs (action graphs)
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def graphs_list(client: AuthenticatedClient, *, name: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
172
+ """List action graphs. Returns output lines."""
173
+ resp = list_action_graphs_v1_action_graphs_get.sync_detailed(
174
+ client=client,
175
+ name=name if name else UNSET,
176
+ limit=limit,
177
+ offset=offset,
178
+ )
179
+ if resp.status_code != 200:
180
+ return [f"Error: HTTP {resp.status_code}"]
181
+
182
+ result = resp.parsed
183
+ lines = [
184
+ f"Action Graphs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}):",
185
+ "",
186
+ f"{'ID':<28} {'Name':<30} {'Ver':<6} {'Published':<10} {'Created'}",
187
+ "-" * 105,
188
+ ]
189
+ for doc in result.docs:
190
+ lines.append(
191
+ f"{doc.field_id:<28} {_val(doc.name):<30} {_val(doc.version):<6} "
192
+ f"{_val(doc.is_published):<10} {_fmt_date(doc.created_at)}"
193
+ )
194
+ return lines
195
+
196
+
197
+ def graphs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
198
+ """Get action graph details. Returns output lines."""
199
+ resp = get_action_graph_v1_action_graphs_id_get.sync_detailed(id=id, client=client)
200
+ if resp.status_code != 200:
201
+ return [f"Error: HTTP {resp.status_code}"]
202
+
203
+ doc = resp.parsed
204
+ if hasattr(doc, 'field_id'):
205
+ return [
206
+ f"ID: {doc.field_id}",
207
+ f"Name: {_val(getattr(doc, 'name', ''))}",
208
+ f"Version: {_val(getattr(doc, 'version', ''))}",
209
+ f"Description: {_val(getattr(doc, 'description', ''))}",
210
+ f"Published: {_val(getattr(doc, 'is_published', ''))}",
211
+ f"Created: {_fmt_date(getattr(doc, 'created_at', None))}",
212
+ f"Updated: {_fmt_date(getattr(doc, 'updated_at', None))}",
213
+ ]
214
+ import json
215
+ if hasattr(doc, 'to_dict'):
216
+ return [json.dumps(doc.to_dict(), indent=2, default=str)]
217
+ return [str(doc)]
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # graph-runs (action graph results)
222
+ # ---------------------------------------------------------------------------
223
+
224
+ def graph_runs_list(client: AuthenticatedClient, *, action_graph_id: Optional[str] = None,
225
+ action_graph_name: Optional[str] = None, status: Optional[str] = None,
226
+ limit: int = 20, offset: int = 0) -> list[str]:
227
+ """List action graph runs. Returns output lines."""
228
+ from autobots_client.models.event_result_status import EventResultStatus
229
+
230
+ status_enum = UNSET
231
+ if status:
232
+ try:
233
+ status_enum = EventResultStatus(status.lower())
234
+ except ValueError:
235
+ return [f"Invalid status: {status}. Valid: processing, success, error, stuck, waiting"]
236
+
237
+ resp = list_action_graph_result_v1_action_graphs_results_get.sync_detailed(
238
+ client=client,
239
+ action_graph_id=action_graph_id if action_graph_id else UNSET,
240
+ action_graph_name=action_graph_name if action_graph_name else UNSET,
241
+ status=status_enum,
242
+ limit=limit,
243
+ offset=offset,
244
+ )
245
+ if resp.status_code != 200:
246
+ return [f"Error: HTTP {resp.status_code}"]
247
+
248
+ result = resp.parsed
249
+ lines = [
250
+ f"Action Graph Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}):",
251
+ "",
252
+ f"{'ID':<28} {'Status':<12} {'Name':<30} {'Created':<18} {'Updated'}",
253
+ "-" * 105,
254
+ ]
255
+ for doc in result.docs:
256
+ lines.append(
257
+ f"{doc.field_id:<28} {doc.status.value:<12} {_val(doc.name):<30} "
258
+ f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
259
+ )
260
+ return lines
261
+
262
+
263
+ def graph_runs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
264
+ """Get action graph run details. Returns output lines."""
265
+ resp = get_action_graph_result_v1_action_graphs_results_id_get.sync_detailed(id=id, client=client)
266
+ if resp.status_code != 200:
267
+ return [f"Error: HTTP {resp.status_code}"]
268
+
269
+ doc = resp.parsed
270
+ lines = [
271
+ f"ID: {doc.field_id}",
272
+ f"Status: {doc.status.value}",
273
+ f"Name: {_val(doc.name)}",
274
+ f"Type: {doc.type_.value}",
275
+ f"Saved: {_val(doc.is_saved)}",
276
+ f"Created: {_fmt_date(doc.created_at)}",
277
+ f"Updated: {_fmt_date(doc.updated_at)}",
278
+ ]
279
+ if doc.result and not isinstance(doc.result, type(UNSET)):
280
+ lines += [
281
+ "",
282
+ "Action Graph:",
283
+ f" Name: {doc.result.name}",
284
+ f" ID: {doc.result.field_id}",
285
+ ]
286
+ return lines
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Command dispatcher — parses "/command args" from TUI input
291
+ # ---------------------------------------------------------------------------
292
+
293
+ HELP_TEXT = """\
294
+ Session:
295
+ /use <action_id> Switch to a different action
296
+ /continue <run_id> Continue an existing run
297
+ /new Start new conversation
298
+ /status Show current action & run
299
+
300
+ Query:
301
+ /actions list [--name NAME] [--limit N]
302
+ /actions get <id>
303
+ /runs list [--action-id ID] [--status STATUS] [--limit N]
304
+ /runs get <id>
305
+ /graphs list [--name NAME] [--limit N]
306
+ /graphs get <id>
307
+ /graph-runs list [--graph-id ID] [--status STATUS] [--limit N]
308
+ /graph-runs get <id>
309
+
310
+ /help Show this help"""
311
+
312
+
313
+ def dispatch(command_str: str, client: AuthenticatedClient) -> list[str]:
314
+ """Parse a /command string and execute it. Returns output lines.
315
+
316
+ Args:
317
+ command_str: The full input string starting with "/" (e.g. "/actions list --limit 5")
318
+ client: Authenticated API client
319
+ """
320
+ parts = command_str.strip().split()
321
+ if not parts:
322
+ return [HELP_TEXT]
323
+
324
+ # Remove leading "/" from first token
325
+ group = parts[0].lstrip("/").lower()
326
+ sub = parts[1].lower() if len(parts) > 1 else ""
327
+ args = parts[2:]
328
+
329
+ def _parse_opts(args: list[str]) -> dict[str, str]:
330
+ """Parse --key value pairs from args list."""
331
+ opts: dict[str, str] = {}
332
+ positional: list[str] = []
333
+ i = 0
334
+ while i < len(args):
335
+ if args[i].startswith("--"):
336
+ key = args[i][2:].replace("-", "_")
337
+ if i + 1 < len(args) and not args[i + 1].startswith("--"):
338
+ opts[key] = args[i + 1]
339
+ i += 2
340
+ else:
341
+ opts[key] = "true"
342
+ i += 1
343
+ else:
344
+ positional.append(args[i])
345
+ i += 1
346
+ if positional:
347
+ opts["_positional"] = positional
348
+ return opts
349
+
350
+ opts = _parse_opts(args)
351
+
352
+ def _int(val: str, default: int) -> int:
353
+ try:
354
+ return int(val)
355
+ except (ValueError, TypeError):
356
+ return default
357
+
358
+ try:
359
+ if group == "help":
360
+ return [HELP_TEXT]
361
+
362
+ elif group == "actions":
363
+ if sub == "list":
364
+ return actions_list(
365
+ client,
366
+ name=opts.get("name"),
367
+ limit=_int(opts.get("limit", "20"), 20),
368
+ offset=_int(opts.get("offset", "0"), 0),
369
+ )
370
+ elif sub == "get":
371
+ pos = opts.get("_positional", [])
372
+ if not pos:
373
+ return ["Usage: /actions get <id>"]
374
+ return actions_get(client, id=pos[0])
375
+ else:
376
+ return ["Usage: /actions list | /actions get <id>"]
377
+
378
+ elif group == "runs":
379
+ if sub == "list":
380
+ return runs_list(
381
+ client,
382
+ action_id=opts.get("action_id"),
383
+ action_name=opts.get("action_name"),
384
+ status=opts.get("status"),
385
+ limit=_int(opts.get("limit", "20"), 20),
386
+ offset=_int(opts.get("offset", "0"), 0),
387
+ )
388
+ elif sub == "get":
389
+ pos = opts.get("_positional", [])
390
+ if not pos:
391
+ return ["Usage: /runs get <id>"]
392
+ return runs_get(client, id=pos[0])
393
+ else:
394
+ return ["Usage: /runs list | /runs get <id>"]
395
+
396
+ elif group == "graphs":
397
+ if sub == "list":
398
+ return graphs_list(
399
+ client,
400
+ name=opts.get("name"),
401
+ limit=_int(opts.get("limit", "20"), 20),
402
+ offset=_int(opts.get("offset", "0"), 0),
403
+ )
404
+ elif sub == "get":
405
+ pos = opts.get("_positional", [])
406
+ if not pos:
407
+ return ["Usage: /graphs get <id>"]
408
+ return graphs_get(client, id=pos[0])
409
+ else:
410
+ return ["Usage: /graphs list | /graphs get <id>"]
411
+
412
+ elif group in ("graph-runs", "graph_runs"):
413
+ if sub == "list":
414
+ return graph_runs_list(
415
+ client,
416
+ action_graph_id=opts.get("graph_id"),
417
+ action_graph_name=opts.get("graph_name"),
418
+ status=opts.get("status"),
419
+ limit=_int(opts.get("limit", "20"), 20),
420
+ offset=_int(opts.get("offset", "0"), 0),
421
+ )
422
+ elif sub == "get":
423
+ pos = opts.get("_positional", [])
424
+ if not pos:
425
+ return ["Usage: /graph-runs get <id>"]
426
+ return graph_runs_get(client, id=pos[0])
427
+ else:
428
+ return ["Usage: /graph-runs list | /graph-runs get <id>"]
429
+
430
+ else:
431
+ return [f"Unknown command: /{group}", "", HELP_TEXT]
432
+
433
+ except Exception as e:
434
+ return [f"Command error: {e}"]
kiwi_tui/config.py ADDED
@@ -0,0 +1,79 @@
1
+ """Configuration management for Autobots TUI."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from loguru import logger
7
+ from .models import AppConfig
8
+
9
+
10
+ class ConfigManager:
11
+ """Manages application configuration."""
12
+
13
+ def __init__(self, config_path: Optional[Path] = None):
14
+ """Initialize config manager.
15
+
16
+ Args:
17
+ config_path: Path to config file, defaults to ~/.autobots-tui/config.json
18
+ """
19
+ if config_path is None:
20
+ config_path = Path.home() / ".autobots-tui" / "config.json"
21
+
22
+ self.config_path = config_path
23
+ self._config: Optional[AppConfig] = None
24
+
25
+ def load(self) -> AppConfig:
26
+ """Load configuration from file or create default."""
27
+ if self.config_path.exists():
28
+ try:
29
+ with open(self.config_path, "r") as f:
30
+ data = json.load(f)
31
+ self._config = AppConfig(**data)
32
+ logger.info(f"Configuration loaded from {self.config_path}")
33
+ except Exception as e:
34
+ logger.error(f"Failed to load config: {e}, using defaults")
35
+ self._config = AppConfig()
36
+ else:
37
+ logger.info("No config file found, using defaults")
38
+ self._config = AppConfig()
39
+ self.save()
40
+
41
+ return self._config
42
+
43
+ def save(self) -> None:
44
+ """Save configuration to file."""
45
+ if self._config is None:
46
+ logger.warning("No config to save")
47
+ return
48
+
49
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
50
+
51
+ try:
52
+ with open(self.config_path, "w") as f:
53
+ json.dump(self._config.model_dump(), f, indent=2, default=str)
54
+ logger.info(f"Configuration saved to {self.config_path}")
55
+ except Exception as e:
56
+ logger.error(f"Failed to save config: {e}")
57
+
58
+ @property
59
+ def config(self) -> AppConfig:
60
+ """Get current configuration."""
61
+ if self._config is None:
62
+ return self.load()
63
+ return self._config
64
+
65
+ def update(self, **kwargs) -> None:
66
+ """Update configuration values.
67
+
68
+ Args:
69
+ **kwargs: Configuration fields to update
70
+ """
71
+ if self._config is None:
72
+ self.load()
73
+
74
+ for key, value in kwargs.items():
75
+ if hasattr(self._config, key):
76
+ setattr(self._config, key, value)
77
+ logger.debug(f"Config updated: {key} = {value}")
78
+
79
+ self.save()
kiwi_tui/logger.py ADDED
@@ -0,0 +1,32 @@
1
+ """Logging configuration using loguru."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from loguru import logger
6
+
7
+
8
+ def setup_logging(log_level: str = "INFO", log_file: str = "kiwi_tui.log") -> None:
9
+ """Configure loguru logger for the application.
10
+
11
+ Args:
12
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
13
+ log_file: Path to log file
14
+ """
15
+ # Remove default handler
16
+ logger.remove()
17
+
18
+ # Only log to file, not to console (to avoid interference with Textual UI)
19
+ log_path = Path.home() / ".autobots-tui" / log_file
20
+ log_path.parent.mkdir(parents=True, exist_ok=True)
21
+
22
+ logger.add(
23
+ str(log_path),
24
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
25
+ level=log_level,
26
+ rotation="10 MB",
27
+ retention="7 days",
28
+ compression="zip",
29
+ )
30
+
31
+ logger.info(f"Logging initialized at level {log_level}")
32
+ logger.info(f"Log file: {log_path}")