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/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}")
|