kiwi-code 0.0.511__tar.gz → 0.0.513__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.511 → kiwi_code-0.0.513}/PKG-INFO +1 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/pyproject.toml +1 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/autocode.py +33 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/terminal_mode.py +3 -2
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/commands.py +95 -2
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/handlers.py +7 -2
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/link.py +15 -4
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/sessions.py +10 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/tui_bridge.py +22 -4
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_dashboard.py +12 -5
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_autocode_actions.py +49 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_connection.py +157 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_term_dashboard_ui.py +53 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/uv.lock +1 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/.gitignore +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/.python-version +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/Makefile +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/README.md +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/docs/runtime_audit.html +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/docs/social_login_e2e.md +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/scripts/set_edition.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/scripts/ws_auth_check.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/common/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/common/banner.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/_edition.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/edition.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/human_questions.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/io_threads.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/oauth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/run_activity.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_cli/subagents.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/auth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/checkpoints.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/cli.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/constants.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/batch.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/codelens.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/context.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/edits.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/handler.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/paths.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/read.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/search.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/text.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/upload.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/fs/write.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/restricted.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/state.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/ui.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_runtime/uploads.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/attention.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/autoupdate.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/latex_unicode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/role_mode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/detach_files.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_activity_view.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_dashboard_css.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_dashboard_helpers.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_dashboard_widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_question_widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_status_line.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/screens/term_subagent_widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/term_app.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/terminal_theme.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/ui_settings.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/test_hello.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/conftest.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_account_server.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_attention.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_autoupdate.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_batch_fs_tool.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_command_tips.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_copy_path_fs_tool.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_fs_edit_fidelity.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_human_questions.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_io_threads.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_login_social.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_math_and_tables.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_oauth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_read_file_streaming.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_reply_selection.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_role_mode_injection.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_run_activity.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_run_steering.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_cleanup_per_account.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_edit_diffs.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_fixes.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_imports_on_windows.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_runtime_social_login.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_search_in_files_fs_tool.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_stats_command.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_status_line.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_subagents.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_terminal_theme.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_toasts.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_unified_diff.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.513}/tests/test_worktrees.py +0 -0
|
@@ -8,6 +8,11 @@ contains "autocode" in any spelling: "Autocode", "AutoCodeClaude",
|
|
|
8
8
|
When two of them have the same name ignoring case, spaces and punctuation
|
|
9
9
|
("AutocodeClaude" and "AutoCodeClaude", "Autocode OpenAI Flash" and
|
|
10
10
|
"AutocodeOpenaiFlash"), only the most recently created one is kept.
|
|
11
|
+
|
|
12
|
+
Which one a new session starts with depends on the account (``default_id_for``):
|
|
13
|
+
Work (Enterprise, api.meetkiwi.ai) starts with "Autocode OpenAI Flash", Personal
|
|
14
|
+
(Subscription, api.runkai.ai) and every other server with "Autocode". The VS Code
|
|
15
|
+
extension makes the same choice (src/defaultAgent.ts).
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
18
|
import re
|
|
@@ -17,11 +22,17 @@ from datetime import datetime, timezone
|
|
|
17
22
|
from pykiwiai import AuthenticatedClient
|
|
18
23
|
from pykiwiai.api.actions import list_system_actions_v1_actions_system_actions_get
|
|
19
24
|
|
|
25
|
+
from kiwi_cli.server import SERVER_PRESETS
|
|
26
|
+
|
|
20
27
|
# Fetch every system action in one page: paging with offset is not reliable on
|
|
21
28
|
# the servers (offset > 0 returns an empty page).
|
|
22
29
|
_SYSTEM_ACTIONS_LIMIT = 500
|
|
23
30
|
|
|
24
31
|
|
|
32
|
+
#: The default on the Work (Enterprise) server, compared as _normalize compares names.
|
|
33
|
+
_ENTERPRISE_DEFAULT = "autocodeopenaiflash"
|
|
34
|
+
|
|
35
|
+
|
|
25
36
|
def _normalize(name: object) -> str:
|
|
26
37
|
return re.sub(r"[^a-z0-9]", "", str(name or "").lower())
|
|
27
38
|
|
|
@@ -64,6 +75,28 @@ class AutocodeActions:
|
|
|
64
75
|
return action.id
|
|
65
76
|
return self.actions[0].id if self.actions else None
|
|
66
77
|
|
|
78
|
+
def name_of(self, action_id: object) -> str | None:
|
|
79
|
+
"""The name of the AutoCode action ``action_id``, or None if it is not one."""
|
|
80
|
+
for action in self.actions:
|
|
81
|
+
if action.id == action_id:
|
|
82
|
+
return action.name
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def default_id_for(self, backend_url: object) -> str | None:
|
|
86
|
+
"""The action a new session starts with on ``backend_url``: "Autocode OpenAI
|
|
87
|
+
Flash" on the Work (Enterprise) server when it has one, else ``default_id``."""
|
|
88
|
+
if is_enterprise_backend(backend_url):
|
|
89
|
+
for action in self.actions:
|
|
90
|
+
if _normalize(action.name) == _ENTERPRISE_DEFAULT:
|
|
91
|
+
return action.id
|
|
92
|
+
return self.default_id
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_enterprise_backend(backend_url: object) -> bool:
|
|
96
|
+
"""True for the Work (Enterprise) server, api.meetkiwi.ai."""
|
|
97
|
+
url = str(backend_url or "").strip().rstrip("/").lower()
|
|
98
|
+
return url == SERVER_PRESETS["work"]["http"]
|
|
99
|
+
|
|
67
100
|
|
|
68
101
|
def _created(action: AutocodeAction) -> datetime:
|
|
69
102
|
"""When the action was created. Falls back to the timestamp in its ObjectId."""
|
|
@@ -227,10 +227,11 @@ def _default_action_id(session: _TerminalSession) -> str:
|
|
|
227
227
|
found = fetch_autocode_actions(session.client.client)
|
|
228
228
|
if found.status_code in (401, 403) and _refresh_tokens(session):
|
|
229
229
|
found = fetch_autocode_actions(session.client.client)
|
|
230
|
-
|
|
230
|
+
default_id = found.default_id_for(session.backend_url)
|
|
231
|
+
if default_id is None:
|
|
231
232
|
detail = "no AutoCode actions on this server" if found.ok else f"HTTP {found.status_code}"
|
|
232
233
|
raise TerminalModeError(f"Could not find the AutoCode action ({detail}). Pass --action-id to choose one.")
|
|
233
|
-
return
|
|
234
|
+
return default_id
|
|
234
235
|
|
|
235
236
|
|
|
236
237
|
def _build_authenticated_session(server: str | None) -> _TerminalSession:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""One-off and interactive commands."""
|
|
2
2
|
import asyncio
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
5
|
+
import re
|
|
4
6
|
import shlex
|
|
5
7
|
import signal
|
|
6
8
|
import subprocess
|
|
@@ -9,6 +11,7 @@ import time
|
|
|
9
11
|
import uuid
|
|
10
12
|
from collections import deque
|
|
11
13
|
from typing import Any
|
|
14
|
+
from urllib.parse import unquote, urlsplit
|
|
12
15
|
|
|
13
16
|
from kiwi_runtime.constants import (
|
|
14
17
|
IS_WINDOWS,
|
|
@@ -222,13 +225,94 @@ async def _kill_command(proc: asyncio.subprocess.Process) -> None:
|
|
|
222
225
|
pass
|
|
223
226
|
|
|
224
227
|
|
|
228
|
+
#: Values shorter than this are not masked: too likely to match ordinary output.
|
|
229
|
+
_MIN_MASKED_LEN = 6
|
|
230
|
+
#: What a forwarded variable's name may be; anything else is refused.
|
|
231
|
+
_ENV_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
232
|
+
#: At most this many forwarded variables, each at most this long.
|
|
233
|
+
_MAX_ENV_VARS = 64
|
|
234
|
+
_MAX_ENV_VALUE = 16_384
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def command_env(value: Any) -> dict[str, str] | None:
|
|
238
|
+
"""
|
|
239
|
+
The `env` a one-off or interactive command message carries (connection credentials
|
|
240
|
+
the backend resolved for it), checked: string names a shell accepts, string values, a
|
|
241
|
+
bounded number. Anything else is dropped rather than half-applied. None when there is none.
|
|
242
|
+
"""
|
|
243
|
+
if not isinstance(value, dict) or not value:
|
|
244
|
+
return None
|
|
245
|
+
env: dict[str, str] = {}
|
|
246
|
+
for key, val in list(value.items())[:_MAX_ENV_VARS]:
|
|
247
|
+
if isinstance(key, str) and _ENV_NAME.match(key) and isinstance(val, str) and len(val) <= _MAX_ENV_VALUE:
|
|
248
|
+
env[key] = val
|
|
249
|
+
return env or None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
#: Strings in a JSON credential (a service account's private key and the like) shorter
|
|
253
|
+
#: than this are not masked on their own: field names, types, project ids.
|
|
254
|
+
_MIN_JSON_SECRET_LEN = 16
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _secret_parts(value: str) -> list[str]:
|
|
258
|
+
"""
|
|
259
|
+
What of a forwarded value a command could print and must not: the value itself; for a
|
|
260
|
+
header like "Bearer <token>", the token; for a database connection string, its
|
|
261
|
+
password; for a JSON credential (BigQuery), its long string fields (a private key).
|
|
262
|
+
Kept in step with the backend's terminal_tool._secret_parts and the extension's
|
|
263
|
+
runtime/commandEnv.ts.
|
|
264
|
+
"""
|
|
265
|
+
parts = [value]
|
|
266
|
+
_, _, token = value.partition(" ")
|
|
267
|
+
parts.append(token)
|
|
268
|
+
if "://" in value:
|
|
269
|
+
try:
|
|
270
|
+
password = urlsplit(value).password
|
|
271
|
+
except ValueError:
|
|
272
|
+
password = None
|
|
273
|
+
if password:
|
|
274
|
+
parts += [password, unquote(password)]
|
|
275
|
+
if value.lstrip().startswith("{"):
|
|
276
|
+
try:
|
|
277
|
+
loaded = json.loads(value)
|
|
278
|
+
except ValueError:
|
|
279
|
+
loaded = None
|
|
280
|
+
if isinstance(loaded, dict):
|
|
281
|
+
for field in loaded.values():
|
|
282
|
+
if isinstance(field, str) and len(field) >= _MIN_JSON_SECRET_LEN:
|
|
283
|
+
parts += [field, field.replace("\n", "\\n")]
|
|
284
|
+
return parts
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def mask_secrets(text: str, env: dict[str, str] | None) -> str:
|
|
288
|
+
"""
|
|
289
|
+
Every forwarded value in `text` replaced by its variable's name, in case the command
|
|
290
|
+
printed one: each value whole, and the parts of it that are secrets on their own
|
|
291
|
+
(`_secret_parts`). (The backend masks again; this keeps it from leaving the machine
|
|
292
|
+
at all.)
|
|
293
|
+
"""
|
|
294
|
+
if not text or not env:
|
|
295
|
+
return text
|
|
296
|
+
pairs = [
|
|
297
|
+
(part, key)
|
|
298
|
+
for key, value in env.items()
|
|
299
|
+
for part in _secret_parts(value)
|
|
300
|
+
if len(part) >= _MIN_MASKED_LEN
|
|
301
|
+
]
|
|
302
|
+
for value, key in sorted(pairs, key=lambda p: len(p[0]), reverse=True):
|
|
303
|
+
text = text.replace(value, f"[{key}]")
|
|
304
|
+
return text
|
|
305
|
+
|
|
306
|
+
|
|
225
307
|
async def run_command(
|
|
226
308
|
command: str,
|
|
227
309
|
timeout: int = 30,
|
|
228
310
|
mode: str = "restricted",
|
|
229
311
|
allowed_dirs: list[str] | None = None,
|
|
312
|
+
env: dict[str, str] | None = None,
|
|
230
313
|
) -> dict:
|
|
231
|
-
"""Execute a shell command and return stdout, stderr, exit_code.
|
|
314
|
+
"""Execute a shell command and return stdout, stderr, exit_code. `env` is added to this
|
|
315
|
+
command's environment only, on top of the runtime's own; it is not logged."""
|
|
232
316
|
# Validate in restricted mode
|
|
233
317
|
if mode == "restricted" and allowed_dirs:
|
|
234
318
|
ok, reason = validate_command(command, allowed_dirs)
|
|
@@ -248,6 +332,8 @@ async def run_command(
|
|
|
248
332
|
"stdin": asyncio.subprocess.DEVNULL,
|
|
249
333
|
"cwd": cwd,
|
|
250
334
|
}
|
|
335
|
+
if env:
|
|
336
|
+
popen_kwargs["env"] = {**os.environ, **env}
|
|
251
337
|
if not IS_WINDOWS:
|
|
252
338
|
popen_kwargs["start_new_session"] = True
|
|
253
339
|
|
|
@@ -294,8 +380,11 @@ async def run_interactive_command(
|
|
|
294
380
|
mode: str = "restricted",
|
|
295
381
|
allowed_dirs: list[str] | None = None,
|
|
296
382
|
request_id: str | None = None,
|
|
383
|
+
env: dict[str, str] | None = None,
|
|
297
384
|
) -> dict:
|
|
298
|
-
"""Execute a command with the user's terminal attached for interactive I/O.
|
|
385
|
+
"""Execute a command with the user's terminal attached for interactive I/O. `env` is
|
|
386
|
+
added to this command's environment only, as for run_command. What the command prints
|
|
387
|
+
goes to the user's terminal, never to the server: the server gets only the exit code."""
|
|
299
388
|
if mode == "restricted" and allowed_dirs:
|
|
300
389
|
ok, reason = validate_command(command, allowed_dirs)
|
|
301
390
|
if not ok:
|
|
@@ -317,6 +406,8 @@ async def run_interactive_command(
|
|
|
317
406
|
mode=mode,
|
|
318
407
|
allowed_dirs=allowed_dirs,
|
|
319
408
|
timeout_sec=timeout_eff,
|
|
409
|
+
env=env,
|
|
410
|
+
mask=(lambda text: mask_secrets(text, env)) if env else None,
|
|
320
411
|
)
|
|
321
412
|
|
|
322
413
|
try:
|
|
@@ -346,6 +437,8 @@ async def run_interactive_command(
|
|
|
346
437
|
old_term = None
|
|
347
438
|
|
|
348
439
|
popen_kwargs: dict[str, Any] = {"shell": True, "cwd": cwd}
|
|
440
|
+
if env:
|
|
441
|
+
popen_kwargs["env"] = {**os.environ, **env}
|
|
349
442
|
if not IS_WINDOWS:
|
|
350
443
|
popen_kwargs["start_new_session"] = True
|
|
351
444
|
# The command writes to the terminal directly; what was printed goes first.
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from typing import Any, Awaitable, Callable
|
|
6
6
|
|
|
7
7
|
from kiwi_runtime.checkpoints import _set_current_run_id
|
|
8
|
-
from kiwi_runtime.commands import _looks_interactive_oneoff, run_interactive_command
|
|
8
|
+
from kiwi_runtime.commands import _looks_interactive_oneoff, command_env, run_interactive_command
|
|
9
9
|
from kiwi_runtime.constants import IS_WINDOWS
|
|
10
10
|
from kiwi_runtime.fs.handler import FS_HANDLERS, handle_fs_request
|
|
11
11
|
from kiwi_runtime.restricted import validate_command
|
|
@@ -79,8 +79,10 @@ async def _command(state: _RuntimeState, msg: dict[str, Any]) -> None:
|
|
|
79
79
|
timeout_sec = int(msg.get("timeout_sec", 30) or 30)
|
|
80
80
|
except Exception:
|
|
81
81
|
timeout_sec = 30
|
|
82
|
+
# App connection credentials for this command only (the "command_env" capability).
|
|
83
|
+
env = command_env(msg.get("env"))
|
|
82
84
|
try:
|
|
83
|
-
state.command_queue.put_nowait((str(request_id), str(command), timeout_sec))
|
|
85
|
+
state.command_queue.put_nowait((str(request_id), str(command), timeout_sec, env))
|
|
84
86
|
except asyncio.QueueFull:
|
|
85
87
|
print_cmd_log(request_id, f"{RED}FAILED{RESET}", False)
|
|
86
88
|
await state.reply(_command_result(request_id, "Runtime command queue full; please retry."))
|
|
@@ -90,6 +92,8 @@ async def _interactive(state: _RuntimeState, msg: dict[str, Any]) -> None:
|
|
|
90
92
|
request_id = msg.get("request_id", "")
|
|
91
93
|
command = msg.get("command", "")
|
|
92
94
|
print_cmd_log(request_id, f"$ {BOLD}{command}{RESET} {GREY}(interactive){RESET}")
|
|
95
|
+
# Connection credentials for this command only; never logged.
|
|
96
|
+
env = command_env(msg.get("env"))
|
|
93
97
|
|
|
94
98
|
try:
|
|
95
99
|
timeout_sec = int(msg.get("timeout_sec", 120) or 120)
|
|
@@ -103,6 +107,7 @@ async def _interactive(state: _RuntimeState, msg: dict[str, Any]) -> None:
|
|
|
103
107
|
mode=state.mode,
|
|
104
108
|
allowed_dirs=state.allowed_dirs,
|
|
105
109
|
request_id=request_id,
|
|
110
|
+
env=env,
|
|
106
111
|
)
|
|
107
112
|
exit_code = result["exit_code"]
|
|
108
113
|
success = exit_code == 0
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
import websockets
|
|
12
12
|
|
|
13
13
|
from kiwi_runtime.auth import _get_valid_access_token, login
|
|
14
|
-
from kiwi_runtime.commands import run_command
|
|
14
|
+
from kiwi_runtime.commands import mask_secrets, run_command
|
|
15
15
|
from kiwi_runtime.constants import MAX_INBOUND_FRAME_BYTES
|
|
16
16
|
from kiwi_runtime.fs.handler import FS_MAX_CONCURRENCY
|
|
17
17
|
from kiwi_runtime.handlers import handle_message
|
|
@@ -32,7 +32,10 @@ from kiwi_runtime.ui import (
|
|
|
32
32
|
# goodbye sends {"type": "goodbye"} before an intentional exit
|
|
33
33
|
# outbox keeps what it could not send and delivers it after reconnecting
|
|
34
34
|
# pty_survive keeps its PTY sessions across a reconnect and lists them at auth
|
|
35
|
-
|
|
35
|
+
# command_env takes an `env` mapping on a one-off or interactive command (connection
|
|
36
|
+
# credentials), adds it to that command's environment only, and masks it in
|
|
37
|
+
# the output
|
|
38
|
+
RUNTIME_CAPABILITIES = ["goodbye", "outbox", "pty_survive", "command_env"]
|
|
36
39
|
|
|
37
40
|
RECONNECT_BASE_DELAY_SEC = 1.5
|
|
38
41
|
RECONNECT_MAX_DELAY_SEC = 30.0
|
|
@@ -89,11 +92,11 @@ async def _command_worker(state: _RuntimeState, mode: str, allowed_dirs: list[st
|
|
|
89
92
|
"""Runs one-off commands from the queue; a result outlives the connection it was asked
|
|
90
93
|
on. Each command is its own shell, so several workers run them side by side."""
|
|
91
94
|
while True:
|
|
92
|
-
request_id, command, timeout_sec = await state.command_queue.get()
|
|
95
|
+
request_id, command, timeout_sec, env = await state.command_queue.get()
|
|
93
96
|
state.running_commands.append(command)
|
|
94
97
|
try:
|
|
95
98
|
result = await run_command(
|
|
96
|
-
command, timeout=timeout_sec or 30, mode=mode, allowed_dirs=allowed_dirs
|
|
99
|
+
command, timeout=timeout_sec or 30, mode=mode, allowed_dirs=allowed_dirs, env=env
|
|
97
100
|
)
|
|
98
101
|
try:
|
|
99
102
|
exit_code = int(result.get("exit_code", 0))
|
|
@@ -111,6 +114,14 @@ async def _command_worker(state: _RuntimeState, mode: str, allowed_dirs: list[st
|
|
|
111
114
|
finally:
|
|
112
115
|
state.running_commands.remove(command)
|
|
113
116
|
state.command_queue.task_done()
|
|
117
|
+
if env:
|
|
118
|
+
# A credential the command printed must not reach the server, the model or the
|
|
119
|
+
# chat: each value is replaced by its variable's name.
|
|
120
|
+
result = {
|
|
121
|
+
**result,
|
|
122
|
+
"stdout": mask_secrets(result.get("stdout", ""), env),
|
|
123
|
+
"stderr": mask_secrets(result.get("stderr", ""), env),
|
|
124
|
+
}
|
|
114
125
|
await state.link.send(json.dumps({"type": "result", "request_id": request_id, **result}))
|
|
115
126
|
|
|
116
127
|
|
|
@@ -149,9 +149,12 @@ if not IS_WINDOWS:
|
|
|
149
149
|
cols: int = 120,
|
|
150
150
|
rows: int = 40,
|
|
151
151
|
on_exit: Callable[[], None] | None = None,
|
|
152
|
+
env: dict[str, str] | None = None,
|
|
152
153
|
):
|
|
153
154
|
self.session_id = session_id
|
|
154
155
|
self.command = command
|
|
156
|
+
# Added to this process's environment only (connection credentials); not logged.
|
|
157
|
+
self.env = env
|
|
155
158
|
self.cols = cols
|
|
156
159
|
self.rows = rows
|
|
157
160
|
self.master_fd = None
|
|
@@ -190,6 +193,7 @@ if not IS_WINDOWS:
|
|
|
190
193
|
stderr=self.slave_fd,
|
|
191
194
|
preexec_fn=_own_terminal,
|
|
192
195
|
cwd=cwd,
|
|
196
|
+
env={**os.environ, **self.env} if self.env else None,
|
|
193
197
|
)
|
|
194
198
|
|
|
195
199
|
os.close(self.slave_fd)
|
|
@@ -422,9 +426,12 @@ class PipeProcess:
|
|
|
422
426
|
rows: int = 40,
|
|
423
427
|
mode: str = "restricted",
|
|
424
428
|
allowed_dirs: list[str] | None = None,
|
|
429
|
+
env: dict[str, str] | None = None,
|
|
425
430
|
):
|
|
426
431
|
self.session_id = session_id
|
|
427
432
|
self.command = command
|
|
433
|
+
# Added to this process's environment only (connection credentials); not logged.
|
|
434
|
+
self.env = env
|
|
428
435
|
self.cols = cols
|
|
429
436
|
self.rows = rows
|
|
430
437
|
self.proc = None
|
|
@@ -444,6 +451,7 @@ class PipeProcess:
|
|
|
444
451
|
self.cwd = cwd
|
|
445
452
|
self.ws = ws
|
|
446
453
|
self._sentinel_buffer = ""
|
|
454
|
+
env = {**os.environ, **self.env} if self.env else None
|
|
447
455
|
|
|
448
456
|
if IS_WINDOWS:
|
|
449
457
|
shell = os.environ.get("COMSPEC", "cmd.exe")
|
|
@@ -457,6 +465,7 @@ class PipeProcess:
|
|
|
457
465
|
stdout=asyncio.subprocess.PIPE,
|
|
458
466
|
stderr=asyncio.subprocess.STDOUT,
|
|
459
467
|
cwd=cwd,
|
|
468
|
+
env=env,
|
|
460
469
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
461
470
|
)
|
|
462
471
|
else:
|
|
@@ -467,6 +476,7 @@ class PipeProcess:
|
|
|
467
476
|
stdout=asyncio.subprocess.PIPE,
|
|
468
477
|
stderr=asyncio.subprocess.STDOUT,
|
|
469
478
|
cwd=cwd,
|
|
479
|
+
env=env,
|
|
470
480
|
)
|
|
471
481
|
|
|
472
482
|
self._read_task = asyncio.create_task(self._read_loop(ws))
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import uuid
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Callable
|
|
9
9
|
|
|
10
10
|
from kiwi_runtime.constants import (
|
|
11
11
|
IS_WINDOWS,
|
|
@@ -30,8 +30,17 @@ def _tui_managed_control_dir() -> Path | None:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class _LocalInteractivePTYBridge:
|
|
33
|
-
def __init__(
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
control_dir: Path,
|
|
37
|
+
request_id: str,
|
|
38
|
+
command: str,
|
|
39
|
+
mask: Callable[[str], str] | None = None,
|
|
40
|
+
) -> None:
|
|
34
41
|
self.control_dir = control_dir
|
|
42
|
+
# Hides forwarded credentials in what is written to output.log for the TUI to show.
|
|
43
|
+
self._mask = mask
|
|
35
44
|
self.request_id = request_id
|
|
36
45
|
self.command = command
|
|
37
46
|
self.session_dir = control_dir / "interactive"
|
|
@@ -75,6 +84,10 @@ class _LocalInteractivePTYBridge:
|
|
|
75
84
|
def append_output(self, data: str) -> None:
|
|
76
85
|
if not data:
|
|
77
86
|
return
|
|
87
|
+
if self._mask is not None:
|
|
88
|
+
# Chunk by chunk: a value split across two reads is not caught. The file stays on
|
|
89
|
+
# this machine and nothing of it is sent to the server.
|
|
90
|
+
data = self._mask(data)
|
|
78
91
|
with open(self.output_path, "a", encoding="utf-8", errors="replace") as fp:
|
|
79
92
|
fp.write(data)
|
|
80
93
|
fp.flush()
|
|
@@ -179,6 +192,8 @@ async def run_tui_managed_interactive_command(
|
|
|
179
192
|
mode: str = "restricted",
|
|
180
193
|
allowed_dirs: list[str] | None = None,
|
|
181
194
|
timeout_sec: int | None = None,
|
|
195
|
+
env: dict[str, str] | None = None,
|
|
196
|
+
mask: Callable[[str], str] | None = None,
|
|
182
197
|
) -> dict:
|
|
183
198
|
if mode == "restricted" and allowed_dirs:
|
|
184
199
|
ok, reason = validate_command(command, allowed_dirs)
|
|
@@ -186,7 +201,9 @@ async def run_tui_managed_interactive_command(
|
|
|
186
201
|
return {"stdout": "", "stderr": reason, "exit_code": 1}
|
|
187
202
|
|
|
188
203
|
cwd = allowed_dirs[0] if allowed_dirs else None
|
|
189
|
-
bridge = _LocalInteractivePTYBridge(
|
|
204
|
+
bridge = _LocalInteractivePTYBridge(
|
|
205
|
+
control_dir=control_dir, request_id=request_id, command=command, mask=mask
|
|
206
|
+
)
|
|
190
207
|
await asyncio.to_thread(bridge.activate)
|
|
191
208
|
await asyncio.to_thread(bridge.append_output, f"$ {command}\r\n")
|
|
192
209
|
|
|
@@ -202,9 +219,10 @@ async def run_tui_managed_interactive_command(
|
|
|
202
219
|
40,
|
|
203
220
|
mode=mode,
|
|
204
221
|
allowed_dirs=allowed_dirs,
|
|
222
|
+
env=env,
|
|
205
223
|
)
|
|
206
224
|
else:
|
|
207
|
-
pty_proc = PTYProcess(session_id, command, 120, 40)
|
|
225
|
+
pty_proc = PTYProcess(session_id, command, 120, 40, env=env)
|
|
208
226
|
await pty_proc.start(bridge, cwd=cwd)
|
|
209
227
|
input_task = asyncio.create_task(bridge.pump_inputs(pty_proc))
|
|
210
228
|
|
|
@@ -1503,7 +1503,8 @@ class TermDashboardScreen(Screen):
|
|
|
1503
1503
|
cwd = str(Path.cwd())
|
|
1504
1504
|
cwd_short = cwd if len(cwd) <= 44 else f"{cwd[:20]}…{cwd[-20:]}"
|
|
1505
1505
|
|
|
1506
|
-
|
|
1506
|
+
# While its name loads, an action shows as "…", not as its id.
|
|
1507
|
+
action = self.current_action_name or ("…" if self.current_action_id else "")
|
|
1507
1508
|
action = str(action)
|
|
1508
1509
|
action_short = action if len(action) <= 16 else (action[:12] + "…")
|
|
1509
1510
|
|
|
@@ -1643,7 +1644,7 @@ class TermDashboardScreen(Screen):
|
|
|
1643
1644
|
def _refresh_welcome_meta(self) -> None:
|
|
1644
1645
|
"""Update action name + run id displayed in the welcome header."""
|
|
1645
1646
|
try:
|
|
1646
|
-
action = self.current_action_name or self.current_action_id
|
|
1647
|
+
action = self.current_action_name or ("…" if self.current_action_id else "—")
|
|
1647
1648
|
run = self.current_run_id or "—"
|
|
1648
1649
|
self.query_one("#term-welcome-account", Static).update(self._account_text())
|
|
1649
1650
|
self.query_one("#term-welcome-action", Static).update(f"Action: {action}")
|
|
@@ -1764,9 +1765,14 @@ class TermDashboardScreen(Screen):
|
|
|
1764
1765
|
except Exception as e:
|
|
1765
1766
|
logger.debug(f"Loading AutoCode actions failed: {e}")
|
|
1766
1767
|
if not self.current_action_id and self._autocode is not None:
|
|
1767
|
-
self.current_action_id = self._autocode.
|
|
1768
|
+
self.current_action_id = self._autocode.default_id_for(self._autocode_backend) or ""
|
|
1768
1769
|
requested_id = self.current_action_id
|
|
1769
|
-
|
|
1770
|
+
# An AutoCode action's name is already in the list just fetched: show it with the
|
|
1771
|
+
# id, rather than the id first and the name a request later.
|
|
1772
|
+
known = self._autocode.name_of(requested_id) if self._autocode is not None and requested_id else None
|
|
1773
|
+
if known and not self.current_action_name:
|
|
1774
|
+
self.current_action_name = known
|
|
1775
|
+
if not requested_id or known:
|
|
1770
1776
|
self._refresh_top_bar()
|
|
1771
1777
|
self._refresh_hint_model()
|
|
1772
1778
|
self._refresh_input_label()
|
|
@@ -1779,7 +1785,8 @@ class TermDashboardScreen(Screen):
|
|
|
1779
1785
|
if resp.status_code == 200 and resp.parsed:
|
|
1780
1786
|
self.current_action_name = getattr(resp.parsed, "name", None) or requested_id
|
|
1781
1787
|
else:
|
|
1782
|
-
|
|
1788
|
+
# The name could not be found: the id is all there is to show.
|
|
1789
|
+
self.current_action_name = requested_id
|
|
1783
1790
|
self._refresh_top_bar()
|
|
1784
1791
|
self._refresh_hint_model()
|
|
1785
1792
|
self._refresh_input_label()
|
|
@@ -154,3 +154,52 @@ def test_the_default_autocode_is_also_the_latest_one(monkeypatch):
|
|
|
154
154
|
)
|
|
155
155
|
found = fetch_autocode_actions(client=None)
|
|
156
156
|
assert [a.id for a in found.actions] == ["new"] and found.default_id == "new"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# The default by account: Work (Enterprise, api.meetkiwi.ai) starts with Autocode OpenAI
|
|
160
|
+
# Flash, Personal (Subscription, api.runkai.ai) and every other server with Autocode. The
|
|
161
|
+
# VS Code extension makes the same choice (src/defaultAgent.ts).
|
|
162
|
+
|
|
163
|
+
WORK = "https://api.meetkiwi.ai"
|
|
164
|
+
PERSONAL = "https://api.runkai.ai"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _flash_and_autocode(monkeypatch):
|
|
168
|
+
_respond(
|
|
169
|
+
monkeypatch,
|
|
170
|
+
200,
|
|
171
|
+
[
|
|
172
|
+
_doc("a", "Autocode"),
|
|
173
|
+
_doc("f1", "Autocode OpenAI Flash", created_at=datetime(2026, 5, 1)),
|
|
174
|
+
_doc("f2", "AutocodeOpenaiFlash", created_at=datetime(2026, 10, 24)),
|
|
175
|
+
_doc("c", "AutoCodeClaude", "claude"),
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
return fetch_autocode_actions(client=None)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.parametrize("url", [WORK, WORK + "/", "https://API.meetkiwi.ai"])
|
|
182
|
+
def test_work_starts_with_the_latest_flash(monkeypatch, url):
|
|
183
|
+
assert _flash_and_autocode(monkeypatch).default_id_for(url) == "f2"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.parametrize("url", [PERSONAL, "https://dev.api.myautobots.com", "http://localhost:8000", "", None])
|
|
187
|
+
def test_personal_and_other_servers_start_with_autocode(monkeypatch, url):
|
|
188
|
+
assert _flash_and_autocode(monkeypatch).default_id_for(url) == "a"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_work_without_flash_starts_with_autocode(monkeypatch):
|
|
192
|
+
_respond(monkeypatch, 200, [_doc("a", "Autocode"), _doc("c", "AutoCodeClaude", "claude")])
|
|
193
|
+
assert fetch_autocode_actions(client=None).default_id_for(WORK) == "a"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_no_actions_means_no_default_on_work_either():
|
|
197
|
+
assert AutocodeActions(status_code=200).default_id_for(WORK) is None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_name_of_gives_an_autocode_actions_name(monkeypatch):
|
|
201
|
+
found = _flash_and_autocode(monkeypatch)
|
|
202
|
+
assert found.name_of("f2") == "AutocodeOpenaiFlash"
|
|
203
|
+
assert found.name_of("a") == "Autocode"
|
|
204
|
+
assert found.name_of("unknown") is None
|
|
205
|
+
assert found.name_of(None) is None
|