kiwi-code 0.0.443__tar.gz → 0.0.445__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.
Files changed (69) hide show
  1. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/__init__.py +1 -1
  4. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/cli.py +14 -38
  5. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/commands.py +1 -160
  6. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_runtime/__init__.py +1 -1
  7. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_runtime/main.py +52 -3
  8. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/__init__.py +1 -1
  9. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/main.py +174 -62
  10. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/runtime_agent.py +10 -21
  11. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/dashboard.py +54 -360
  12. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/term_dashboard.py +231 -18
  13. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/slash_commands.py +36 -6
  14. kiwi_code-0.0.445/src/kiwi_tui/ui_settings.py +40 -0
  15. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_term_dashboard_ui.py +162 -0
  16. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/uv.lock +1 -1
  17. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.github/workflows/publish.yml +0 -0
  18. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.github/workflows/test.yml +0 -0
  19. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.gitignore +0 -0
  20. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.python-version +0 -0
  21. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/CLAUDE.md +0 -0
  22. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/Makefile +0 -0
  23. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/README.md +0 -0
  24. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/auth.py +0 -0
  25. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/checkpoints.py +0 -0
  26. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/client.py +0 -0
  27. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/logger.py +0 -0
  28. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/models.py +0 -0
  29. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/runtime_manager.py +0 -0
  30. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/server.py +0 -0
  31. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/terminal_mode.py +0 -0
  32. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_runtime/__main__.py +0 -0
  33. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/inline_file_picker.py +0 -0
  34. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/random_words.py +0 -0
  35. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/__init__.py +0 -0
  36. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/attach_content.py +0 -0
  37. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/command_result.py +0 -0
  38. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/detach_files.py +0 -0
  39. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/file_browser.py +0 -0
  40. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/help.py +0 -0
  41. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/id_picker.py +0 -0
  42. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/login.py +0 -0
  43. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  44. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  45. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/slash_picker.py +0 -0
  46. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/status_words.py +0 -0
  47. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/term_app.py +0 -0
  48. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/widgets.py +0 -0
  49. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/worktrees.py +0 -0
  50. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/test_hello.py +0 -0
  51. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/__init__.py +0 -0
  52. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/conftest.py +0 -0
  53. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_batch_fs_tool.py +0 -0
  54. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_checkpoints.py +0 -0
  55. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_cli_help.py +0 -0
  56. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_copy_path_fs_tool.py +0 -0
  57. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_imports.py +0 -0
  58. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_read_file_streaming.py +0 -0
  59. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_reexec_kiwi.py +0 -0
  60. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_runtime_log_trimming.py +0 -0
  61. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_search_in_files_fs_tool.py +0 -0
  62. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_slash_commands.py +0 -0
  63. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_terminal_mode.py +0 -0
  64. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tokens.py +0 -0
  65. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tui_headless.py +0 -0
  66. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tui_interactive_runtime.py +0 -0
  67. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tui_palette.py +0 -0
  68. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_unified_diff.py +0 -0
  69. {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_worktrees.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.443
3
+ Version: 0.0.445
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://meetkiwi.ai
6
6
  Project-URL: Repository, https://github.com/jetoslabs/kiwi-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.443"
3
+ version = "0.0.445"
4
4
  description = "A textual-based terminal user interface application"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  requires-python = ">=3.11,<4.0"
@@ -1,3 +1,3 @@
1
1
  """Kiwi CLI - command-line interface and shared infrastructure modules."""
2
2
 
3
- __version__ = "0.0.443"
3
+ __version__ = "0.0.445"
@@ -8,23 +8,26 @@ from .auth import TokenManager
8
8
  from .models import AppConfig
9
9
  from .server import http_url_from_server
10
10
  from . import commands
11
+ from . import __version__ as KIWI_CLI_VERSION
11
12
 
12
13
  app = typer.Typer(name="kiwi", help="Kiwi — interact with the Kiwi AI platform.")
13
14
 
14
15
  actions_app = typer.Typer(name="actions", help="Manage actions.")
15
16
  action_runs_app = typer.Typer(name="runs", help="Manage action runs (results).")
16
- graphs_app = typer.Typer(name="graphs", help="Manage action graphs.")
17
- graph_runs_app = typer.Typer(name="graph-runs", help="Manage action graph runs (results).")
18
17
 
19
18
  app.add_typer(actions_app)
20
19
  app.add_typer(action_runs_app)
21
- app.add_typer(graphs_app)
22
- app.add_typer(graph_runs_app)
23
20
 
24
21
  # NOTE: kiwi-runtime is a standalone executable; this CLI does not manage it.
25
22
 
26
23
  _SERVER: str | None = None
27
24
 
25
+ def _version_callback(value: bool) -> None:
26
+ if value:
27
+ typer.echo(f"kiwicli {KIWI_CLI_VERSION}")
28
+ raise typer.Exit()
29
+
30
+
28
31
  @app.callback()
29
32
  def _main(
30
33
  server: Optional[str] = typer.Option(
@@ -35,6 +38,13 @@ def _main(
35
38
  "or a full URL (e.g. https://custom.server.com)."
36
39
  ),
37
40
  ),
41
+ version: Optional[bool] = typer.Option(
42
+ None,
43
+ "--version",
44
+ callback=_version_callback,
45
+ is_eager=True,
46
+ help="Show version and exit.",
47
+ ),
38
48
  ):
39
49
  """Global options for `kiwicli`."""
40
50
  global _SERVER
@@ -181,40 +191,6 @@ def runs_get_cmd(id: str = typer.Argument(help="Action run ID")):
181
191
  _print(_run_with_refresh_retry(lambda c: commands.runs_get(c, id=id)))
182
192
 
183
193
 
184
- # -- graphs -----------------------------------------------------------------
185
-
186
- @graphs_app.command("list")
187
- def graphs_list_cmd(
188
- name: Optional[str] = typer.Option(None, help="Filter by name"),
189
- limit: int = typer.Option(20, help="Max results"),
190
- offset: int = typer.Option(0, help="Offset"),
191
- ):
192
- """List action graphs."""
193
- _print(_run_with_refresh_retry(lambda c: commands.graphs_list(c, name=name, limit=limit, offset=offset)))
194
-
195
- @graphs_app.command("get")
196
- def graphs_get_cmd(id: str = typer.Argument(help="Action graph ID")):
197
- """Get action graph details."""
198
- _print(_run_with_refresh_retry(lambda c: commands.graphs_get(c, id=id)))
199
-
200
-
201
- # -- graph-runs -------------------------------------------------------------
202
-
203
- @graph_runs_app.command("list")
204
- def graph_runs_list_cmd(
205
- graph_id: Optional[str] = typer.Option(None, "--graph-id", help="Filter by graph ID"),
206
- graph_name: Optional[str] = typer.Option(None, "--graph-name", help="Filter by graph name"),
207
- status: Optional[str] = typer.Option(None, help="Filter by status"),
208
- limit: int = typer.Option(20, help="Max results"),
209
- offset: int = typer.Option(0, help="Offset"),
210
- ):
211
- """List action graph runs (results)."""
212
- _print(_run_with_refresh_retry(lambda c: commands.graph_runs_list(c, action_graph_id=graph_id, action_graph_name=graph_name, status=status, limit=limit, offset=offset)))
213
-
214
- @graph_runs_app.command("get")
215
- def graph_runs_get_cmd(id: str = typer.Argument(help="Graph run ID")):
216
- """Get action graph run details."""
217
- _print(_run_with_refresh_retry(lambda c: commands.graph_runs_get(c, id=id)))
218
194
 
219
195
 
220
196
  # -- runtime ----------------------------------------------------------------
@@ -14,14 +14,7 @@ from autobots_client.api.action_results import (
14
14
  list_action_result_v1_action_results_get,
15
15
  get_action_result_v1_action_results_id_get,
16
16
  )
17
- from autobots_client.api.action_graphs import (
18
- list_action_graphs_v1_action_graphs_get,
19
- get_action_graph_v1_action_graphs_id_get,
20
- )
21
- from autobots_client.api.action_graphs_results import (
22
- list_action_graph_result_v1_action_graphs_results_get,
23
- get_action_graph_result_v1_action_graphs_results_id_get,
24
- )
17
+
25
18
  from autobots_client.types import UNSET
26
19
 
27
20
 
@@ -157,122 +150,7 @@ def runs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
157
150
  return lines
158
151
 
159
152
 
160
- # graphs (action graphs)
161
-
162
- def graphs_list(client: AuthenticatedClient, *, name: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
163
- """List action graphs. Returns output lines."""
164
- resp = list_action_graphs_v1_action_graphs_get.sync_detailed(
165
- client=client,
166
- name=name if name else UNSET,
167
- limit=limit,
168
- offset=offset,
169
- )
170
- if resp.status_code != 200:
171
- return [f"Error: HTTP {resp.status_code}"]
172
-
173
- result = resp.parsed
174
- lines = [
175
- f"Action Graphs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}):",
176
- "",
177
- f"{'ID':<28} {'Name':<30} {'Ver':<6} {'Published':<10} {'Created'}",
178
- "-" * 105,
179
- ]
180
- for doc in result.docs:
181
- lines.append(
182
- f"{doc.field_id:<28} {_val(doc.name):<30} {_val(doc.version):<6} "
183
- f"{_val(doc.is_published):<10} {_fmt_date(doc.created_at)}"
184
- )
185
- return lines
186
-
187
-
188
- def graphs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
189
- """Get action graph details. Returns output lines."""
190
- resp = get_action_graph_v1_action_graphs_id_get.sync_detailed(id=id, client=client)
191
- if resp.status_code != 200:
192
- return [f"Error: HTTP {resp.status_code}"]
193
-
194
- doc = resp.parsed
195
- if hasattr(doc, 'field_id'):
196
- return [
197
- f"ID: {doc.field_id}",
198
- f"Name: {_val(getattr(doc, 'name', ''))}",
199
- f"Version: {_val(getattr(doc, 'version', ''))}",
200
- f"Description: {_val(getattr(doc, 'description', ''))}",
201
- f"Published: {_val(getattr(doc, 'is_published', ''))}",
202
- f"Created: {_fmt_date(getattr(doc, 'created_at', None))}",
203
- f"Updated: {_fmt_date(getattr(doc, 'updated_at', None))}",
204
- ]
205
- import json
206
- if hasattr(doc, 'to_dict'):
207
- return [json.dumps(doc.to_dict(), indent=2, default=str)]
208
- return [str(doc)]
209
-
210
-
211
- # graph-runs (action graph results)
212
-
213
- def graph_runs_list(client: AuthenticatedClient, *, action_graph_id: Optional[str] = None,
214
- action_graph_name: Optional[str] = None, status: Optional[str] = None,
215
- limit: int = 20, offset: int = 0) -> list[str]:
216
- """List action graph runs. Returns output lines."""
217
- from autobots_client.models.event_result_status import EventResultStatus
218
-
219
- status_enum = UNSET
220
- if status:
221
- try:
222
- status_enum = EventResultStatus(status.lower())
223
- except ValueError:
224
- return [f"Invalid status: {status}. Valid: processing, success, error, stuck, waiting"]
225
-
226
- resp = list_action_graph_result_v1_action_graphs_results_get.sync_detailed(
227
- client=client,
228
- action_graph_id=action_graph_id if action_graph_id else UNSET,
229
- action_graph_name=action_graph_name if action_graph_name else UNSET,
230
- status=status_enum,
231
- limit=limit,
232
- offset=offset,
233
- )
234
- if resp.status_code != 200:
235
- return [f"Error: HTTP {resp.status_code}"]
236
-
237
- result = resp.parsed
238
- lines = [
239
- f"Action Graph Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}):",
240
- "",
241
- f"{'ID':<28} {'Status':<12} {'Name':<30} {'Created':<18} {'Updated'}",
242
- "-" * 105,
243
- ]
244
- for doc in result.docs:
245
- lines.append(
246
- f"{doc.field_id:<28} {doc.status.value:<12} {_val(doc.name):<30} "
247
- f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
248
- )
249
- return lines
250
-
251
-
252
- def graph_runs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
253
- """Get action graph run details. Returns output lines."""
254
- resp = get_action_graph_result_v1_action_graphs_results_id_get.sync_detailed(id=id, client=client)
255
- if resp.status_code != 200:
256
- return [f"Error: HTTP {resp.status_code}"]
257
153
 
258
- doc = resp.parsed
259
- lines = [
260
- f"ID: {doc.field_id}",
261
- f"Status: {doc.status.value}",
262
- f"Name: {_val(doc.name)}",
263
- f"Type: {doc.type_.value}",
264
- f"Saved: {_val(doc.is_saved)}",
265
- f"Created: {_fmt_date(doc.created_at)}",
266
- f"Updated: {_fmt_date(doc.updated_at)}",
267
- ]
268
- if doc.result and not isinstance(doc.result, type(UNSET)):
269
- lines += [
270
- "",
271
- "Action Graph:",
272
- f" Name: {doc.result.name}",
273
- f" ID: {doc.result.field_id}",
274
- ]
275
- return lines
276
154
 
277
155
 
278
156
  # Command dispatcher — parses "/command args" from TUI input
@@ -309,10 +187,6 @@ Query:
309
187
  /autocode-select Choose the version of AutoCode you wish to use
310
188
  /runs list [--action-id ID] [--status STATUS] [--limit N]
311
189
  /runs get <id>
312
- /graphs list [--name NAME] [--limit N]
313
- /graphs get <id>
314
- /graph-runs list [--graph-id ID] [--status STATUS] [--limit N]
315
- /graph-runs get <id>
316
190
 
317
191
  /help Show this help"""
318
192
 
@@ -400,39 +274,6 @@ def dispatch(command_str: str, client: AuthenticatedClient) -> list[str]:
400
274
  else:
401
275
  return ["Usage: /runs list | /runs get <id>"]
402
276
 
403
- elif group == "graphs":
404
- if sub == "list":
405
- return graphs_list(
406
- client,
407
- name=opts.get("name"),
408
- limit=_int(opts.get("limit", "20"), 20),
409
- offset=_int(opts.get("offset", "0"), 0),
410
- )
411
- elif sub == "get":
412
- pos = opts.get("_positional", [])
413
- if not pos:
414
- return ["Usage: /graphs get <id>"]
415
- return graphs_get(client, id=pos[0])
416
- else:
417
- return ["Usage: /graphs list | /graphs get <id>"]
418
-
419
- elif group in ("graph-runs", "graph_runs"):
420
- if sub == "list":
421
- return graph_runs_list(
422
- client,
423
- action_graph_id=opts.get("graph_id"),
424
- action_graph_name=opts.get("graph_name"),
425
- status=opts.get("status"),
426
- limit=_int(opts.get("limit", "20"), 20),
427
- offset=_int(opts.get("offset", "0"), 0),
428
- )
429
- elif sub == "get":
430
- pos = opts.get("_positional", [])
431
- if not pos:
432
- return ["Usage: /graph-runs get <id>"]
433
- return graph_runs_get(client, id=pos[0])
434
- else:
435
- return ["Usage: /graph-runs list | /graph-runs get <id>"]
436
277
 
437
278
  else:
438
279
  return [f"Unknown command: /{group}", "", HELP_TEXT]
@@ -1,3 +1,3 @@
1
1
  """Kiwi Runtime — terminal agent that connects to the server via WebSocket."""
2
2
 
3
- __version__ = "0.0.443"
3
+ __version__ = "0.0.445"
@@ -46,6 +46,7 @@ if not IS_WINDOWS:
46
46
  import httpx
47
47
  import websockets
48
48
  import threading
49
+ from kiwi_runtime import __version__ as KIWI_RUNTIME_VERSION
49
50
  FS_MAX_CONCURRENCY = 8
50
51
 
51
52
  # Shared token storage (used by both kiwi-code and standalone kiwi-runtime)
@@ -55,6 +56,11 @@ TOKEN_EXPIRY_SKEW_SEC = 60 # refresh a bit early to avoid edge cases
55
56
  KIWI_TMP_DIR = Path.home() / ".kiwi" / "tmp"
56
57
 
57
58
 
59
+ class _AuthRetry(Exception):
60
+ def __init__(self, token: str, reason: str | None = None) -> None:
61
+ super().__init__(reason or "auth_retry")
62
+ self.token = token
63
+
58
64
  def _parse_expires_at(value: Any) -> datetime | None:
59
65
  """Parse expires_at from tokens.json.
60
66
 
@@ -4226,6 +4232,8 @@ async def connect(
4226
4232
  allowed_dirs: list[str] | None = None,
4227
4233
  agent_id: str | None = None,
4228
4234
  consumer_id: str | None = None,
4235
+ relogin_email: str | None = None,
4236
+ relogin_password: str | None = None,
4229
4237
  *,
4230
4238
  max_retries: int = 12,
4231
4239
  ):
@@ -4312,6 +4320,8 @@ async def connect(
4312
4320
  allowed_dirs=allowed_dirs,
4313
4321
  agent_id=agent_id,
4314
4322
  consumer_id=consumer_id,
4323
+ relogin_email=relogin_email,
4324
+ relogin_password=relogin_password,
4315
4325
  command_queue=command_queue,
4316
4326
  pending_results=pending_results,
4317
4327
  pending_lock=pending_lock,
@@ -4321,6 +4331,9 @@ async def connect(
4321
4331
  return
4322
4332
  except asyncio.CancelledError:
4323
4333
  raise
4334
+ except _AuthRetry as e:
4335
+ token = str(e.token or token)
4336
+ continue
4324
4337
  except websockets.exceptions.ConnectionClosed as e:
4325
4338
  print()
4326
4339
  print_status("!", f"Connection closed: {e}", YELLOW)
@@ -4358,6 +4371,8 @@ async def _connect_once(
4358
4371
  allowed_dirs: list[str] | None = None,
4359
4372
  agent_id: str | None = None,
4360
4373
  consumer_id: str | None = None,
4374
+ relogin_email: str | None = None,
4375
+ relogin_password: str | None = None,
4361
4376
  *,
4362
4377
  command_queue: asyncio.Queue[tuple[str, str]],
4363
4378
  pending_results: deque[dict[str, Any]],
@@ -4372,7 +4387,7 @@ async def _connect_once(
4372
4387
  ws_endpoint,
4373
4388
  ping_interval=20,
4374
4389
  ping_timeout=90,
4375
- close_timeout=10,
4390
+ close_timeout=60,
4376
4391
  max_queue=64,
4377
4392
  ) as ws:
4378
4393
 
@@ -4395,7 +4410,22 @@ async def _connect_once(
4395
4410
  await ws.send(json.dumps(auth_msg))
4396
4411
  auth_resp = json.loads(await ws.recv())
4397
4412
  if auth_resp.get("type") == "error":
4398
- print_status("x", f"Auth failed: {auth_resp.get('message')}", RED)
4413
+ err_msg = str(auth_resp.get("message") or "").strip()
4414
+ print_status("x", f"Auth failed: {err_msg}", RED)
4415
+
4416
+ should_relogin = (
4417
+ bool(relogin_email)
4418
+ and bool(relogin_password)
4419
+ and any(x in err_msg.lower() for x in ("expired", "invalid"))
4420
+ )
4421
+ if should_relogin:
4422
+ try:
4423
+ new_token = await login(http_base_url, str(relogin_email), str(relogin_password))
4424
+ raise _AuthRetry(new_token, reason=err_msg)
4425
+ except _AuthRetry:
4426
+ raise
4427
+ except Exception as e:
4428
+ print_status("x", f"Re-authentication failed: {e}", RED)
4399
4429
  return
4400
4430
 
4401
4431
  if auth_resp.get("type") == "auth_ok":
@@ -4856,6 +4886,8 @@ def main():
4856
4886
  subparsers = parser.add_subparsers(dest="command")
4857
4887
 
4858
4888
  connect_parser = subparsers.add_parser("connect", help="Connect to the server")
4889
+
4890
+ parser.add_argument("--version", action="version", version=f"kiwi-runtime {KIWI_RUNTIME_VERSION}")
4859
4891
  connect_parser.add_argument(
4860
4892
  "--server",
4861
4893
  default="app",
@@ -4955,6 +4987,10 @@ def main():
4955
4987
  loop = asyncio.new_event_loop()
4956
4988
  agent_id = str(uuid.uuid4())
4957
4989
 
4990
+
4991
+ relogin_email: str | None = None
4992
+ relogin_password: str | None = None
4993
+
4958
4994
  provided_token = args.token or os.environ.get("KIWI_AUTH_TOKEN")
4959
4995
  if provided_token:
4960
4996
  # Use provided token — skip interactive login
@@ -4970,6 +5006,9 @@ def main():
4970
5006
  else:
4971
5007
  email = input(f" {C}>{RESET} Email: ")
4972
5008
  password = getpass.getpass(f" {C}>{RESET} Password: ")
5009
+ # Keep credentials in-memory so we can re-authenticate on token expiry.
5010
+ relogin_email = email
5011
+ relogin_password = password
4973
5012
  print()
4974
5013
  print_status(">", f"Agent ID: {BOLD}{agent_id}{RESET}", GREY)
4975
5014
 
@@ -5006,7 +5045,17 @@ def main():
5006
5045
  signal.signal(signal.SIGINT, lambda *_: shutdown())
5007
5046
 
5008
5047
  try:
5009
- loop.run_until_complete(connect(ws_url, token, http_base_url=http_url, mode=mode, allowed_dirs=allowed_dirs, agent_id=agent_id, consumer_id=args.run_id,))
5048
+ loop.run_until_complete(connect(
5049
+ ws_url,
5050
+ token,
5051
+ http_base_url=http_url,
5052
+ mode=mode,
5053
+ allowed_dirs=allowed_dirs,
5054
+ agent_id=agent_id,
5055
+ consumer_id=args.run_id,
5056
+ relogin_email=relogin_email,
5057
+ relogin_password=relogin_password,
5058
+ ))
5010
5059
  except asyncio.CancelledError:
5011
5060
  pass
5012
5061
  finally:
@@ -1,3 +1,3 @@
1
1
  """Autobots TUI - A textual-based terminal user interface."""
2
2
 
3
- __version__ = "0.0.443"
3
+ __version__ = "0.0.445"