tunacode-cli 0.0.40__py3-none-any.whl → 0.0.42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands/__init__.py +2 -0
- tunacode/cli/commands/implementations/__init__.py +3 -0
- tunacode/cli/commands/implementations/debug.py +1 -1
- tunacode/cli/commands/implementations/todo.py +217 -0
- tunacode/cli/commands/registry.py +2 -0
- tunacode/cli/main.py +12 -5
- tunacode/cli/repl.py +205 -136
- tunacode/configuration/defaults.py +2 -0
- tunacode/configuration/models.py +6 -0
- tunacode/constants.py +27 -3
- tunacode/context.py +7 -3
- tunacode/core/agents/dspy_integration.py +223 -0
- tunacode/core/agents/dspy_tunacode.py +458 -0
- tunacode/core/agents/main.py +182 -12
- tunacode/core/agents/utils.py +54 -6
- tunacode/core/recursive/__init__.py +18 -0
- tunacode/core/recursive/aggregator.py +467 -0
- tunacode/core/recursive/budget.py +414 -0
- tunacode/core/recursive/decomposer.py +398 -0
- tunacode/core/recursive/executor.py +467 -0
- tunacode/core/recursive/hierarchy.py +487 -0
- tunacode/core/setup/config_setup.py +5 -0
- tunacode/core/state.py +91 -1
- tunacode/core/token_usage/api_response_parser.py +44 -0
- tunacode/core/token_usage/cost_calculator.py +58 -0
- tunacode/core/token_usage/usage_tracker.py +98 -0
- tunacode/exceptions.py +23 -0
- tunacode/prompts/dspy_task_planning.md +45 -0
- tunacode/prompts/dspy_tool_selection.md +58 -0
- tunacode/prompts/system.md +69 -5
- tunacode/tools/todo.py +343 -0
- tunacode/types.py +20 -1
- tunacode/ui/console.py +1 -1
- tunacode/ui/input.py +1 -1
- tunacode/ui/output.py +38 -1
- tunacode/ui/panels.py +4 -1
- tunacode/ui/recursive_progress.py +380 -0
- tunacode/ui/tool_ui.py +24 -6
- tunacode/ui/utils.py +1 -1
- tunacode/utils/message_utils.py +17 -0
- tunacode/utils/retry.py +163 -0
- tunacode/utils/token_counter.py +78 -8
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/METADATA +4 -1
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/RECORD +48 -32
- tunacode/cli/textual_app.py +0 -420
- tunacode/cli/textual_bridge.py +0 -161
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/top_level.txt +0 -0
tunacode/utils/token_counter.py
CHANGED
|
@@ -1,23 +1,93 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Token counting utility using tiktoken for accurate, offline token estimation."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Optional
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
# Configure logging
|
|
8
|
+
logging.basicConfig(level=logging.INFO)
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Cache for tokenizer encodings
|
|
12
|
+
_encoding_cache = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@lru_cache(maxsize=8)
|
|
16
|
+
def get_encoding(model_name: str):
|
|
17
|
+
"""Get the appropriate tiktoken encoding for a model.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
model_name: The model name in format "provider:model"
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A tiktoken encoding instance
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
import tiktoken
|
|
27
|
+
except ImportError:
|
|
28
|
+
logger.warning("tiktoken not available, falling back to character estimation")
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
# Extract the model part from "provider:model" format
|
|
32
|
+
if ":" in model_name:
|
|
33
|
+
provider, model = model_name.split(":", 1)
|
|
34
|
+
else:
|
|
35
|
+
provider, model = "unknown", model_name
|
|
36
|
+
|
|
37
|
+
# Map common models to their tiktoken encodings
|
|
38
|
+
if provider == "openai":
|
|
39
|
+
if "gpt-4" in model:
|
|
40
|
+
encoding_name = "cl100k_base" # GPT-4 encoding
|
|
41
|
+
elif "gpt-3.5" in model:
|
|
42
|
+
encoding_name = "cl100k_base" # GPT-3.5-turbo encoding
|
|
43
|
+
else:
|
|
44
|
+
encoding_name = "cl100k_base" # Default for newer models
|
|
45
|
+
elif provider == "anthropic":
|
|
46
|
+
# Claude models use similar tokenization to GPT-4
|
|
47
|
+
encoding_name = "cl100k_base"
|
|
48
|
+
else:
|
|
49
|
+
# Default encoding for unknown models
|
|
50
|
+
encoding_name = "cl100k_base"
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return tiktoken.get_encoding(encoding_name)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Error loading tiktoken encoding '{encoding_name}': {e}")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def estimate_tokens(text: str, model_name: Optional[str] = None) -> int:
|
|
5
60
|
"""
|
|
6
|
-
Estimate token count using
|
|
61
|
+
Estimate token count using tiktoken for accurate results.
|
|
7
62
|
|
|
8
|
-
|
|
9
|
-
|
|
63
|
+
Args:
|
|
64
|
+
text: The text to count tokens for.
|
|
65
|
+
model_name: Optional model name for model-specific tokenization.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The estimated number of tokens.
|
|
10
69
|
"""
|
|
11
70
|
if not text:
|
|
12
71
|
return 0
|
|
13
72
|
|
|
14
|
-
#
|
|
73
|
+
# Try tiktoken first if model is specified
|
|
74
|
+
if model_name:
|
|
75
|
+
encoding = get_encoding(model_name)
|
|
76
|
+
if encoding:
|
|
77
|
+
try:
|
|
78
|
+
return len(encoding.encode(text))
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Error counting tokens with tiktoken: {e}")
|
|
81
|
+
|
|
82
|
+
# Fallback to character-based estimation
|
|
15
83
|
# This is roughly accurate for English text
|
|
16
84
|
return len(text) // 4
|
|
17
85
|
|
|
18
86
|
|
|
19
87
|
def format_token_count(count: int) -> str:
|
|
20
|
-
"""Format token count for display."""
|
|
21
|
-
if count >=
|
|
88
|
+
"""Format token count for display with full precision."""
|
|
89
|
+
if count >= 1_000_000:
|
|
90
|
+
return f"{count:,}"
|
|
91
|
+
elif count >= 1000:
|
|
22
92
|
return f"{count:,}"
|
|
23
93
|
return str(count)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.42
|
|
4
4
|
Summary: Your agentic CLI developer.
|
|
5
5
|
Author-email: larock22 <noreply@github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,6 +24,9 @@ Requires-Dist: prompt_toolkit==3.0.51
|
|
|
24
24
|
Requires-Dist: pydantic-ai[logfire]==0.2.6
|
|
25
25
|
Requires-Dist: pygments==2.19.1
|
|
26
26
|
Requires-Dist: rich==14.0.0
|
|
27
|
+
Requires-Dist: tiktoken>=0.5.2
|
|
28
|
+
Requires-Dist: dspy-ai>=0.1.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
27
30
|
Provides-Extra: dev
|
|
28
31
|
Requires-Dist: build; extra == "dev"
|
|
29
32
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
tunacode/constants.py,sha256=
|
|
3
|
-
tunacode/context.py,sha256=
|
|
4
|
-
tunacode/exceptions.py,sha256=
|
|
2
|
+
tunacode/constants.py,sha256=Tteo8BguW-T3cNiGcNFsF49uNa1ddmWsvOvlROt7zBo,5168
|
|
3
|
+
tunacode/context.py,sha256=_gXVCyjU052jlyRAl9tklZSwl5U_zI_EIX8XN87VVWE,2786
|
|
4
|
+
tunacode/exceptions.py,sha256=oDO1SVKOgjcKIylwqdbqh_g5my4roU5mB9Nv4n_Vb0s,3877
|
|
5
5
|
tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
tunacode/setup.py,sha256=XPt4eAK-qcIZQv64jGZ_ryxcImDwps9OmXjJfIS1xcs,1899
|
|
7
|
-
tunacode/types.py,sha256=
|
|
7
|
+
tunacode/types.py,sha256=Czq7jYXHq7fZQtyqkCN5_7eEu1wyYUcB50C6v3sTWDw,8188
|
|
8
8
|
tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
|
|
9
|
-
tunacode/cli/main.py,sha256=
|
|
10
|
-
tunacode/cli/repl.py,sha256=
|
|
11
|
-
tunacode/cli/
|
|
12
|
-
tunacode/cli/textual_bridge.py,sha256=LvqiTtF0hu3gNujzpKaW9h-m6xzEP3OH2M8KL2pCwRc,6333
|
|
13
|
-
tunacode/cli/commands/__init__.py,sha256=YMrLz7szrmseJCRZGGX6_TyO3dJU8_QDCOFEhRAztzo,1634
|
|
9
|
+
tunacode/cli/main.py,sha256=ypefhvSt9hXzNOv0WpR-PlkUOSGadvcFbUIRT13n9oo,2619
|
|
10
|
+
tunacode/cli/repl.py,sha256=CMvl5wlxj7hr45mxil26fwbmWpDYxRsRVS9aIGJvKK0,18525
|
|
11
|
+
tunacode/cli/commands/__init__.py,sha256=zmE9JcJ9Qd2xJhgdS4jMDJOoZsrAZmL5MAFxbKkk7F8,1670
|
|
14
12
|
tunacode/cli/commands/base.py,sha256=GxUuDsDSpz0iXryy8MrEw88UM3C3yxL__kDK1QhshoA,2517
|
|
15
|
-
tunacode/cli/commands/registry.py,sha256=
|
|
16
|
-
tunacode/cli/commands/implementations/__init__.py,sha256=
|
|
13
|
+
tunacode/cli/commands/registry.py,sha256=XVuLpp5S4Fw7GfIZfLrVZFo4jMLMNmYNpYN7xWgXyOk,8223
|
|
14
|
+
tunacode/cli/commands/implementations/__init__.py,sha256=lMgLZRX9hnw-ftZu4ykqoJoHqkZ5Yu0lBvYuzHylm7Q,986
|
|
17
15
|
tunacode/cli/commands/implementations/conversation.py,sha256=EsnsZB6yyVI_sbNNMvk37tCz3iAj4E85R9ev696qeqg,4683
|
|
18
|
-
tunacode/cli/commands/implementations/debug.py,sha256=
|
|
16
|
+
tunacode/cli/commands/implementations/debug.py,sha256=hWr9DOIS-kn8z89IJZ6HuRkyN1tOsnFZg5qlB8YPbG8,6763
|
|
19
17
|
tunacode/cli/commands/implementations/development.py,sha256=kZRdVgReVmGU0uijFxtPio2RYkTrYMufOwgI1Aj1_NU,2729
|
|
20
18
|
tunacode/cli/commands/implementations/model.py,sha256=uthx6IX9KwgwywNTDklkJpqCbaTX9h1_p-eVmqL73WQ,2245
|
|
21
19
|
tunacode/cli/commands/implementations/system.py,sha256=2cGw5iCJO3aNhXTFF28CgAIyLgslvHmpfyL2ZHVB6oQ,7903
|
|
20
|
+
tunacode/cli/commands/implementations/todo.py,sha256=Dtz5bgcuK2VXGPWEBBZQgnWUMYkRXNzTGf_qkVPLF2U,8125
|
|
22
21
|
tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
|
|
23
|
-
tunacode/configuration/defaults.py,sha256=
|
|
24
|
-
tunacode/configuration/models.py,sha256=
|
|
22
|
+
tunacode/configuration/defaults.py,sha256=Qny32BhbSeRBrE9baWZcAit3XFXgWuXFMDJUZudj55M,922
|
|
23
|
+
tunacode/configuration/models.py,sha256=buH8ZquvcYI3OQBDIZeJ08cu00rSCeNABtUwl3VQS0E,4103
|
|
25
24
|
tunacode/configuration/settings.py,sha256=KoN0u6GG3Hh_TWt02D_wpRfbACYri3gCDTXHtJfHl2w,994
|
|
26
25
|
tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
26
|
tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,15661
|
|
28
|
-
tunacode/core/state.py,sha256=
|
|
27
|
+
tunacode/core/state.py,sha256=2lsDgq0uIIc_MnXPE9SG_1fYFBDWWlgqgqm2Ik1iFBs,5599
|
|
29
28
|
tunacode/core/tool_handler.py,sha256=BPjR013OOO0cLXPdLeL2FDK0ixUwOYu59FfHdcdFhp4,2277
|
|
30
29
|
tunacode/core/agents/__init__.py,sha256=UUJiPYb91arwziSpjd7vIk7XNGA_4HQbsOIbskSqevA,149
|
|
31
|
-
tunacode/core/agents/
|
|
32
|
-
tunacode/core/agents/
|
|
30
|
+
tunacode/core/agents/dspy_integration.py,sha256=h3gJ-qr0fXpB2CRaU-MVv_8xG--ah-8nra7WO960Gbo,9152
|
|
31
|
+
tunacode/core/agents/dspy_tunacode.py,sha256=tIluqDsHAv3nbNYtqFh4oyZA9yqMrnyY2G-QOpkTLCs,17398
|
|
32
|
+
tunacode/core/agents/main.py,sha256=Ul-wGjt4kopyXals7WNB-GZpDtOI_VMIdVdn0SVhQeQ,48682
|
|
33
|
+
tunacode/core/agents/utils.py,sha256=7kJAiUlkyWO3-b4T07XsGgycVrcNhv3NEPLdaktBnP4,12847
|
|
33
34
|
tunacode/core/background/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
35
|
tunacode/core/background/manager.py,sha256=rJdl3eDLTQwjbT7VhxXcJbZopCNR3M8ZGMbmeVnwwMc,1126
|
|
35
36
|
tunacode/core/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
tunacode/core/recursive/__init__.py,sha256=S9_dN0faJqam3Pnaum9PRC8Hd90bpES8syFgAD8-QbI,446
|
|
38
|
+
tunacode/core/recursive/aggregator.py,sha256=iKKKd_tZT1sXaPjpjRVVoCG8zFfuxilktSGUdalzTUM,15719
|
|
39
|
+
tunacode/core/recursive/budget.py,sha256=0wY6xSrZKmudUwthwX1mlcF5yat2_y3fNu5shX_IzvA,14131
|
|
40
|
+
tunacode/core/recursive/decomposer.py,sha256=7JbRPPXK083jipaaMSpnWIH4wtJC_yjhxBY-ZSkKqr0,14552
|
|
41
|
+
tunacode/core/recursive/executor.py,sha256=5UcFPK57KP2WhtUeHrBPE3BxkKsZ7QxoMaSUHTYrsoE,16436
|
|
42
|
+
tunacode/core/recursive/hierarchy.py,sha256=67tdDCKB5mTSn6VIwm-knEHB_VKUkeljFdwH62U5chE,15648
|
|
36
43
|
tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
|
|
37
44
|
tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
|
|
38
45
|
tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
|
|
39
|
-
tunacode/core/setup/config_setup.py,sha256=
|
|
46
|
+
tunacode/core/setup/config_setup.py,sha256=ctf9GuN7niqCsRK5rbEzCbzsN0MrAOeWzKtwOnyJJmY,14654
|
|
40
47
|
tunacode/core/setup/coordinator.py,sha256=oVTN2xIeJERXitVJpkIk9tDGLs1D1bxIRmaogJwZJFI,2049
|
|
41
48
|
tunacode/core/setup/environment_setup.py,sha256=n3IrObKEynHZSwtUJ1FddMg2C4sHz7ca42awemImV8s,2225
|
|
42
49
|
tunacode/core/setup/git_safety_setup.py,sha256=CRIqrQt0QUJQRS344njty_iCqTorrDhHlXRuET7w0Tk,6714
|
|
43
|
-
tunacode/
|
|
50
|
+
tunacode/core/token_usage/api_response_parser.py,sha256=CTtqGaFaxpkzkW3TEbe00QJzyRULpWN1EQxIYMleseg,1622
|
|
51
|
+
tunacode/core/token_usage/cost_calculator.py,sha256=oQPMphGhqIt7NKdOg1o5Zbo59_nwFWmRJMQ30ViiCWs,1835
|
|
52
|
+
tunacode/core/token_usage/usage_tracker.py,sha256=uKCpdZgmDfLauwsawSCifMu0kJE3lAnK7Sjd-0KYUgA,3894
|
|
53
|
+
tunacode/prompts/dspy_task_planning.md,sha256=RNimkmnFcNgskwQrguGb3wB8A-Zngp6Qc9lXfPj61OU,2512
|
|
54
|
+
tunacode/prompts/dspy_tool_selection.md,sha256=CPBHrI4QlWD2mzNdoVkFK7GydgCLW9Bi20sD8ZEysRo,3070
|
|
55
|
+
tunacode/prompts/system.md,sha256=hXpjZ8Yiv2Acr2_6EmC2uOklP8FbmvyYR9oais-1KLk,16290
|
|
44
56
|
tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
|
|
45
57
|
tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
|
|
46
58
|
tunacode/tools/__init__.py,sha256=ECBuUWWF1JjHW42CCceaPKgVTQyuljbz3RlhuA2fe2s,314
|
|
@@ -52,36 +64,40 @@ tunacode/tools/list_dir.py,sha256=1kNqzYCNlcA5rqXIEVqcjQy6QxlLZLj5AG6YIECfwio,72
|
|
|
52
64
|
tunacode/tools/read_file.py,sha256=z2omev9xzj4-0GG9mRssD13rj-Aa1c-pszFi2Z7Hxvk,3268
|
|
53
65
|
tunacode/tools/read_file_async_poc.py,sha256=2v2ckLQlwahgPGWGdE2c5Es37B35Y7zWdseZwT46E1E,6453
|
|
54
66
|
tunacode/tools/run_command.py,sha256=7UvXjFQI1Av4vceXx48MbQCTrsFNj4PlygTAAhNDYIA,4376
|
|
67
|
+
tunacode/tools/todo.py,sha256=bVbohgwKqvvTe8efxXrMZDQU8vdk4E3jF9Cj38dRq7k,12727
|
|
55
68
|
tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
|
|
56
69
|
tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
|
|
57
70
|
tunacode/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
|
|
58
71
|
tunacode/ui/completers.py,sha256=Jx1zyCESwdm_4ZopvCBtb0bCJF-bRy8aBWG2yhPQtDc,4878
|
|
59
|
-
tunacode/ui/console.py,sha256=
|
|
72
|
+
tunacode/ui/console.py,sha256=icb7uYrV8XmZg9glreEy5MrvDkmrKxbf_ZkNqElN1uE,2120
|
|
60
73
|
tunacode/ui/constants.py,sha256=A76B_KpM8jCuBYRg4cPmhi8_j6LLyWttO7_jjv47r3w,421
|
|
61
74
|
tunacode/ui/decorators.py,sha256=e2KM-_pI5EKHa2M045IjUe4rPkTboxaKHXJT0K3461g,1914
|
|
62
|
-
tunacode/ui/input.py,sha256=
|
|
75
|
+
tunacode/ui/input.py,sha256=E_zAJqNYoAVFA-j4xE9Qgs22y-GrdSZNqiseX-Or0ho,2955
|
|
63
76
|
tunacode/ui/keybindings.py,sha256=h0MlD73CW_3i2dQzb9EFSPkqy0raZ_isgjxUiA9u6ts,691
|
|
64
77
|
tunacode/ui/lexers.py,sha256=tmg4ic1enyTRLzanN5QPP7D_0n12YjX_8ZhsffzhXA4,1340
|
|
65
|
-
tunacode/ui/output.py,sha256=
|
|
66
|
-
tunacode/ui/panels.py,sha256=
|
|
78
|
+
tunacode/ui/output.py,sha256=51O0VHajte4dXHK5Az5SSP4IOb2q5SbCwvqdAoxyg7c,5665
|
|
79
|
+
tunacode/ui/panels.py,sha256=ckL-TYxYWlpBAFj8SC9Od8vrW7Kf5N92bZRYBWR14jE,8338
|
|
67
80
|
tunacode/ui/prompt_manager.py,sha256=U2cntB34vm-YwOj3gzFRUK362zccrz8pigQfpxr5sv8,4650
|
|
68
|
-
tunacode/ui/
|
|
69
|
-
tunacode/ui/
|
|
81
|
+
tunacode/ui/recursive_progress.py,sha256=V0dGpJWt19TVArOYcQ3Lki8cR3ZepFT6iGwnChSFhFI,12906
|
|
82
|
+
tunacode/ui/tool_ui.py,sha256=qp1aZUpLO5UOdJziY8tw0URC8gjoWoSKdGu5y2wuTUU,7013
|
|
83
|
+
tunacode/ui/utils.py,sha256=yvoCTz8AOdRfV0XIqUX3sgg88g_wntV9yhnQP6WzAVs,114
|
|
70
84
|
tunacode/ui/validators.py,sha256=MMIMT1I2v0l2jIy-gxX_4GSApvUTi8XWIOACr_dmoBA,758
|
|
71
85
|
tunacode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
86
|
tunacode/utils/bm25.py,sha256=yq7KFWP3g_zIsjUV7l2hFPXYCzXyNQUInLU7u4qsc_4,1909
|
|
73
87
|
tunacode/utils/diff_utils.py,sha256=V9QqQ0q4MfabVTnWptF3IXDp3estnfOKcJtDe_Sj14I,2372
|
|
74
88
|
tunacode/utils/file_utils.py,sha256=AXiAJ_idtlmXEi9pMvwtfPy9Ys3yK-F4K7qb_NpwonU,923
|
|
75
89
|
tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDgs,295
|
|
90
|
+
tunacode/utils/message_utils.py,sha256=kM6VSS2Dudjplie009khHgmIRjDoBUzv6tvHcYNDAAE,586
|
|
91
|
+
tunacode/utils/retry.py,sha256=AHdUzY6m-mwlT4OPXdtWWMAafL_NeS7JAMORGyM8c5k,4931
|
|
76
92
|
tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
|
|
77
93
|
tunacode/utils/security.py,sha256=e_zo9VmcOKFjgFMr9GOBIFhAmND4PBlJZgY7zqnsGjI,6548
|
|
78
94
|
tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
|
|
79
95
|
tunacode/utils/text_utils.py,sha256=6YBD9QfkDO44-6jxnwRWIpmfIifPG-NqMzy_O2NAouc,7277
|
|
80
|
-
tunacode/utils/token_counter.py,sha256=
|
|
96
|
+
tunacode/utils/token_counter.py,sha256=l5KemYLfsypAtAF_YrDtVKFtBEghydS_MA8c-8mpPvM,2721
|
|
81
97
|
tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
|
|
82
|
-
tunacode_cli-0.0.
|
|
83
|
-
tunacode_cli-0.0.
|
|
84
|
-
tunacode_cli-0.0.
|
|
85
|
-
tunacode_cli-0.0.
|
|
86
|
-
tunacode_cli-0.0.
|
|
87
|
-
tunacode_cli-0.0.
|
|
98
|
+
tunacode_cli-0.0.42.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
99
|
+
tunacode_cli-0.0.42.dist-info/METADATA,sha256=VsQw_2wQ_Y1gnU-Ts2cNqnNRzEjvkkLAUwuKrM6Wi6Q,5203
|
|
100
|
+
tunacode_cli-0.0.42.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
101
|
+
tunacode_cli-0.0.42.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
|
|
102
|
+
tunacode_cli-0.0.42.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
|
|
103
|
+
tunacode_cli-0.0.42.dist-info/RECORD,,
|
tunacode/cli/textual_app.py
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Modern Textual-based TUI for TunaCode.
|
|
3
|
-
|
|
4
|
-
Provides a rich, interactive terminal user interface with:
|
|
5
|
-
- Split-pane layout with chat history and input
|
|
6
|
-
- Sidebar with model info and commands
|
|
7
|
-
- Modern dialog boxes for tool confirmations
|
|
8
|
-
- Real-time status updates
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import asyncio
|
|
12
|
-
from typing import Optional
|
|
13
|
-
|
|
14
|
-
from textual import on
|
|
15
|
-
from textual.app import App, ComposeResult
|
|
16
|
-
from textual.binding import Binding
|
|
17
|
-
from textual.containers import Container, Horizontal, Vertical, VerticalScroll
|
|
18
|
-
from textual.message import Message
|
|
19
|
-
from textual.widgets import Button, Footer, Header, Static, TextArea
|
|
20
|
-
|
|
21
|
-
from tunacode.core.state import StateManager
|
|
22
|
-
from tunacode.setup import setup
|
|
23
|
-
from tunacode.utils.system import check_for_updates
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class ChatMessage(Static):
|
|
27
|
-
"""A single chat message widget."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, sender: str, content: str, message_type: str = "user"):
|
|
30
|
-
super().__init__()
|
|
31
|
-
self.sender = sender
|
|
32
|
-
self.content = content
|
|
33
|
-
self.message_type = message_type
|
|
34
|
-
|
|
35
|
-
def compose(self) -> ComposeResult:
|
|
36
|
-
"""Compose the chat message."""
|
|
37
|
-
if self.message_type == "user":
|
|
38
|
-
yield Static(f"[bold cyan]❯ You[/bold cyan]\n{self.content}", classes="user-message")
|
|
39
|
-
elif self.message_type == "agent":
|
|
40
|
-
yield Static(
|
|
41
|
-
f"[bold green]🤖 TunaCode[/bold green]\n{self.content}", classes="agent-message"
|
|
42
|
-
)
|
|
43
|
-
elif self.message_type == "system":
|
|
44
|
-
yield Static(
|
|
45
|
-
f"[bold yellow]⚠️ System[/bold yellow]\n{self.content}", classes="system-message"
|
|
46
|
-
)
|
|
47
|
-
elif self.message_type == "tool":
|
|
48
|
-
yield Static(
|
|
49
|
-
f"[bold magenta]🔧 Tool[/bold magenta]\n{self.content}", classes="tool-message"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class Sidebar(Container):
|
|
54
|
-
"""Sidebar with model info and commands."""
|
|
55
|
-
|
|
56
|
-
def __init__(self, state_manager: StateManager):
|
|
57
|
-
super().__init__()
|
|
58
|
-
self.state_manager = state_manager
|
|
59
|
-
|
|
60
|
-
def compose(self) -> ComposeResult:
|
|
61
|
-
"""Compose the sidebar."""
|
|
62
|
-
yield Static("[bold]TunaCode[/bold]", classes="sidebar-title")
|
|
63
|
-
yield Static(f"Model: {self.state_manager.session.current_model}", id="current-model")
|
|
64
|
-
yield Static("", classes="spacer")
|
|
65
|
-
|
|
66
|
-
yield Static("[bold]Commands[/bold]", classes="section-title")
|
|
67
|
-
yield Static("/help - Show help", classes="command-item")
|
|
68
|
-
yield Static("/clear - Clear chat", classes="command-item")
|
|
69
|
-
yield Static("/model - Switch model", classes="command-item")
|
|
70
|
-
yield Static("/yolo - Toggle confirmations", classes="command-item")
|
|
71
|
-
yield Static("", classes="spacer")
|
|
72
|
-
|
|
73
|
-
yield Static("[bold]Status[/bold]", classes="section-title")
|
|
74
|
-
yield Static("● Ready", id="status", classes="status-ready")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class ChatHistory(VerticalScroll):
|
|
78
|
-
"""Scrollable chat history container."""
|
|
79
|
-
|
|
80
|
-
def add_message(self, sender: str, content: str, message_type: str = "user") -> None:
|
|
81
|
-
"""Add a new message to the chat history."""
|
|
82
|
-
message = ChatMessage(sender, content, message_type)
|
|
83
|
-
self.mount(message)
|
|
84
|
-
self.scroll_end(animate=True)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class InputArea(Container):
|
|
88
|
-
"""Input area with text area and send button."""
|
|
89
|
-
|
|
90
|
-
class SendMessage(Message):
|
|
91
|
-
"""Message sent when user submits input."""
|
|
92
|
-
|
|
93
|
-
def __init__(self, content: str) -> None:
|
|
94
|
-
self.content = content
|
|
95
|
-
super().__init__()
|
|
96
|
-
|
|
97
|
-
def compose(self) -> ComposeResult:
|
|
98
|
-
"""Compose the input area."""
|
|
99
|
-
with Horizontal():
|
|
100
|
-
yield TextArea(id="message-input")
|
|
101
|
-
yield Button("Send", id="send-button", variant="primary")
|
|
102
|
-
|
|
103
|
-
@on(Button.Pressed, "#send-button")
|
|
104
|
-
def send_message(self) -> None:
|
|
105
|
-
"""Send the current message."""
|
|
106
|
-
text_area = self.query_one("#message-input", TextArea)
|
|
107
|
-
content = text_area.text.strip()
|
|
108
|
-
if content:
|
|
109
|
-
self.post_message(self.SendMessage(content))
|
|
110
|
-
text_area.clear()
|
|
111
|
-
|
|
112
|
-
def on_key(self, event) -> None:
|
|
113
|
-
"""Handle key events."""
|
|
114
|
-
if event.key == "ctrl+enter":
|
|
115
|
-
self.send_message()
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class TunaCodeApp(App):
|
|
119
|
-
"""Main TunaCode Textual application."""
|
|
120
|
-
|
|
121
|
-
CSS = """
|
|
122
|
-
Sidebar {
|
|
123
|
-
width: 30;
|
|
124
|
-
background: $surface;
|
|
125
|
-
border: thick $primary;
|
|
126
|
-
padding: 1;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.sidebar-title {
|
|
130
|
-
text-align: center;
|
|
131
|
-
color: $primary;
|
|
132
|
-
margin-bottom: 1;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.section-title {
|
|
136
|
-
color: $accent;
|
|
137
|
-
margin: 1 0;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.command-item {
|
|
141
|
-
color: $text-muted;
|
|
142
|
-
margin-left: 1;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.status-ready {
|
|
146
|
-
color: $success;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.status-busy {
|
|
150
|
-
color: $warning;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.status-error {
|
|
154
|
-
color: $error;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
ChatHistory {
|
|
158
|
-
border: thick $primary;
|
|
159
|
-
padding: 1;
|
|
160
|
-
height: 1fr;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.user-message {
|
|
164
|
-
background: $surface;
|
|
165
|
-
border-left: thick $primary;
|
|
166
|
-
padding: 1;
|
|
167
|
-
margin: 1 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.agent-message {
|
|
171
|
-
background: $surface;
|
|
172
|
-
border-left: thick $success;
|
|
173
|
-
padding: 1;
|
|
174
|
-
margin: 1 0;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.system-message {
|
|
178
|
-
background: $surface;
|
|
179
|
-
border-left: thick $warning;
|
|
180
|
-
padding: 1;
|
|
181
|
-
margin: 1 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.tool-message {
|
|
185
|
-
background: $surface;
|
|
186
|
-
border-left: thick $accent;
|
|
187
|
-
padding: 1;
|
|
188
|
-
margin: 1 0;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
InputArea {
|
|
192
|
-
height: 5;
|
|
193
|
-
padding: 1;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
#message-input {
|
|
197
|
-
height: 3;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
#send-button {
|
|
201
|
-
width: 10;
|
|
202
|
-
margin-left: 1;
|
|
203
|
-
}
|
|
204
|
-
"""
|
|
205
|
-
|
|
206
|
-
BINDINGS = [
|
|
207
|
-
Binding("ctrl+c", "quit", "Quit"),
|
|
208
|
-
Binding("ctrl+l", "clear_chat", "Clear"),
|
|
209
|
-
Binding("f1", "help", "Help"),
|
|
210
|
-
Binding("f2", "model_info", "Model"),
|
|
211
|
-
]
|
|
212
|
-
|
|
213
|
-
def __init__(self, state_manager: StateManager):
|
|
214
|
-
super().__init__()
|
|
215
|
-
self.state_manager = state_manager
|
|
216
|
-
self.chat_history: Optional[ChatHistory] = None
|
|
217
|
-
self.sidebar: Optional[Sidebar] = None
|
|
218
|
-
self.input_area: Optional[InputArea] = None
|
|
219
|
-
|
|
220
|
-
def compose(self) -> ComposeResult:
|
|
221
|
-
"""Compose the main application layout."""
|
|
222
|
-
yield Header()
|
|
223
|
-
|
|
224
|
-
with Horizontal():
|
|
225
|
-
self.sidebar = Sidebar(self.state_manager)
|
|
226
|
-
yield self.sidebar
|
|
227
|
-
|
|
228
|
-
with Vertical():
|
|
229
|
-
self.chat_history = ChatHistory()
|
|
230
|
-
yield self.chat_history
|
|
231
|
-
|
|
232
|
-
self.input_area = InputArea()
|
|
233
|
-
yield self.input_area
|
|
234
|
-
|
|
235
|
-
yield Footer()
|
|
236
|
-
|
|
237
|
-
def on_mount(self) -> None:
|
|
238
|
-
"""Called when the app is mounted."""
|
|
239
|
-
# Add welcome messages
|
|
240
|
-
self.chat_history.add_message(
|
|
241
|
-
"System", "Welcome to TunaCode v0.11 - Your AI-powered development assistant", "system"
|
|
242
|
-
)
|
|
243
|
-
self.chat_history.add_message(
|
|
244
|
-
"System", f"Current model: {self.state_manager.session.current_model}", "system"
|
|
245
|
-
)
|
|
246
|
-
self.chat_history.add_message(
|
|
247
|
-
"System",
|
|
248
|
-
"⚠️ IMPORTANT: Always use git branches before making major changes\n"
|
|
249
|
-
"Type '/help' for available commands",
|
|
250
|
-
"system",
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
@on(InputArea.SendMessage)
|
|
254
|
-
async def handle_message(self, message: InputArea.SendMessage) -> None:
|
|
255
|
-
"""Handle incoming messages from the input area."""
|
|
256
|
-
content = message.content
|
|
257
|
-
|
|
258
|
-
# Add user message to chat
|
|
259
|
-
self.chat_history.add_message("You", content, "user")
|
|
260
|
-
|
|
261
|
-
# Update status
|
|
262
|
-
status_widget = self.sidebar.query_one("#status", Static)
|
|
263
|
-
status_widget.update("● Processing...")
|
|
264
|
-
status_widget.classes = "status-busy"
|
|
265
|
-
|
|
266
|
-
if content.startswith("/"):
|
|
267
|
-
await self.handle_command(content)
|
|
268
|
-
else:
|
|
269
|
-
await self.handle_user_input(content)
|
|
270
|
-
|
|
271
|
-
# Reset status
|
|
272
|
-
status_widget.update("● Ready")
|
|
273
|
-
status_widget.classes = "status-ready"
|
|
274
|
-
|
|
275
|
-
async def handle_command(self, command: str) -> None:
|
|
276
|
-
"""Handle slash commands."""
|
|
277
|
-
if command == "/help":
|
|
278
|
-
help_text = """Available Commands:
|
|
279
|
-
|
|
280
|
-
/help - Show this help message
|
|
281
|
-
/clear - Clear chat history
|
|
282
|
-
/model - Show current model info
|
|
283
|
-
/yolo - Toggle confirmation prompts
|
|
284
|
-
/quit - Exit the application
|
|
285
|
-
|
|
286
|
-
Keyboard Shortcuts:
|
|
287
|
-
Ctrl+C - Quit
|
|
288
|
-
Ctrl+L - Clear chat
|
|
289
|
-
F1 - Help
|
|
290
|
-
F2 - Model info
|
|
291
|
-
Ctrl+Enter - Send message"""
|
|
292
|
-
self.chat_history.add_message("System", help_text, "system")
|
|
293
|
-
|
|
294
|
-
elif command == "/clear":
|
|
295
|
-
await self.action_clear_chat()
|
|
296
|
-
|
|
297
|
-
elif command == "/model":
|
|
298
|
-
model_info = f"Current model: {self.state_manager.session.current_model}"
|
|
299
|
-
self.chat_history.add_message("System", model_info, "system")
|
|
300
|
-
|
|
301
|
-
elif command == "/yolo":
|
|
302
|
-
# Toggle yolo mode
|
|
303
|
-
current_state = getattr(self.state_manager.session, "yolo_mode", False)
|
|
304
|
-
self.state_manager.session.yolo_mode = not current_state
|
|
305
|
-
new_state = "enabled" if not current_state else "disabled"
|
|
306
|
-
self.chat_history.add_message("System", f"Confirmation prompts {new_state}", "system")
|
|
307
|
-
|
|
308
|
-
elif command == "/quit":
|
|
309
|
-
await self.action_quit()
|
|
310
|
-
|
|
311
|
-
else:
|
|
312
|
-
self.chat_history.add_message("System", f"Unknown command: {command}", "system")
|
|
313
|
-
|
|
314
|
-
async def handle_user_input(self, text: str) -> None:
|
|
315
|
-
"""Handle regular user input."""
|
|
316
|
-
try:
|
|
317
|
-
# Use the bridge to process the input
|
|
318
|
-
if not hasattr(self, "_bridge"):
|
|
319
|
-
from tunacode.cli.textual_bridge import TextualAgentBridge
|
|
320
|
-
|
|
321
|
-
self._bridge = TextualAgentBridge(self.state_manager, self._bridge_message_callback)
|
|
322
|
-
|
|
323
|
-
# Process the request
|
|
324
|
-
response = await self._bridge.process_user_input(text)
|
|
325
|
-
|
|
326
|
-
# Add the agent's response to chat
|
|
327
|
-
self.chat_history.add_message("TunaCode", response, "agent")
|
|
328
|
-
|
|
329
|
-
except Exception as e:
|
|
330
|
-
self.chat_history.add_message("System", f"Error: {str(e)}", "system")
|
|
331
|
-
|
|
332
|
-
async def _bridge_message_callback(self, message_type: str, content: str) -> None:
|
|
333
|
-
"""Callback for bridge to send messages to the UI."""
|
|
334
|
-
self.chat_history.add_message("System", content, message_type)
|
|
335
|
-
|
|
336
|
-
def action_clear_chat(self) -> None:
|
|
337
|
-
"""Clear the chat history."""
|
|
338
|
-
self.chat_history.remove_children()
|
|
339
|
-
self.chat_history.add_message("System", "Chat cleared", "system")
|
|
340
|
-
|
|
341
|
-
def action_help(self) -> None:
|
|
342
|
-
"""Show help information."""
|
|
343
|
-
self.handle_command("/help")
|
|
344
|
-
|
|
345
|
-
def action_model_info(self) -> None:
|
|
346
|
-
"""Show model information."""
|
|
347
|
-
self.handle_command("/model")
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
async def run_textual_app(state_manager: StateManager) -> None:
|
|
351
|
-
"""Run the Textual application."""
|
|
352
|
-
app = TunaCodeApp(state_manager)
|
|
353
|
-
await app.run_async()
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def main():
|
|
357
|
-
"""Main entry point for the Textual app."""
|
|
358
|
-
import sys
|
|
359
|
-
|
|
360
|
-
# Handle command line arguments
|
|
361
|
-
version_flag = "--version" in sys.argv or "-v" in sys.argv
|
|
362
|
-
if version_flag:
|
|
363
|
-
from tunacode.constants import APP_VERSION
|
|
364
|
-
|
|
365
|
-
print(f"TunaCode v{APP_VERSION}")
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
# Initialize state manager
|
|
369
|
-
state_manager = StateManager()
|
|
370
|
-
# Show banner
|
|
371
|
-
print("🐟 TunaCode - Modern AI Development Assistant")
|
|
372
|
-
print("=" * 50)
|
|
373
|
-
|
|
374
|
-
# Check for updates
|
|
375
|
-
has_update, latest_version = check_for_updates()
|
|
376
|
-
if has_update:
|
|
377
|
-
print(f"📦 Update available: v{latest_version}")
|
|
378
|
-
print("Run: pip install --upgrade tunacode-cli")
|
|
379
|
-
print()
|
|
380
|
-
|
|
381
|
-
# Parse CLI arguments for configuration
|
|
382
|
-
cli_config = {}
|
|
383
|
-
args = sys.argv[1:]
|
|
384
|
-
i = 0
|
|
385
|
-
while i < len(args):
|
|
386
|
-
if args[i] == "--model" and i + 1 < len(args):
|
|
387
|
-
cli_config["model"] = args[i + 1]
|
|
388
|
-
i += 2
|
|
389
|
-
elif args[i] == "--key" and i + 1 < len(args):
|
|
390
|
-
cli_config["key"] = args[i + 1]
|
|
391
|
-
i += 2
|
|
392
|
-
elif args[i] == "--baseurl" and i + 1 < len(args):
|
|
393
|
-
cli_config["baseurl"] = args[i + 1]
|
|
394
|
-
i += 2
|
|
395
|
-
elif args[i] == "--setup":
|
|
396
|
-
cli_config["setup"] = True
|
|
397
|
-
i += 1
|
|
398
|
-
else:
|
|
399
|
-
i += 1
|
|
400
|
-
|
|
401
|
-
async def run_app():
|
|
402
|
-
try:
|
|
403
|
-
# Run setup
|
|
404
|
-
run_setup = cli_config.get("setup", False)
|
|
405
|
-
await setup(run_setup, state_manager, cli_config)
|
|
406
|
-
|
|
407
|
-
# Run the Textual app
|
|
408
|
-
await run_textual_app(state_manager)
|
|
409
|
-
|
|
410
|
-
except KeyboardInterrupt:
|
|
411
|
-
print("\n👋 Goodbye!")
|
|
412
|
-
except Exception as e:
|
|
413
|
-
print(f"❌ Error: {e}")
|
|
414
|
-
|
|
415
|
-
# Run the async app
|
|
416
|
-
asyncio.run(run_app())
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if __name__ == "__main__":
|
|
420
|
-
main()
|