kiwi-code 0.0.4__tar.gz → 0.0.5__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.
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/Makefile +6 -1
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/PKG-INFO +1 -1
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/pyproject.toml +1 -1
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_runtime/main.py +203 -21
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/main.py +81 -6
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/runtime_manager.py +46 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/screens/login.py +1 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/uv.lock +567 -568
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/.gitignore +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/.python-version +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/README.md +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/poetry.lock +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/auth.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/cli.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/client.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/commands.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/config.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/logger.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/models.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/screens/actions.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/screens/autobots.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/screens/dashboard.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.4 → kiwi_code-0.0.5}/test_hello.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: run dev cli runtime test-hello install sync clean help
|
|
1
|
+
.PHONY: run dev serve cli runtime test-hello install sync clean help
|
|
2
2
|
|
|
3
3
|
# Run the Kiwi TUI application
|
|
4
4
|
run:
|
|
@@ -8,6 +8,10 @@ run:
|
|
|
8
8
|
dev:
|
|
9
9
|
uv run --dev kiwi
|
|
10
10
|
|
|
11
|
+
# Serve the Kiwi TUI in the browser via textual serve (e.g. make serve PORT=8566)
|
|
12
|
+
serve:
|
|
13
|
+
uv run textual serve --port $(or $(PORT),8566) -c "python -m kiwi_tui.main"
|
|
14
|
+
|
|
11
15
|
# Run the Kiwi CLI
|
|
12
16
|
cli:
|
|
13
17
|
uv run kiwicli
|
|
@@ -53,6 +57,7 @@ help:
|
|
|
53
57
|
@echo "Commands:"
|
|
54
58
|
@echo " run - Run the Kiwi TUI application (kiwi)"
|
|
55
59
|
@echo " dev - Run TUI in development mode (with live reload)"
|
|
60
|
+
@echo " serve - Serve the TUI in the browser (PORT=8566 make serve)"
|
|
56
61
|
@echo " cli - Run the Kiwi CLI (kiwicli)"
|
|
57
62
|
@echo " runtime - Run the Kiwi Runtime agent (kiwi-runtime)"
|
|
58
63
|
@echo " e.g. make runtime ARGS=\"connect --server app\""
|
|
@@ -370,9 +370,12 @@ def _is_within_allowed(resolved_path: str, allowed_dirs: list[str]) -> bool:
|
|
|
370
370
|
# Core logic
|
|
371
371
|
# ---------------------------------------------------------------------------
|
|
372
372
|
|
|
373
|
-
async def login(http_base_url: str, email: str, password: str) ->
|
|
374
|
-
"""Authenticate with email/password
|
|
375
|
-
|
|
373
|
+
async def login(http_base_url: str, email: str, password: str) -> dict:
|
|
374
|
+
"""Authenticate with email/password via session API.
|
|
375
|
+
|
|
376
|
+
Returns dict with access_token, refresh_token, and expires_in.
|
|
377
|
+
"""
|
|
378
|
+
url = f"{http_base_url}/v1/auth"
|
|
376
379
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
377
380
|
resp = await client.post(
|
|
378
381
|
url,
|
|
@@ -388,10 +391,41 @@ async def login(http_base_url: str, email: str, password: str) -> str:
|
|
|
388
391
|
raise Exception(f"Login failed (HTTP {resp.status_code}): {detail}")
|
|
389
392
|
|
|
390
393
|
body = resp.json()
|
|
391
|
-
|
|
392
|
-
if not
|
|
393
|
-
raise Exception(f"No
|
|
394
|
-
return
|
|
394
|
+
session = body.get("session")
|
|
395
|
+
if not session or not session.get("access_token"):
|
|
396
|
+
raise Exception(f"No session in response: {body}")
|
|
397
|
+
return {
|
|
398
|
+
"access_token": session["access_token"],
|
|
399
|
+
"refresh_token": session.get("refresh_token"),
|
|
400
|
+
"expires_in": session.get("expires_in"),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def refresh_access_token(http_base_url: str, refresh_token: str) -> dict:
|
|
405
|
+
"""Use a refresh token to obtain a new access token.
|
|
406
|
+
|
|
407
|
+
Returns dict with access_token, refresh_token, and expires_in.
|
|
408
|
+
"""
|
|
409
|
+
url = f"{http_base_url}/v1/auth/session/refresh"
|
|
410
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
411
|
+
resp = await client.post(url, params={"refresh_token": refresh_token})
|
|
412
|
+
if resp.status_code != 200:
|
|
413
|
+
detail = resp.text
|
|
414
|
+
try:
|
|
415
|
+
detail = resp.json().get("detail", resp.text)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
raise Exception(f"Token refresh failed (HTTP {resp.status_code}): {detail}")
|
|
419
|
+
|
|
420
|
+
body = resp.json()
|
|
421
|
+
session = body.get("session")
|
|
422
|
+
if not session or not session.get("access_token"):
|
|
423
|
+
raise Exception(f"No session in refresh response: {body}")
|
|
424
|
+
return {
|
|
425
|
+
"access_token": session["access_token"],
|
|
426
|
+
"refresh_token": session.get("refresh_token"),
|
|
427
|
+
"expires_in": session.get("expires_in"),
|
|
428
|
+
}
|
|
395
429
|
|
|
396
430
|
|
|
397
431
|
async def run_command(
|
|
@@ -668,13 +702,86 @@ class PipeProcess:
|
|
|
668
702
|
pass
|
|
669
703
|
|
|
670
704
|
|
|
705
|
+
def _get_shared_token_path() -> str | None:
|
|
706
|
+
"""Return the path to the shared token file written by the TUI."""
|
|
707
|
+
try:
|
|
708
|
+
from kiwi_tui.runtime_manager import token_path
|
|
709
|
+
return str(token_path())
|
|
710
|
+
except ImportError:
|
|
711
|
+
return None
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _get_shared_refresh_token_path() -> str | None:
|
|
715
|
+
"""Return the path to the shared refresh token file written by the TUI."""
|
|
716
|
+
try:
|
|
717
|
+
from kiwi_tui.runtime_manager import refresh_token_path
|
|
718
|
+
return str(refresh_token_path())
|
|
719
|
+
except ImportError:
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _read_file(path: str | None) -> str | None:
|
|
724
|
+
"""Read a token from a file path, returning None on failure."""
|
|
725
|
+
if not path:
|
|
726
|
+
return None
|
|
727
|
+
try:
|
|
728
|
+
val = open(path).read().strip()
|
|
729
|
+
return val or None
|
|
730
|
+
except OSError:
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _write_file(path: str | None, value: str) -> None:
|
|
735
|
+
"""Write a value to a file with restricted permissions."""
|
|
736
|
+
if not path:
|
|
737
|
+
return
|
|
738
|
+
try:
|
|
739
|
+
with open(path, "w") as f:
|
|
740
|
+
f.write(value)
|
|
741
|
+
os.chmod(path, 0o600)
|
|
742
|
+
except OSError:
|
|
743
|
+
pass
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
async def _watch_token_file(ws, current_token: str, token_file: str | None):
|
|
747
|
+
"""Periodically check the shared token file for updates.
|
|
748
|
+
|
|
749
|
+
When the TUI refreshes the token it writes the new value to disk.
|
|
750
|
+
This coroutine detects the change and sends a fresh auth message.
|
|
751
|
+
"""
|
|
752
|
+
if not token_file:
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
last_token = current_token
|
|
756
|
+
while True:
|
|
757
|
+
await asyncio.sleep(30) # check every 30 seconds
|
|
758
|
+
try:
|
|
759
|
+
new_token = open(token_file).read().strip()
|
|
760
|
+
if new_token and new_token != last_token:
|
|
761
|
+
last_token = new_token
|
|
762
|
+
print_status("~", "Token refreshed by TUI, re-authenticating...", C)
|
|
763
|
+
await ws.send(json.dumps({"type": "auth", "token": new_token}))
|
|
764
|
+
resp = json.loads(await ws.recv())
|
|
765
|
+
if resp.get("type") == "auth_ok":
|
|
766
|
+
print_status(">", "Re-authenticated with refreshed token", GREEN)
|
|
767
|
+
else:
|
|
768
|
+
print_status("!", f"Re-auth response: {resp.get('type')}", YELLOW)
|
|
769
|
+
except (OSError, json.JSONDecodeError):
|
|
770
|
+
pass
|
|
771
|
+
except Exception as e:
|
|
772
|
+
print_status("!", f"Token watch error: {e}", YELLOW)
|
|
773
|
+
|
|
774
|
+
|
|
671
775
|
async def connect(
|
|
672
776
|
ws_url: str,
|
|
673
777
|
token: str,
|
|
674
778
|
mode: str = "restricted",
|
|
675
779
|
allowed_dirs: list[str] | None = None,
|
|
676
|
-
):
|
|
677
|
-
"""Connect to the server WebSocket and process commands.
|
|
780
|
+
) -> str:
|
|
781
|
+
"""Connect to the server WebSocket and process commands.
|
|
782
|
+
|
|
783
|
+
Returns a status string: "ok", "auth_failed", or "error".
|
|
784
|
+
"""
|
|
678
785
|
ws_endpoint = f"{ws_url}/v1/terminal/ws/connect"
|
|
679
786
|
print_status("~", f"Connecting to {BOLD}{ws_endpoint}{RESET}", C)
|
|
680
787
|
|
|
@@ -685,14 +792,14 @@ async def connect(
|
|
|
685
792
|
|
|
686
793
|
if auth_resp.get("type") == "error":
|
|
687
794
|
print_status("x", f"Auth failed: {auth_resp.get('message')}", RED)
|
|
688
|
-
return
|
|
795
|
+
return "auth_failed"
|
|
689
796
|
|
|
690
797
|
if auth_resp.get("type") == "auth_ok":
|
|
691
798
|
user_id = auth_resp.get('user_id', 'unknown')
|
|
692
799
|
print_status(">", f"Connected as {BOLD}{user_id}{RESET}", GREEN)
|
|
693
800
|
else:
|
|
694
801
|
print_status("x", f"Unexpected response: {auth_resp}", RED)
|
|
695
|
-
return
|
|
802
|
+
return "auth_failed"
|
|
696
803
|
|
|
697
804
|
print()
|
|
698
805
|
if mode == "restricted" and allowed_dirs:
|
|
@@ -708,6 +815,12 @@ async def connect(
|
|
|
708
815
|
|
|
709
816
|
active_pty_sessions: dict[str, PTYProcess] = {}
|
|
710
817
|
|
|
818
|
+
# Start background task to watch for token refreshes from TUI
|
|
819
|
+
token_file = _get_shared_token_path()
|
|
820
|
+
token_watcher = asyncio.create_task(
|
|
821
|
+
_watch_token_file(ws, token, token_file)
|
|
822
|
+
)
|
|
823
|
+
|
|
711
824
|
try:
|
|
712
825
|
async for message in ws:
|
|
713
826
|
msg = json.loads(message)
|
|
@@ -817,6 +930,7 @@ async def connect(
|
|
|
817
930
|
else:
|
|
818
931
|
print_status("?", f"Unknown message: {msg_type}", YELLOW)
|
|
819
932
|
finally:
|
|
933
|
+
token_watcher.cancel()
|
|
820
934
|
for sid, pty_proc in active_pty_sessions.items():
|
|
821
935
|
try:
|
|
822
936
|
await pty_proc.close()
|
|
@@ -826,12 +940,17 @@ async def connect(
|
|
|
826
940
|
except websockets.exceptions.ConnectionClosed as e:
|
|
827
941
|
print()
|
|
828
942
|
print_status("!", f"Connection closed: {e}", YELLOW)
|
|
943
|
+
return "error"
|
|
829
944
|
except ConnectionRefusedError:
|
|
830
945
|
print()
|
|
831
946
|
print_status("x", f"Could not connect to {ws_endpoint}. Is the server running?", RED)
|
|
947
|
+
return "error"
|
|
832
948
|
except Exception as e:
|
|
833
949
|
print()
|
|
834
950
|
print_status("x", f"Error: {e}", RED)
|
|
951
|
+
return "error"
|
|
952
|
+
|
|
953
|
+
return "ok"
|
|
835
954
|
|
|
836
955
|
|
|
837
956
|
def main():
|
|
@@ -929,7 +1048,12 @@ def main():
|
|
|
929
1048
|
|
|
930
1049
|
loop = asyncio.new_event_loop()
|
|
931
1050
|
|
|
1051
|
+
token_file = _get_shared_token_path()
|
|
1052
|
+
refresh_file = _get_shared_refresh_token_path()
|
|
1053
|
+
|
|
932
1054
|
provided_token = args.token or os.environ.get("KIWI_AUTH_TOKEN")
|
|
1055
|
+
refresh_token = _read_file(refresh_file) # May have been saved by TUI
|
|
1056
|
+
|
|
933
1057
|
if provided_token:
|
|
934
1058
|
# Use provided token — skip interactive login
|
|
935
1059
|
token = provided_token
|
|
@@ -947,8 +1071,14 @@ def main():
|
|
|
947
1071
|
|
|
948
1072
|
print_status("~", f"Authenticating with {BOLD}{http_url}{RESET}...", C)
|
|
949
1073
|
try:
|
|
950
|
-
|
|
1074
|
+
session = loop.run_until_complete(login(http_url, email, password))
|
|
1075
|
+
token = session["access_token"]
|
|
1076
|
+
refresh_token = session.get("refresh_token")
|
|
951
1077
|
print_status(">", "Login successful!", GREEN)
|
|
1078
|
+
# Save tokens to shared files
|
|
1079
|
+
_write_file(token_file, token)
|
|
1080
|
+
if refresh_token:
|
|
1081
|
+
_write_file(refresh_file, refresh_token)
|
|
952
1082
|
except Exception as e:
|
|
953
1083
|
print_status("x", f"Login failed: {e}", RED)
|
|
954
1084
|
loop.close()
|
|
@@ -970,16 +1100,68 @@ def main():
|
|
|
970
1100
|
|
|
971
1101
|
signal.signal(signal.SIGINT, lambda *_: shutdown())
|
|
972
1102
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1103
|
+
# Auto-reconnect loop with token refresh
|
|
1104
|
+
backoff = 5
|
|
1105
|
+
max_backoff = 60
|
|
1106
|
+
|
|
1107
|
+
while not _shutting_down:
|
|
1108
|
+
# Read latest tokens from shared files before each attempt
|
|
1109
|
+
fresh_token = _read_file(token_file)
|
|
1110
|
+
if fresh_token:
|
|
1111
|
+
token = fresh_token
|
|
1112
|
+
fresh_refresh = _read_file(refresh_file)
|
|
1113
|
+
if fresh_refresh:
|
|
1114
|
+
refresh_token = fresh_refresh
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
status = loop.run_until_complete(
|
|
1118
|
+
connect(ws_url, token, mode=mode, allowed_dirs=allowed_dirs)
|
|
1119
|
+
)
|
|
1120
|
+
except asyncio.CancelledError:
|
|
1121
|
+
break
|
|
1122
|
+
except Exception:
|
|
1123
|
+
status = "error"
|
|
1124
|
+
|
|
1125
|
+
if _shutting_down:
|
|
1126
|
+
break
|
|
1127
|
+
|
|
1128
|
+
# On auth failure, try to refresh the token before reconnecting
|
|
1129
|
+
if status == "auth_failed" and refresh_token:
|
|
1130
|
+
print_status("~", "Access token expired, refreshing...", C)
|
|
1131
|
+
try:
|
|
1132
|
+
session = loop.run_until_complete(
|
|
1133
|
+
refresh_access_token(http_url, refresh_token)
|
|
1134
|
+
)
|
|
1135
|
+
token = session["access_token"]
|
|
1136
|
+
if session.get("refresh_token"):
|
|
1137
|
+
refresh_token = session["refresh_token"]
|
|
1138
|
+
print_status(">", "Token refreshed successfully", GREEN)
|
|
1139
|
+
# Save refreshed tokens to shared files
|
|
1140
|
+
_write_file(token_file, token)
|
|
1141
|
+
if refresh_token:
|
|
1142
|
+
_write_file(refresh_file, refresh_token)
|
|
1143
|
+
backoff = 2 # Quick retry after successful refresh
|
|
1144
|
+
except Exception as e:
|
|
1145
|
+
print_status("x", f"Token refresh failed: {e}", RED)
|
|
1146
|
+
|
|
1147
|
+
# Reset backoff after a successful session
|
|
1148
|
+
if status == "ok":
|
|
1149
|
+
backoff = 5
|
|
1150
|
+
|
|
982
1151
|
print()
|
|
1152
|
+
print_section("Reconnecting")
|
|
1153
|
+
print_status("~", f"Reconnecting in {backoff}s...", C)
|
|
1154
|
+
try:
|
|
1155
|
+
loop.run_until_complete(asyncio.sleep(backoff))
|
|
1156
|
+
except asyncio.CancelledError:
|
|
1157
|
+
break
|
|
1158
|
+
backoff = min(backoff * 2, max_backoff)
|
|
1159
|
+
|
|
1160
|
+
loop.close()
|
|
1161
|
+
print()
|
|
1162
|
+
print(f" {GREY}{'─' * 50}{RESET}")
|
|
1163
|
+
print_status(">", "Disconnected. Goodbye!", C)
|
|
1164
|
+
print()
|
|
983
1165
|
else:
|
|
984
1166
|
print_banner()
|
|
985
1167
|
parser.print_help()
|
|
@@ -131,6 +131,7 @@ class AutobotsTUI(App):
|
|
|
131
131
|
if self.token_manager.is_authenticated():
|
|
132
132
|
logger.info("User is authenticated, showing dashboard")
|
|
133
133
|
self._start_kiwi_session()
|
|
134
|
+
self._start_token_refresh()
|
|
134
135
|
try:
|
|
135
136
|
self.switch_screen("dashboard")
|
|
136
137
|
except IndexError:
|
|
@@ -270,6 +271,53 @@ class AutobotsTUI(App):
|
|
|
270
271
|
self._kiwi_pid = None
|
|
271
272
|
self._close_log_tail()
|
|
272
273
|
|
|
274
|
+
# ------------------------------------------------------------------
|
|
275
|
+
# Periodic token refresh
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def _start_token_refresh(self) -> None:
|
|
279
|
+
"""Schedule periodic token refresh (every `refresh_interval` minutes)."""
|
|
280
|
+
interval_min = self.config.refresh_interval # default 5
|
|
281
|
+
logger.info(f"Token refresh scheduled every {interval_min} minutes")
|
|
282
|
+
self.set_interval(interval_min * 60, self._refresh_token_if_needed)
|
|
283
|
+
# Write current tokens to shared files so runtime can read them
|
|
284
|
+
if self._kiwi_token:
|
|
285
|
+
runtime_manager.save_token(self._kiwi_token)
|
|
286
|
+
tokens = self.token_manager.load_tokens()
|
|
287
|
+
if tokens and tokens.refresh_token:
|
|
288
|
+
runtime_manager.save_refresh_token(tokens.refresh_token)
|
|
289
|
+
|
|
290
|
+
def _refresh_token_if_needed(self) -> None:
|
|
291
|
+
"""Check token expiry and refresh if needed."""
|
|
292
|
+
tokens = self.token_manager.load_tokens()
|
|
293
|
+
if not tokens:
|
|
294
|
+
logger.debug("No tokens to refresh")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if not tokens.is_expired():
|
|
298
|
+
logger.debug("Token still valid, skipping refresh")
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
logger.info("Token expired or near expiry, refreshing...")
|
|
302
|
+
success, new_tokens, message = self.autobots_client.refresh_token(
|
|
303
|
+
tokens.refresh_token
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if success and new_tokens:
|
|
307
|
+
logger.info("Token refreshed successfully")
|
|
308
|
+
# Save to disk
|
|
309
|
+
self.token_manager.save_tokens(new_tokens)
|
|
310
|
+
# Update TUI client
|
|
311
|
+
self.autobots_client.update_token(new_tokens.access_token)
|
|
312
|
+
# Update shared state
|
|
313
|
+
self._kiwi_token = new_tokens.access_token
|
|
314
|
+
# Write to runtime directory so the runtime process can pick it up
|
|
315
|
+
runtime_manager.save_token(new_tokens.access_token)
|
|
316
|
+
if new_tokens.refresh_token:
|
|
317
|
+
runtime_manager.save_refresh_token(new_tokens.refresh_token)
|
|
318
|
+
else:
|
|
319
|
+
logger.error(f"Token refresh failed: {message}")
|
|
320
|
+
|
|
273
321
|
# ------------------------------------------------------------------
|
|
274
322
|
|
|
275
323
|
def action_runtime_logs(self) -> None:
|
|
@@ -315,23 +363,50 @@ class AutobotsTUI(App):
|
|
|
315
363
|
self.exit()
|
|
316
364
|
|
|
317
365
|
|
|
318
|
-
def
|
|
319
|
-
"""Run the
|
|
320
|
-
# Setup logging
|
|
366
|
+
def _run_tui():
|
|
367
|
+
"""Run the TUI in the terminal."""
|
|
321
368
|
config_manager = ConfigManager()
|
|
322
369
|
config = config_manager.load()
|
|
323
370
|
setup_logging(log_level=config.log_level)
|
|
324
371
|
|
|
325
|
-
logger.info("="*60)
|
|
372
|
+
logger.info("=" * 60)
|
|
326
373
|
logger.info("Starting Autobots TUI")
|
|
327
|
-
logger.info("="*60)
|
|
374
|
+
logger.info("=" * 60)
|
|
328
375
|
|
|
329
|
-
# Run app
|
|
330
376
|
app = AutobotsTUI(config_manager=config_manager)
|
|
331
377
|
app.run()
|
|
332
378
|
|
|
333
379
|
logger.info("Autobots TUI terminated")
|
|
334
380
|
|
|
335
381
|
|
|
382
|
+
def _serve_tui(port: int = 8566):
|
|
383
|
+
"""Serve the TUI in the browser via textual serve."""
|
|
384
|
+
import sys
|
|
385
|
+
from textual_serve.server import Server
|
|
386
|
+
|
|
387
|
+
server = Server(f"{sys.executable} -m kiwi_tui.main", port=port)
|
|
388
|
+
server.serve()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def main():
|
|
392
|
+
"""Entry point for the kiwi command."""
|
|
393
|
+
import argparse
|
|
394
|
+
|
|
395
|
+
parser = argparse.ArgumentParser(prog="kiwi", description="Kiwi Code TUI")
|
|
396
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
397
|
+
|
|
398
|
+
serve_parser = subparsers.add_parser("serve", help="Serve the TUI in the browser")
|
|
399
|
+
serve_parser.add_argument(
|
|
400
|
+
"--port", "-p", type=int, default=8566, help="Port to serve on (default: 8566)"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
args = parser.parse_args()
|
|
404
|
+
|
|
405
|
+
if args.command == "serve":
|
|
406
|
+
_serve_tui(port=args.port)
|
|
407
|
+
else:
|
|
408
|
+
_run_tui()
|
|
409
|
+
|
|
410
|
+
|
|
336
411
|
if __name__ == "__main__":
|
|
337
412
|
main()
|
|
@@ -58,6 +58,52 @@ def log_path(cwd: str | None = None) -> Path:
|
|
|
58
58
|
return runtime_dir(cwd) / "log"
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def token_path(cwd: str | None = None) -> Path:
|
|
62
|
+
"""Return the shared token file path for a directory's runtime."""
|
|
63
|
+
return runtime_dir(cwd) / "token"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def refresh_token_path(cwd: str | None = None) -> Path:
|
|
67
|
+
"""Return the shared refresh token file path for a directory's runtime."""
|
|
68
|
+
return runtime_dir(cwd) / "refresh_token"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def save_token(token: str, cwd: str | None = None) -> None:
|
|
72
|
+
"""Write a refreshed access token so the runtime can pick it up."""
|
|
73
|
+
tp = token_path(cwd)
|
|
74
|
+
tp.write_text(token)
|
|
75
|
+
tp.chmod(0o600)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def save_refresh_token(token: str, cwd: str | None = None) -> None:
|
|
79
|
+
"""Write the refresh token so the runtime can refresh on its own."""
|
|
80
|
+
tp = refresh_token_path(cwd)
|
|
81
|
+
tp.write_text(token)
|
|
82
|
+
tp.chmod(0o600)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_token(cwd: str | None = None) -> str | None:
|
|
86
|
+
"""Read the shared access token (returns None if missing)."""
|
|
87
|
+
tp = token_path(cwd)
|
|
88
|
+
if not tp.exists():
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
return tp.read_text().strip() or None
|
|
92
|
+
except OSError:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def read_refresh_token(cwd: str | None = None) -> str | None:
|
|
97
|
+
"""Read the shared refresh token (returns None if missing)."""
|
|
98
|
+
tp = refresh_token_path(cwd)
|
|
99
|
+
if not tp.exists():
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
return tp.read_text().strip() or None
|
|
103
|
+
except OSError:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
61
107
|
def stop_runtime(cwd: str | None = None) -> bool:
|
|
62
108
|
"""Stop the runtime for a directory. Returns True if a process was stopped."""
|
|
63
109
|
pid = get_running_pid(cwd)
|
|
@@ -280,6 +280,7 @@ class LoginScreen(Screen):
|
|
|
280
280
|
# Share token with kiwi CLI session and start it
|
|
281
281
|
self.app._kiwi_token = tokens.access_token
|
|
282
282
|
self.app._start_kiwi_session()
|
|
283
|
+
self.app._start_token_refresh()
|
|
283
284
|
|
|
284
285
|
# Notify app of successful login
|
|
285
286
|
self.app.post_message(LoginSuccess(tokens))
|