tunacode-cli 0.0.14__tar.gz → 0.0.16__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.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (74) hide show
  1. {tunacode_cli-0.0.14/src/tunacode_cli.egg-info → tunacode_cli-0.0.16}/PKG-INFO +1 -1
  2. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/pyproject.toml +1 -1
  3. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/commands.py +101 -0
  4. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/constants.py +1 -1
  5. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
  6. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/SOURCES.txt +2 -1
  7. tunacode_cli-0.0.16/tests/test_escape_mechanism.py +184 -0
  8. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/LICENSE +0 -0
  9. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/README.md +0 -0
  10. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/setup.cfg +0 -0
  11. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/setup.py +0 -0
  12. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/__init__.py +0 -0
  13. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/__init__.py +0 -0
  14. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/main.py +0 -0
  15. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/repl.py +0 -0
  16. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/textual_app.py +0 -0
  17. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/textual_bridge.py +0 -0
  18. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/__init__.py +0 -0
  19. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/defaults.py +0 -0
  20. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/models.py +0 -0
  21. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/settings.py +0 -0
  22. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/context.py +0 -0
  23. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/__init__.py +0 -0
  24. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/agents/__init__.py +0 -0
  25. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/agents/main.py +0 -0
  26. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/__init__.py +0 -0
  27. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/agent_setup.py +0 -0
  28. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/base.py +0 -0
  29. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/config_setup.py +0 -0
  30. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/coordinator.py +0 -0
  31. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/environment_setup.py +0 -0
  32. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/git_safety_setup.py +0 -0
  33. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/state.py +0 -0
  34. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/tool_handler.py +0 -0
  35. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/exceptions.py +0 -0
  36. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/prompts/system.txt +0 -0
  37. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/py.typed +0 -0
  38. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/services/__init__.py +0 -0
  39. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/services/mcp.py +0 -0
  40. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/setup.py +0 -0
  41. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/__init__.py +0 -0
  42. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/base.py +0 -0
  43. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/bash.py +0 -0
  44. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/grep.py +0 -0
  45. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/read_file.py +0 -0
  46. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/run_command.py +0 -0
  47. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/update_file.py +0 -0
  48. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/write_file.py +0 -0
  49. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/types.py +0 -0
  50. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/__init__.py +0 -0
  51. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/completers.py +0 -0
  52. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/console.py +0 -0
  53. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/constants.py +0 -0
  54. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/decorators.py +0 -0
  55. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/input.py +0 -0
  56. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/keybindings.py +0 -0
  57. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/lexers.py +0 -0
  58. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/output.py +0 -0
  59. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/panels.py +0 -0
  60. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/prompt_manager.py +0 -0
  61. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/tool_ui.py +0 -0
  62. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/validators.py +0 -0
  63. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/__init__.py +0 -0
  64. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/bm25.py +0 -0
  65. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/diff_utils.py +0 -0
  66. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/file_utils.py +0 -0
  67. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/ripgrep.py +0 -0
  68. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/system.py +0 -0
  69. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/text_utils.py +0 -0
  70. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/user_configuration.py +0 -0
  71. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
  72. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
  73. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/requires.txt +0 -0
  74. {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.14
3
+ Version: 0.0.16
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tunacode-cli"
7
- version = "0.0.14"
7
+ version = "0.0.16"
8
8
  description = "Your agentic CLI developer."
9
9
  keywords = ["cli", "agent", "development", "automation"]
10
10
  readme = "README.md"
@@ -493,6 +493,106 @@ class CompactCommand(SimpleCommand):
493
493
  context.state_manager.session.messages = context.state_manager.session.messages[-2:]
494
494
 
495
495
 
496
+ class UpdateCommand(SimpleCommand):
497
+ """Update TunaCode to the latest version."""
498
+
499
+ def __init__(self):
500
+ super().__init__(
501
+ CommandSpec(
502
+ name="update",
503
+ aliases=["/update"],
504
+ description="Update TunaCode to the latest version",
505
+ category=CommandCategory.SYSTEM,
506
+ )
507
+ )
508
+
509
+ async def execute(self, args: List[str], context: CommandContext) -> None:
510
+ import subprocess
511
+ import sys
512
+ import shutil
513
+
514
+ await ui.info("Checking for TunaCode updates...")
515
+
516
+ # Detect installation method
517
+ installation_method = None
518
+
519
+ # Check if installed via pipx
520
+ if shutil.which("pipx"):
521
+ try:
522
+ result = subprocess.run(
523
+ ["pipx", "list"],
524
+ capture_output=True,
525
+ text=True,
526
+ timeout=10
527
+ )
528
+ if "tunacode" in result.stdout.lower():
529
+ installation_method = "pipx"
530
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
531
+ pass
532
+
533
+ # Check if installed via pip
534
+ if not installation_method:
535
+ try:
536
+ result = subprocess.run(
537
+ [sys.executable, "-m", "pip", "show", "tunacode-cli"],
538
+ capture_output=True,
539
+ text=True,
540
+ timeout=10
541
+ )
542
+ if result.returncode == 0:
543
+ installation_method = "pip"
544
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
545
+ pass
546
+
547
+ if not installation_method:
548
+ await ui.error("Could not detect TunaCode installation method")
549
+ await ui.muted("Manual update options:")
550
+ await ui.muted(" pipx: pipx upgrade tunacode")
551
+ await ui.muted(" pip: pip install --upgrade tunacode-cli")
552
+ return
553
+
554
+ # Perform update based on detected method
555
+ try:
556
+ if installation_method == "pipx":
557
+ await ui.info("Updating via pipx...")
558
+ result = subprocess.run(
559
+ ["pipx", "upgrade", "tunacode"],
560
+ capture_output=True,
561
+ text=True,
562
+ timeout=60
563
+ )
564
+ else: # pip
565
+ await ui.info("Updating via pip...")
566
+ result = subprocess.run(
567
+ [sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
568
+ capture_output=True,
569
+ text=True,
570
+ timeout=60
571
+ )
572
+
573
+ if result.returncode == 0:
574
+ await ui.success("TunaCode updated successfully!")
575
+ await ui.muted("Restart TunaCode to use the new version")
576
+
577
+ # Show update output if available
578
+ if result.stdout.strip():
579
+ output_lines = result.stdout.strip().split('\n')
580
+ for line in output_lines[-5:]: # Show last 5 lines
581
+ if line.strip():
582
+ await ui.muted(f" {line}")
583
+ else:
584
+ await ui.error("Update failed")
585
+ if result.stderr:
586
+ await ui.muted(f"Error: {result.stderr.strip()}")
587
+
588
+ except subprocess.TimeoutExpired:
589
+ await ui.error("Update timed out")
590
+ except subprocess.CalledProcessError as e:
591
+ await ui.error(f"Update failed: {e}")
592
+ except FileNotFoundError:
593
+ await ui.error(f"Could not find {installation_method} executable")
594
+
595
+
496
596
  class ModelCommand(SimpleCommand):
497
597
  """Manage model selection."""
498
598
 
@@ -628,6 +728,7 @@ class CommandRegistry:
628
728
  FixCommand,
629
729
  ParseToolsCommand,
630
730
  RefreshConfigCommand,
731
+ UpdateCommand,
631
732
  HelpCommand,
632
733
  BranchCommand,
633
734
  # TunaCodeCommand, # TODO: Temporarily disabled
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.14"
10
+ APP_VERSION = "0.0.16"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.14
3
+ Version: 0.0.16
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -68,4 +68,5 @@ src/tunacode_cli.egg-info/SOURCES.txt
68
68
  src/tunacode_cli.egg-info/dependency_links.txt
69
69
  src/tunacode_cli.egg-info/entry_points.txt
70
70
  src/tunacode_cli.egg-info/requires.txt
71
- src/tunacode_cli.egg-info/top_level.txt
71
+ src/tunacode_cli.egg-info/top_level.txt
72
+ tests/test_escape_mechanism.py
@@ -0,0 +1,184 @@
1
+ import os
2
+ import time
3
+
4
+ import pytest
5
+
6
+ from tunacode.types import EscapeState
7
+ from tunacode.exceptions import EscapeInterrupt
8
+ from tunacode.core.state import StateManager
9
+ from tunacode.core.tool_handler import ToolHandler
10
+ from tunacode.tools.write_file import WriteFileTool, write_file
11
+ from tunacode.tools.update_file import UpdateFileTool, update_file
12
+ try:
13
+ from tunacode.ui.keybindings import create_key_bindings
14
+ except ImportError:
15
+ create_key_bindings = None
16
+
17
+
18
+ @pytest.fixture(autouse=True)
19
+ def freeze_time(monkeypatch):
20
+ """Freeze time.time() to a mutable value for predictable testing."""
21
+ t = [1000.0]
22
+ monkeypatch.setattr(time, 'time', lambda: t[0])
23
+ return t
24
+
25
+
26
+ def test_escape_state_initial_window_inactive():
27
+ state = EscapeState()
28
+ assert not state.is_window_active()
29
+ assert state.last_escape_time is None
30
+
31
+
32
+ def test_escape_state_window_active_and_reset(freeze_time):
33
+ state = EscapeState()
34
+ # First press sets last_escape_time
35
+ freeze_time[0] = 1000.0
36
+ state.last_escape_time = freeze_time[0]
37
+ # Within 1 second window it should be active
38
+ freeze_time[0] = 1000.5
39
+ assert state.is_window_active()
40
+ # After window expires it should reset state
41
+ freeze_time[0] = 2000.0
42
+ assert not state.is_window_active()
43
+ assert state.last_escape_time is None
44
+ assert not state.escape_pending
45
+ assert not state.message_shown
46
+
47
+
48
+ def test_escape_state_reset():
49
+ state = EscapeState(last_escape_time=1.0, escape_pending=True, message_shown=True)
50
+ state.reset_escape_state()
51
+ assert state.last_escape_time is None
52
+ assert not state.escape_pending
53
+ assert not state.message_shown
54
+
55
+
56
+ def test_tool_handler_check_escape_noop():
57
+ manager = StateManager()
58
+ handler = ToolHandler(manager)
59
+ handler.check_escape()
60
+
61
+
62
+ def test_tool_handler_check_escape_triggers(freeze_time):
63
+ manager = StateManager()
64
+ manager.escape_state.last_escape_time = freeze_time[0]
65
+ manager.escape_state.escape_pending = True
66
+ handler = ToolHandler(manager)
67
+ with pytest.raises(EscapeInterrupt):
68
+ handler.check_escape()
69
+ assert manager.escape_state.last_escape_time is None
70
+ assert not manager.escape_state.escape_pending
71
+
72
+
73
+ class _DummyBuffer:
74
+ def __init__(self):
75
+ self.text_inserted = ''
76
+
77
+ def validate_and_handle(self):
78
+ pass
79
+
80
+ def insert_text(self, text):
81
+ self.text_inserted += text
82
+
83
+
84
+ class _DummyOutput:
85
+ def __init__(self):
86
+ self.messages = []
87
+ def write(self, txt):
88
+ self.messages.append(txt)
89
+
90
+
91
+ class _DummyApp:
92
+ pass
93
+
94
+
95
+ def _make_event(manager):
96
+ app = _DummyApp()
97
+ app.state_manager = manager
98
+ app.output = _DummyOutput()
99
+ return type('Evt', (), {'app': app, 'current_buffer': _DummyBuffer()})()
100
+
101
+
102
+ def test_double_escape_binding(freeze_time):
103
+ if create_key_bindings is None:
104
+ pytest.skip("prompt_toolkit not installed, skipping keybinding test")
105
+ manager = StateManager()
106
+ # Simulate a running task so ESC ESC hint is enabled
107
+ manager.session.current_task = type('DummyTask', (), {'done': lambda self: False})()
108
+ kb = create_key_bindings()
109
+ esc_binding = next(b for b in kb.bindings if b.keys == ('escape',))
110
+ event = _make_event(manager)
111
+ # first ESC press
112
+ freeze_time[0] = 1000.0
113
+ esc_binding.handler(event)
114
+ assert manager.escape_state.escape_pending
115
+ assert event.app.output.messages == ['Press ESC again to stop\n']
116
+ # second ESC press within window
117
+ freeze_time[0] = 1000.5
118
+ with pytest.raises(EscapeInterrupt):
119
+ esc_binding.handler(event)
120
+
121
+ def test_escape_enter_binding(freeze_time):
122
+ if create_key_bindings is None:
123
+ pytest.skip("prompt_toolkit not installed, skipping keybinding test")
124
+ manager = StateManager()
125
+ kb = create_key_bindings()
126
+ # Keys.ControlM is used for Enter key
127
+ from prompt_toolkit.keys import Keys
128
+ ee_binding = next(b for b in kb.bindings if b.keys == (Keys.Escape, Keys.ControlM))
129
+ event = _make_event(manager)
130
+ ee_binding.handler(event)
131
+ buf = event.current_buffer
132
+ assert buf.text_inserted == "\n"
133
+ assert not manager.escape_state.escape_pending
134
+
135
+ def test_ctrl_c_not_overridden():
136
+ if create_key_bindings is None:
137
+ pytest.skip("prompt_toolkit not installed, skipping Ctrl+C compatibility test")
138
+ kb = create_key_bindings()
139
+ # Ensure no custom binding overrides Ctrl+C
140
+ assert not any(binding.keys == ('c-c',) for binding in kb.bindings)
141
+
142
+
143
+ @pytest.mark.asyncio
144
+ async def test_write_file_tool_atomic(tmp_path):
145
+ filepath = tmp_path / 'out.txt'
146
+ content = 'Hello, Tuna!'
147
+ tool = WriteFileTool(None)
148
+ result = await tool._execute(str(filepath), content)
149
+ assert filepath.read_text(encoding='utf-8') == content
150
+ assert 'Successfully wrote to new file' in result
151
+ assert not os.path.exists(str(filepath) + '.tuna_tmp')
152
+
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_write_file_convenience(tmp_path):
156
+ filepath = tmp_path / 'out2.txt'
157
+ content = 'TunaCode!'
158
+ result = await write_file(str(filepath), content)
159
+ assert filepath.read_text(encoding='utf-8') == content
160
+ assert 'Successfully wrote to new file' in result
161
+
162
+
163
+ @pytest.mark.asyncio
164
+ async def test_update_file_tool_atomic(tmp_path):
165
+ filepath = tmp_path / 'in.txt'
166
+ original = 'foo\nbar\nbaz\n'
167
+ filepath.write_text(original, encoding='utf-8')
168
+ tool = UpdateFileTool(None)
169
+ result = await tool._execute(str(filepath), 'bar', 'qux')
170
+ updated = filepath.read_text(encoding='utf-8')
171
+ assert 'bar' not in updated and 'qux' in updated
172
+ assert 'updated successfully' in result
173
+ assert not os.path.exists(str(filepath) + '.tuna_tmp')
174
+
175
+
176
+ @pytest.mark.asyncio
177
+ async def test_update_file_convenience(tmp_path):
178
+ filepath = tmp_path / 'in2.txt'
179
+ original = 'alpha\nbeta\n'
180
+ filepath.write_text(original, encoding='utf-8')
181
+ result = await update_file(str(filepath), 'alpha', 'gamma')
182
+ updated = filepath.read_text(encoding='utf-8')
183
+ assert 'alpha' not in updated and 'gamma' in updated
184
+ assert 'updated successfully' in result
File without changes
File without changes
File without changes
File without changes