teddy-cli 0.1.0__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.
Files changed (143) hide show
  1. teddy_cli-0.1.0.dist-info/LICENSE +677 -0
  2. teddy_cli-0.1.0.dist-info/METADATA +33 -0
  3. teddy_cli-0.1.0.dist-info/RECORD +143 -0
  4. teddy_cli-0.1.0.dist-info/WHEEL +4 -0
  5. teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
  6. teddy_executor/__init__.py +1 -0
  7. teddy_executor/__main__.py +335 -0
  8. teddy_executor/adapters/__init__.py +0 -0
  9. teddy_executor/adapters/inbound/__init__.py +0 -0
  10. teddy_executor/adapters/inbound/cli_formatter.py +107 -0
  11. teddy_executor/adapters/inbound/cli_helpers.py +249 -0
  12. teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
  13. teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
  14. teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
  15. teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
  16. teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
  17. teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
  18. teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
  19. teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
  20. teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
  21. teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
  22. teddy_executor/adapters/outbound/__init__.py +7 -0
  23. teddy_executor/adapters/outbound/console_interactor.py +212 -0
  24. teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
  25. teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
  26. teddy_executor/adapters/outbound/console_tooling.py +62 -0
  27. teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
  28. teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
  29. teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
  30. teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
  31. teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
  32. teddy_executor/adapters/outbound/shell_adapter.py +344 -0
  33. teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
  34. teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
  35. teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
  36. teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
  37. teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
  38. teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
  39. teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
  40. teddy_executor/container.py +333 -0
  41. teddy_executor/core/__init__.py +0 -0
  42. teddy_executor/core/domain/__init__.py +0 -0
  43. teddy_executor/core/domain/models/__init__.py +44 -0
  44. teddy_executor/core/domain/models/action_ports.py +28 -0
  45. teddy_executor/core/domain/models/change_set.py +10 -0
  46. teddy_executor/core/domain/models/exceptions.py +40 -0
  47. teddy_executor/core/domain/models/execution_report.py +65 -0
  48. teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
  49. teddy_executor/core/domain/models/plan.py +85 -0
  50. teddy_executor/core/domain/models/planning_ports.py +43 -0
  51. teddy_executor/core/domain/models/project_context.py +56 -0
  52. teddy_executor/core/domain/models/report_assembly_data.py +18 -0
  53. teddy_executor/core/domain/models/session.py +17 -0
  54. teddy_executor/core/domain/models/shell_output.py +12 -0
  55. teddy_executor/core/domain/models/web_search_results.py +26 -0
  56. teddy_executor/core/ports/__init__.py +0 -0
  57. teddy_executor/core/ports/inbound/__init__.py +0 -0
  58. teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
  59. teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
  60. teddy_executor/core/ports/inbound/init.py +15 -0
  61. teddy_executor/core/ports/inbound/plan_parser.py +52 -0
  62. teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
  63. teddy_executor/core/ports/inbound/plan_validator.py +26 -0
  64. teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
  65. teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
  66. teddy_executor/core/ports/outbound/__init__.py +34 -0
  67. teddy_executor/core/ports/outbound/config_service.py +29 -0
  68. teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
  69. teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
  70. teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
  71. teddy_executor/core/ports/outbound/llm_client.py +90 -0
  72. teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
  73. teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
  74. teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
  75. teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
  76. teddy_executor/core/ports/outbound/session_manager.py +97 -0
  77. teddy_executor/core/ports/outbound/session_repository.py +65 -0
  78. teddy_executor/core/ports/outbound/shell_executor.py +24 -0
  79. teddy_executor/core/ports/outbound/system_environment.py +25 -0
  80. teddy_executor/core/ports/outbound/time_service.py +28 -0
  81. teddy_executor/core/ports/outbound/user_interactor.py +126 -0
  82. teddy_executor/core/ports/outbound/web_scraper.py +24 -0
  83. teddy_executor/core/ports/outbound/web_searcher.py +25 -0
  84. teddy_executor/core/services/__init__.py +0 -0
  85. teddy_executor/core/services/action_changeset_builder.py +90 -0
  86. teddy_executor/core/services/action_diff_manager.py +110 -0
  87. teddy_executor/core/services/action_dispatcher.py +142 -0
  88. teddy_executor/core/services/action_executor.py +209 -0
  89. teddy_executor/core/services/action_factory.py +197 -0
  90. teddy_executor/core/services/action_parser_complex.py +216 -0
  91. teddy_executor/core/services/action_parser_strategies.py +84 -0
  92. teddy_executor/core/services/context_service.py +437 -0
  93. teddy_executor/core/services/edit_simulator.py +128 -0
  94. teddy_executor/core/services/execution_orchestrator.py +295 -0
  95. teddy_executor/core/services/execution_report_assembler.py +62 -0
  96. teddy_executor/core/services/init_service.py +80 -0
  97. teddy_executor/core/services/markdown_plan_parser.py +309 -0
  98. teddy_executor/core/services/markdown_report_formatter.py +143 -0
  99. teddy_executor/core/services/parser_infrastructure.py +222 -0
  100. teddy_executor/core/services/parser_metadata.py +153 -0
  101. teddy_executor/core/services/parser_reporting.py +267 -0
  102. teddy_executor/core/services/plan_validator.py +82 -0
  103. teddy_executor/core/services/planning_service.py +242 -0
  104. teddy_executor/core/services/prompt_manager.py +146 -0
  105. teddy_executor/core/services/session_lifecycle_manager.py +228 -0
  106. teddy_executor/core/services/session_loop_guard.py +46 -0
  107. teddy_executor/core/services/session_orchestrator.py +538 -0
  108. teddy_executor/core/services/session_planner.py +43 -0
  109. teddy_executor/core/services/session_pruning_service.py +438 -0
  110. teddy_executor/core/services/session_replanner.py +105 -0
  111. teddy_executor/core/services/session_repository.py +194 -0
  112. teddy_executor/core/services/session_service.py +529 -0
  113. teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
  114. teddy_executor/core/services/validation_rules/__init__.py +4 -0
  115. teddy_executor/core/services/validation_rules/edit.py +207 -0
  116. teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
  117. teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
  118. teddy_executor/core/services/validation_rules/execute.py +37 -0
  119. teddy_executor/core/services/validation_rules/filesystem.py +73 -0
  120. teddy_executor/core/services/validation_rules/helpers.py +178 -0
  121. teddy_executor/core/services/validation_rules/message.py +29 -0
  122. teddy_executor/core/utils/__init__.py +1 -0
  123. teddy_executor/core/utils/diff.py +57 -0
  124. teddy_executor/core/utils/io.py +75 -0
  125. teddy_executor/core/utils/markdown.py +131 -0
  126. teddy_executor/core/utils/serialization.py +39 -0
  127. teddy_executor/core/utils/string.py +351 -0
  128. teddy_executor/prompts.py +45 -0
  129. teddy_executor/registries/__init__.py +1 -0
  130. teddy_executor/registries/infrastructure.py +147 -0
  131. teddy_executor/registries/reviewer.py +57 -0
  132. teddy_executor/registries/validators.py +47 -0
  133. teddy_executor/resources/__init__.py +1 -0
  134. teddy_executor/resources/config/.gitignore +2 -0
  135. teddy_executor/resources/config/__init__.py +1 -0
  136. teddy_executor/resources/config/config.yaml +49 -0
  137. teddy_executor/resources/config/init.context +5 -0
  138. teddy_executor/resources/config/prompts/architect.xml +462 -0
  139. teddy_executor/resources/config/prompts/assistant.xml +336 -0
  140. teddy_executor/resources/config/prompts/debugger.xml +456 -0
  141. teddy_executor/resources/config/prompts/developer.xml +481 -0
  142. teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
  143. teddy_executor/resources/config/prompts/prototyper.xml +425 -0
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ import typer
5
+
6
+ if TYPE_CHECKING:
7
+ from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
8
+ from teddy_executor.adapters.outbound.console_tooling import ConsoleToolingHelper
9
+
10
+
11
+ class ConsoleAskLoop:
12
+ """Handles the interactive loop for capturing user response via terminal or editor."""
13
+
14
+ def __init__(self, system_env: ISystemEnvironment, tooling: ConsoleToolingHelper):
15
+ self._system_env = system_env
16
+ self._tooling = tooling
17
+ self._active_editor_path: Optional[str] = None
18
+ self._active_editor_marker: Optional[str] = None
19
+
20
+ def run(self, prompt: str) -> str:
21
+ """Orchestrates the interactive loop for capturing user response."""
22
+ while True:
23
+ prompt_label = "Response (type 'e' for editor) › "
24
+ if self._active_editor_path:
25
+ prompt_label = (
26
+ "Editor opened. Terminal reply or [Enter] to confirm editor › "
27
+ )
28
+
29
+ typer.echo(prompt_label, nl=False, err=True)
30
+ try:
31
+ user_input = input().strip()
32
+ except EOFError:
33
+ self.cleanup()
34
+ return ""
35
+
36
+ if user_input.lower() == "e":
37
+ self._launch_editor_background(prompt)
38
+ continue
39
+
40
+ if user_input:
41
+ self.cleanup()
42
+ return user_input
43
+
44
+ response = self._handle_empty_input(prompt)
45
+ if response is not None:
46
+ return response
47
+
48
+ def _handle_empty_input(self, prompt: str) -> Optional[str]:
49
+ """Handles logic when Enter is pressed without terminal input."""
50
+ if self._active_editor_path:
51
+ return self._read_editor_result()
52
+
53
+ typer.echo(
54
+ "Press [Enter] again to confirm empty response › ",
55
+ nl=False,
56
+ err=True,
57
+ )
58
+ try:
59
+ confirm = input().strip()
60
+ except EOFError:
61
+ return ""
62
+
63
+ if not confirm:
64
+ return ""
65
+
66
+ if confirm.lower() == "e":
67
+ self._launch_editor_background(prompt)
68
+ return None
69
+
70
+ return confirm
71
+
72
+ def cleanup(self):
73
+ """Removes the temp file and resets active state."""
74
+ if self._active_editor_path:
75
+ self._system_env.delete_file(self._active_editor_path)
76
+ self._active_editor_path = None
77
+ self._active_editor_marker = None
78
+
79
+ def _launch_editor_background(self, prompt: str) -> None:
80
+ """Opens a temporary file in a non-blocking external editor."""
81
+ marker = "<!-- Please enter your response above this line. -->"
82
+ initial_content = f"\n\n{marker}\n\n{prompt}\n"
83
+
84
+ temp_path = self._system_env.create_temp_file(suffix=".md")
85
+ with open(temp_path, "w", encoding="utf-8") as f:
86
+ f.write(initial_content)
87
+
88
+ editor_cmd = self._tooling.find_editor()
89
+ if not editor_cmd:
90
+ typer.echo("Error: No suitable editor found.", err=True)
91
+ self._system_env.delete_file(temp_path)
92
+ return
93
+
94
+ try:
95
+ cmd = editor_cmd + [temp_path]
96
+ self._system_env.run_command(cmd, background=True)
97
+ self._active_editor_path = temp_path
98
+ self._active_editor_marker = marker
99
+ except Exception as e:
100
+ typer.echo(f"Error: Editor launch failed: {e}", err=True)
101
+ self._system_env.delete_file(temp_path)
102
+
103
+ def _read_editor_result(self) -> str:
104
+ """Reads the content of the background editor's temp file."""
105
+ if not self._active_editor_path:
106
+ return ""
107
+
108
+ try:
109
+ with open(self._active_editor_path, "r", encoding="utf-8") as f:
110
+ content = f.read()
111
+
112
+ marker = self._active_editor_marker or ""
113
+ if marker in content:
114
+ content = content.split(marker)[0]
115
+
116
+ return content.strip()
117
+ except Exception as e:
118
+ typer.echo(f"Error: Reading editor result failed: {e}", err=True)
119
+ return ""
120
+ finally:
121
+ self.cleanup()
@@ -0,0 +1,95 @@
1
+ import logging
2
+ import sys
3
+ from typing import List, Optional
4
+
5
+ import typer
6
+
7
+ from teddy_executor.adapters.inbound.cli_formatter import (
8
+ echo_handoff_details,
9
+ echo_skipped_action,
10
+ )
11
+ from teddy_executor.core.domain.models.change_set import ChangeSet
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def restore_terminal_mode():
17
+ """Restores stdin to canonical/echo mode (Unix only)."""
18
+ import os
19
+
20
+ if (
21
+ sys.platform == "win32"
22
+ or "PYTEST_CURRENT_TEST" in os.environ
23
+ or not sys.stdin.isatty()
24
+ ):
25
+ return
26
+
27
+ try:
28
+ import termios
29
+
30
+ try:
31
+ fd = sys.stdin.fileno()
32
+ except Exception:
33
+ return
34
+ attrs = termios.tcgetattr(fd)
35
+ # iflags: Ensure ICRNL is set (Map CR to NL on input)
36
+ attrs[0] = attrs[0] | termios.ICRNL
37
+ # lflags: Re-enable canonical mode (ICANON) and echo (ECHO)
38
+ attrs[3] = (
39
+ attrs[3] | termios.ICANON | termios.ECHO | termios.ISIG | termios.IEXTEN
40
+ )
41
+ # Apply changes and FLUSH the input buffer
42
+ termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)
43
+ except Exception as e:
44
+ logger.debug("Failed to restore terminal mode: %s", e)
45
+
46
+
47
+ def prepare_external_preview_files(
48
+ system_env, change_set: ChangeSet, temp_files: List[str]
49
+ ) -> List[str]:
50
+ """Sets up temp files for external diff/editor."""
51
+ ext = "".join(change_set.path.suffixes)
52
+ if change_set.action_type == "CREATE":
53
+ preview_path = system_env.create_temp_file(suffix=f".preview{ext}")
54
+ temp_files.append(preview_path)
55
+ with open(preview_path, "w", encoding="utf-8") as f:
56
+ f.write(change_set.after_content)
57
+ return [preview_path]
58
+
59
+ before_path = system_env.create_temp_file(suffix=f".before{ext}")
60
+ after_path = system_env.create_temp_file(suffix=f".after{ext}")
61
+ temp_files.extend([before_path, after_path])
62
+ with open(before_path, "w", encoding="utf-8") as f:
63
+ f.write(change_set.before_content)
64
+ with open(after_path, "w", encoding="utf-8") as f:
65
+ f.write(change_set.after_content)
66
+ return [before_path, after_path]
67
+
68
+
69
+ def display_handoff_and_confirm(
70
+ action_type: str,
71
+ target_agent: Optional[str],
72
+ resources: List[str],
73
+ message: str,
74
+ ) -> tuple[bool, str]:
75
+ """Displays a handoff request and asks for confirmation."""
76
+ echo_handoff_details(action_type, target_agent, resources, message)
77
+ try:
78
+ response = typer.prompt(
79
+ "Press [Enter] to approve, or type a reason for rejection",
80
+ default="",
81
+ show_default=False,
82
+ err=True,
83
+ )
84
+
85
+ if response:
86
+ return False, response # Rejected
87
+ return True, "" # Approved
88
+ except (EOFError, typer.Abort):
89
+ typer.echo("\nAborted.", err=True)
90
+ return False, "Skipped due to non-interactive session."
91
+
92
+
93
+ def print_skipped_action(action, reason: str) -> None:
94
+ """Prints a colorized warning that an action was skipped."""
95
+ echo_skipped_action(action, reason)
@@ -0,0 +1,62 @@
1
+ import shlex
2
+ from typing import Optional, List
3
+ from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
4
+ from teddy_executor.core.ports.outbound.config_service import IConfigService
5
+
6
+
7
+ class ConsoleToolingHelper:
8
+ def __init__(self, system_env: ISystemEnvironment, config_service: IConfigService):
9
+ self._system_env = system_env
10
+ self._config_service = config_service
11
+
12
+ def get_diff_viewer_command(self) -> Optional[List[str]]:
13
+ custom_tool_str = self._system_env.get_env("TEDDY_DIFF_TOOL")
14
+ if custom_tool_str:
15
+ custom_tool_parts = shlex.split(custom_tool_str)
16
+ tool_name = custom_tool_parts[0]
17
+ if tool_path := self._system_env.which(tool_name):
18
+ custom_tool_parts[0] = tool_path
19
+ return custom_tool_parts
20
+ return None
21
+
22
+ if code_path := self._system_env.which("code"):
23
+ return [code_path, "-r", "--diff", "--wait"]
24
+ return None
25
+
26
+ def find_editor(self) -> Optional[List[str]]:
27
+ # 1. Check Config
28
+ if cmd := self._resolve_editor_cmd(self._config_service.get_setting("editor")):
29
+ return cmd
30
+
31
+ # 2. Check Env
32
+ env_editor = self._system_env.get_env("VISUAL") or self._system_env.get_env(
33
+ "EDITOR"
34
+ )
35
+ if cmd := self._resolve_editor_cmd(env_editor):
36
+ return cmd
37
+
38
+ # 3. Discovery Fallback
39
+ for fallback in ["code", "nano"]:
40
+ if path := self._system_env.which(fallback):
41
+ cmd = [path]
42
+ if fallback == "code":
43
+ cmd.extend(["-r", "--wait"])
44
+ return cmd
45
+
46
+ return None
47
+
48
+ def _resolve_editor_cmd(self, editor_str: Optional[str]) -> Optional[List[str]]:
49
+ """Parses a command string and resolves the executable path."""
50
+ if not editor_str:
51
+ return None
52
+ parts = shlex.split(editor_str)
53
+
54
+ if tool_path := self._system_env.which(parts[0]):
55
+ parts[0] = tool_path
56
+ # If the tool is VS Code, ensure reuse and wait flags are present
57
+ if parts[0].endswith("code") and "code" in parts[0].lower():
58
+ for flag in ["-r", "--wait"]:
59
+ if flag not in parts:
60
+ parts.append(flag)
61
+ return parts
62
+ return None
@@ -0,0 +1,61 @@
1
+ from pathlib import Path
2
+ from typing import Any, Iterator, Tuple
3
+
4
+
5
+ def load_ignore_spec(root_dir: Path) -> Any:
6
+ """
7
+ Loads and returns a pathspec for ignores (.gitignore and .teddyignore).
8
+ """
9
+ import pathspec
10
+
11
+ default_ignores = {
12
+ ".git/",
13
+ ".venv/",
14
+ "__pycache__/",
15
+ ".teddy/",
16
+ ".ruff_cache/",
17
+ }
18
+ lines = list(default_ignores)
19
+
20
+ gitignore_path = root_dir / ".gitignore"
21
+ if gitignore_path.is_file():
22
+ lines.append(gitignore_path.name)
23
+ lines.extend(gitignore_path.read_text(encoding="utf-8").splitlines())
24
+
25
+ teddyignore_path = root_dir / ".teddyignore"
26
+ if teddyignore_path.is_file():
27
+ lines.append(teddyignore_path.name)
28
+ lines.extend(teddyignore_path.read_text(encoding="utf-8").splitlines())
29
+
30
+ return pathspec.PathSpec.from_lines("gitwildmatch", lines)
31
+
32
+
33
+ def walk_recursive(
34
+ root_dir: Path,
35
+ start_dir: Path,
36
+ spec: Any,
37
+ ) -> Iterator[Tuple[Path, bool]]:
38
+ """
39
+ Recursively walks a directory, yielding (Path, is_dir) for non-ignored entries.
40
+ Yielded paths are absolute.
41
+ """
42
+ for entry in start_dir.iterdir():
43
+ try:
44
+ rel_path = entry.relative_to(root_dir)
45
+ except ValueError:
46
+ # Fallback for paths outside root (e.g. symlinks)
47
+ rel_path = entry
48
+
49
+ rel_path_str = str(rel_path).replace("\\", "/")
50
+ is_real_dir = entry.is_dir() and not entry.is_symlink()
51
+
52
+ # For directories, add a trailing slash to match gitignore behavior
53
+ match_path = rel_path_str + "/" if is_real_dir else rel_path_str
54
+
55
+ if spec.match_file(match_path):
56
+ continue
57
+
58
+ yield entry, is_real_dir
59
+
60
+ if is_real_dir:
61
+ yield from walk_recursive(root_dir, entry, spec)