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.
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/PKG-INFO +1 -1
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/pyproject.toml +1 -1
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/__init__.py +1 -1
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/cli.py +14 -38
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/commands.py +1 -160
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_runtime/__init__.py +1 -1
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_runtime/main.py +52 -3
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/__init__.py +1 -1
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/main.py +174 -62
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/runtime_agent.py +10 -21
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/dashboard.py +54 -360
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/term_dashboard.py +231 -18
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/slash_commands.py +36 -6
- kiwi_code-0.0.445/src/kiwi_tui/ui_settings.py +40 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_term_dashboard_ui.py +162 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/uv.lock +1 -1
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.gitignore +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/.python-version +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/CLAUDE.md +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/Makefile +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/README.md +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/detach_files.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/term_app.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/test_hello.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/__init__.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/conftest.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_batch_fs_tool.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_copy_path_fs_tool.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_read_file_streaming.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_search_in_files_fs_tool.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tui_headless.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_unified_diff.py +0 -0
- {kiwi_code-0.0.443 → kiwi_code-0.0.445}/tests/test_worktrees.py +0 -0
|
@@ -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
|
-
|
|
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]
|
|
@@ -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=
|
|
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
|
-
|
|
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(
|
|
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:
|