teddy-cli 0.1.4__tar.gz → 0.1.6__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.
Files changed (144) hide show
  1. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/PKG-INFO +2 -1
  2. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/pyproject.toml +2 -1
  3. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/__main__.py +119 -16
  4. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/cli_helpers.py +15 -0
  5. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/session_cli_handlers.py +149 -2
  6. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +3 -0
  7. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/system_environment_inspector.py +17 -1
  8. teddy_cli-0.1.6/src/teddy_executor/core/ports/inbound/init.py +47 -0
  9. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/environment_inspector.py +10 -1
  10. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/context_service.py +7 -10
  11. teddy_cli-0.1.6/src/teddy_executor/core/services/init_service.py +138 -0
  12. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/markdown_plan_parser.py +5 -3
  13. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/parser_infrastructure.py +3 -1
  14. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/parser_metadata.py +76 -0
  15. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/planning_service.py +1 -0
  16. teddy_cli-0.1.6/src/teddy_executor/core/services/update_checker.py +212 -0
  17. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/helpers.py +1 -1
  18. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/config.yaml +0 -4
  19. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/prompts/architect.xml +23 -8
  20. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/prompts/debugger.xml +4 -4
  21. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/prompts/developer.xml +9 -6
  22. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/prompts/pathfinder.xml +5 -5
  23. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/prompts/prototyper.xml +23 -18
  24. teddy_cli-0.1.4/src/teddy_executor/core/ports/inbound/init.py +0 -15
  25. teddy_cli-0.1.4/src/teddy_executor/core/services/init_service.py +0 -80
  26. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/LICENSE +0 -0
  27. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/__init__.py +0 -0
  28. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/__init__.py +0 -0
  29. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/__init__.py +0 -0
  30. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/cli_formatter.py +0 -0
  31. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/console_plan_reviewer.py +0 -0
  32. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer.py +0 -0
  33. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +0 -0
  34. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +0 -0
  35. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +0 -0
  36. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +0 -0
  37. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +0 -0
  38. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +0 -0
  39. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/__init__.py +0 -0
  40. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/console_interactor.py +0 -0
  41. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/console_interactor_ask_loop.py +0 -0
  42. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/console_interactor_helpers.py +0 -0
  43. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/console_tooling.py +0 -0
  44. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/filesystem_helpers.py +0 -0
  45. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/litellm_adapter.py +0 -0
  46. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/local_file_system_adapter.py +0 -0
  47. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/local_repo_tree_generator.py +0 -0
  48. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/openrouter_hydrator.py +0 -0
  49. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/shell_adapter.py +0 -0
  50. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/shell_command_builder.py +0 -0
  51. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/system_environment_adapter.py +0 -0
  52. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/system_time_adapter.py +0 -0
  53. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/web_scraper_adapter.py +0 -0
  54. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/web_searcher_adapter.py +0 -0
  55. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/adapters/outbound/yaml_config_adapter.py +0 -0
  56. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/container.py +0 -0
  57. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/__init__.py +0 -0
  58. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/__init__.py +0 -0
  59. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/__init__.py +0 -0
  60. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/action_ports.py +0 -0
  61. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/change_set.py +0 -0
  62. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/exceptions.py +0 -0
  63. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/execution_report.py +0 -0
  64. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/orchestrator_ports.py +0 -0
  65. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/plan.py +0 -0
  66. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/planning_ports.py +0 -0
  67. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/project_context.py +0 -0
  68. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/report_assembly_data.py +0 -0
  69. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/session.py +0 -0
  70. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/shell_output.py +0 -0
  71. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/domain/models/web_search_results.py +0 -0
  72. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/__init__.py +0 -0
  73. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/__init__.py +0 -0
  74. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/edit_simulator.py +0 -0
  75. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/get_context_use_case.py +0 -0
  76. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/plan_parser.py +0 -0
  77. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/plan_reviewer.py +0 -0
  78. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/plan_validator.py +0 -0
  79. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/planning_use_case.py +0 -0
  80. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/inbound/run_plan_use_case.py +0 -0
  81. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/__init__.py +0 -0
  82. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/config_service.py +0 -0
  83. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/execution_report_assembler.py +0 -0
  84. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/file_system_manager.py +0 -0
  85. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/llm_client.py +0 -0
  86. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/markdown_report_formatter.py +0 -0
  87. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/prompt_manager.py +0 -0
  88. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/repo_tree_generator.py +0 -0
  89. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/session_loop_guard.py +0 -0
  90. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/session_manager.py +0 -0
  91. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/session_repository.py +0 -0
  92. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/shell_executor.py +0 -0
  93. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/system_environment.py +0 -0
  94. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/time_service.py +0 -0
  95. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/user_interactor.py +0 -0
  96. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/web_scraper.py +0 -0
  97. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/ports/outbound/web_searcher.py +0 -0
  98. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/__init__.py +0 -0
  99. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_changeset_builder.py +0 -0
  100. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_diff_manager.py +0 -0
  101. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_dispatcher.py +0 -0
  102. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_executor.py +0 -0
  103. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_factory.py +0 -0
  104. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_parser_complex.py +0 -0
  105. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/action_parser_strategies.py +0 -0
  106. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/edit_simulator.py +0 -0
  107. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/execution_orchestrator.py +0 -0
  108. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/execution_report_assembler.py +0 -0
  109. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/markdown_report_formatter.py +0 -0
  110. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/parser_reporting.py +0 -0
  111. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/plan_validator.py +0 -0
  112. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/prompt_manager.py +0 -0
  113. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_lifecycle_manager.py +0 -0
  114. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_loop_guard.py +0 -0
  115. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_orchestrator.py +0 -0
  116. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_planner.py +0 -0
  117. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_pruning_service.py +0 -0
  118. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_replanner.py +0 -0
  119. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_repository.py +0 -0
  120. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/session_service.py +0 -0
  121. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/templates/execution_report.md.j2 +0 -0
  122. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/__init__.py +0 -0
  123. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/edit.py +0 -0
  124. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/edit_matcher.py +0 -0
  125. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +0 -0
  126. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/execute.py +0 -0
  127. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/filesystem.py +0 -0
  128. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/services/validation_rules/message.py +0 -0
  129. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/utils/__init__.py +0 -0
  130. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/utils/diff.py +0 -0
  131. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/utils/io.py +0 -0
  132. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/utils/markdown.py +0 -0
  133. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/utils/serialization.py +0 -0
  134. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/core/utils/string.py +0 -0
  135. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/prompts.py +0 -0
  136. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/registries/__init__.py +0 -0
  137. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/registries/infrastructure.py +0 -0
  138. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/registries/reviewer.py +0 -0
  139. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/registries/validators.py +0 -0
  140. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/__init__.py +0 -0
  141. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/.gitignore +0 -0
  142. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/__init__.py +0 -0
  143. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/init.context +0 -0
  144. {teddy_cli-0.1.4 → teddy_cli-0.1.6}/src/teddy_executor/resources/config/prompts/assistant.xml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: teddy-cli
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: A local-first, file-based AI coding workflow that applies the UNIX philosophy to AI collaboration.
5
5
  License: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -30,6 +30,7 @@ Requires-Dist: python-dotenv (>=1.0.1)
30
30
  Requires-Dist: pyyaml (>=6.0,<7.0)
31
31
  Requires-Dist: requests (>=2.33.0,<3.0.0)
32
32
  Requires-Dist: textual (>=8.1.0,<9.0.0)
33
+ Requires-Dist: tld (>=0.10,<1.0)
33
34
  Requires-Dist: trafilatura (>=2.0.0,<3.0.0)
34
35
  Requires-Dist: typer[all]
35
36
  Requires-Dist: urllib3 (>=2.7.0,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "teddy-cli"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "A local-first, file-based AI coding workflow that applies the UNIX philosophy to AI collaboration."
5
5
  authors = ["Raphael Atteritano"]
6
6
  license = "AGPL-3.0-only"
@@ -29,6 +29,7 @@ bandit = ">=1.7.9,<1.8.0"
29
29
  pip-audit = ">=2.7.3,<2.8.0"
30
30
  textual = "^8.1.0"
31
31
  urllib3 = "^2.7.0"
32
+ tld = ">=0.10,<1.0"
32
33
 
33
34
  [tool.poetry.group.dev.dependencies]
34
35
  pytest = "^9.0.3"
@@ -57,8 +57,12 @@ logging.basicConfig(
57
57
  )
58
58
 
59
59
 
60
- def _ensure_project_initialized(container) -> None:
61
- """Lazily performs project anchoring and initialization."""
60
+ def _ensure_project_initialized(container, root_dir: str | None = None) -> None:
61
+ """Lazily performs project anchoring and initialization.
62
+
63
+ If root_dir is provided, uses that path as the root. Otherwise, finds
64
+ the nearest parent containing .teddy/ via find_project_root().
65
+ """
62
66
  from teddy_executor.adapters.inbound.cli_helpers import find_project_root
63
67
  from teddy_executor.core.ports.outbound.file_system_manager import (
64
68
  IFileSystemManager,
@@ -77,7 +81,10 @@ def _ensure_project_initialized(container) -> None:
77
81
  from teddy_executor.adapters.outbound.yaml_config_adapter import YamlConfigAdapter
78
82
  from punq import Scope
79
83
 
80
- root = str(find_project_root())
84
+ if root_dir is None:
85
+ root = str(find_project_root())
86
+ else:
87
+ root = root_dir
81
88
  c = container
82
89
 
83
90
  regs = getattr(c.registrations, "_Registry__registrations", {})
@@ -165,24 +172,61 @@ def start( # noqa: PLR0913
165
172
  )
166
173
 
167
174
 
168
- @app.command()
169
- def init():
175
+ init_app = typer.Typer()
176
+
177
+
178
+ @init_app.callback(invoke_without_command=True)
179
+ def init_callback(ctx: typer.Context):
170
180
  """
171
181
  Initializes the .teddy directory and pre-warms heavy imports for faster startup.
172
182
  """
183
+ from teddy_executor.adapters.inbound.cli_helpers import prewarm_imports
184
+ from teddy_executor.core.ports.inbound.init import IInitUseCase
185
+
173
186
  container = get_container()
174
- _ensure_project_initialized(container)
175
- # Pre-warm heavy imports to reduce first-run latency
176
- try:
177
- import litellm # noqa: F401
178
- import trafilatura # noqa: F401
179
- import pyperclip # noqa: F401
180
- from bs4 import BeautifulSoup # noqa: F401
181
- from ddgs import DDGS # noqa: F401
182
- except ImportError:
183
- pass # Some optional dependencies may not be installed
187
+ _ensure_project_initialized(container, root_dir=str(Path.cwd()))
188
+ prewarm_imports()
184
189
  # Auto-login would trigger here once `teddy login` is implemented (no-op for now)
185
- typer.echo("TeDDy initialized in .teddy folder.")
190
+ if ctx.invoked_subcommand is not None:
191
+ # A subcommand (e.g., prompts, config) will handle its own initialization
192
+ # and output. Skip the full summary to avoid conflicting messages.
193
+ return
194
+ init_use_case = container.resolve(IInitUseCase)
195
+ summary = init_use_case.ensure_initialized()
196
+ typer.echo(f"TeDDy initialized in .teddy folder. {summary}")
197
+
198
+
199
+ @init_app.command()
200
+ def prompts():
201
+ """
202
+ Overwrites bundled prompt XMLs in .teddy/prompts/ with defaults.
203
+ """
204
+ from teddy_executor.core.ports.inbound.init import IInitUseCase
205
+
206
+ container = get_container()
207
+ init_use_case = container.resolve(IInitUseCase)
208
+ status = init_use_case.ensure_prompts_initialized(overwrite=True)
209
+ typer.echo(status)
210
+
211
+
212
+ @init_app.command()
213
+ def config():
214
+ """
215
+ Overwrites config.yaml, .gitignore, and init.context with defaults.
216
+ """
217
+ from teddy_executor.core.ports.inbound.init import IInitUseCase
218
+
219
+ container = get_container()
220
+ init_use_case = container.resolve(IInitUseCase)
221
+ status = init_use_case.ensure_config_initialized(overwrite=True)
222
+ typer.echo(status)
223
+
224
+
225
+ app.add_typer(
226
+ init_app,
227
+ name="init",
228
+ help="Initialize .teddy directory, prompts, or configuration.",
229
+ )
186
230
 
187
231
 
188
232
  @app.command()
@@ -194,6 +238,65 @@ def version() -> None:
194
238
  typer.echo(f"TeDDy v{installed}")
195
239
 
196
240
 
241
+ @app.command()
242
+ def update(
243
+ experimental: bool = typer.Option(
244
+ False,
245
+ "--experimental",
246
+ help="Check and upgrade from TestPyPI instead of PyPI.",
247
+ ),
248
+ ):
249
+ """Checks PyPI for the latest version of TeDDy and displays upgrade
250
+ instructions. Does not upgrade automatically."""
251
+ from teddy_executor.core.services.update_checker import (
252
+ get_current_version,
253
+ fetch_latest_version,
254
+ compare_versions,
255
+ is_prerelease,
256
+ PYPI_URL,
257
+ TEST_PYPI_URL,
258
+ )
259
+
260
+ index_url = TEST_PYPI_URL if experimental else PYPI_URL
261
+ # When experimental, include prerelease versions (TestPyPI only has dev releases)
262
+ latest = fetch_latest_version(index_url, stable_only=not experimental)
263
+
264
+ if latest is None:
265
+ typer.echo("Could not check for updates: network error.")
266
+ return
267
+
268
+ current = get_current_version()
269
+
270
+ needs_update = compare_versions(current, latest)
271
+ is_channel_switch = False
272
+ if not needs_update:
273
+ # If current is a pre-release and the latest is stable, allow downgrade
274
+ # to the stable channel
275
+ if is_prerelease(current) and not is_prerelease(latest):
276
+ is_channel_switch = True
277
+
278
+ if not needs_update and not is_channel_switch:
279
+ typer.echo(f"You are already running the latest version ({current}).")
280
+ return
281
+
282
+ if is_channel_switch:
283
+ typer.echo(f"You are running the latest experimental version ({current}).")
284
+ typer.echo(
285
+ "To switch to the stable release, run: pip uninstall -y teddy-cli && pip install teddy-cli"
286
+ )
287
+ typer.echo("To apply prompt updates, run: teddy init prompts")
288
+ elif experimental:
289
+ typer.echo(f"A new experimental version {latest} is available.")
290
+ typer.echo(
291
+ "To upgrade, run: pip install --upgrade teddy-cli --index-url https://test.pypi.org/simple/"
292
+ )
293
+ typer.echo("To apply prompt updates, run: teddy init prompts")
294
+ else:
295
+ typer.echo(f"A new version {latest} is available.")
296
+ typer.echo("To upgrade, run: pip install --upgrade teddy-cli")
297
+ typer.echo("To apply prompt updates, run: teddy init prompts")
298
+
299
+
197
300
  @app.command()
198
301
  def context(
199
302
  no_copy: bool = typer.Option(
@@ -247,3 +247,18 @@ def apply_ui_mode_override(container: Container, ui_mode_bool: bool) -> None:
247
247
 
248
248
  mode = "tui" if ui_mode_bool else "console"
249
249
  register_reviewer(container, ui_mode=mode)
250
+
251
+
252
+ def prewarm_imports() -> None:
253
+ """
254
+ Pre-warm heavy imports to reduce first-run latency.
255
+ Extracted from __main__.py init command for reuse by update command.
256
+ """
257
+ try:
258
+ import litellm # noqa: F401
259
+ import trafilatura # noqa: F401
260
+ import pyperclip # noqa: F401
261
+ from bs4 import BeautifulSoup # noqa: F401
262
+ from ddgs import DDGS # noqa: F401
263
+ except ImportError:
264
+ pass
@@ -1,5 +1,9 @@
1
+ import logging
2
+ import shutil
3
+ import subprocess # nosec B404
1
4
  from pathlib import Path
2
5
  from typing import Dict, Optional, Sequence
6
+
3
7
  import typer
4
8
  from punq import Container
5
9
 
@@ -17,9 +21,94 @@ from teddy_executor.core.utils.string import slugify
17
21
  from teddy_executor.adapters.inbound.cli_formatter import format_project_context
18
22
  from teddy_executor.adapters.inbound.cli_helpers import (
19
23
  echo_and_copy,
24
+ find_project_root,
25
+ )
26
+ from teddy_executor.core.services.update_checker import (
27
+ background_check,
28
+ compare_versions,
29
+ get_current_version,
30
+ read_update_cache,
20
31
  )
21
32
 
22
33
 
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def _ensure_commit_hooks() -> None:
38
+ """Install pre-commit hooks if config exists and CLI is available.
39
+ Shows green notification on success, yellow warning if config missing
40
+ or CLI not found."""
41
+ config_path = Path.cwd() / ".pre-commit-config.yaml"
42
+ if not config_path.exists():
43
+ typer.secho(
44
+ "⚠ No pre-commit hooks configured",
45
+ fg=typer.colors.YELLOW,
46
+ err=True,
47
+ )
48
+ return
49
+ if not shutil.which("pre-commit"):
50
+ typer.secho(
51
+ "⚠ pre-commit CLI not found",
52
+ fg=typer.colors.YELLOW,
53
+ err=True,
54
+ )
55
+ return
56
+ try:
57
+ subprocess.run( # nosec B603 B607
58
+ ["pre-commit", "install", "-t", "pre-commit", "-t", "post-commit"],
59
+ check=True,
60
+ capture_output=True,
61
+ )
62
+ except subprocess.CalledProcessError:
63
+ logger.debug("pre-commit install failed", exc_info=True)
64
+ return
65
+ typer.secho(
66
+ "✓ pre-commit hooks installed",
67
+ fg=typer.colors.GREEN,
68
+ err=True,
69
+ )
70
+
71
+
72
+ def _check_git_initialized() -> None:
73
+ """Verify git repository status. Auto-initializes if not a repo.
74
+ Shows green notification if already a repo or freshly initialized,
75
+ yellow warning if git CLI not found.
76
+ Only checks the current working directory for a `.git` marker.
77
+ Does NOT walk up parent directories."""
78
+ if not shutil.which("git"):
79
+ typer.secho(
80
+ "⚠ Git CLI not found",
81
+ fg=typer.colors.YELLOW,
82
+ err=True,
83
+ )
84
+ return
85
+ # Check if CWD itself contains a .git marker (directory or file)
86
+ if (Path.cwd() / ".git").exists():
87
+ typer.secho(
88
+ "✓ Git repository detected",
89
+ fg=typer.colors.GREEN,
90
+ err=True,
91
+ )
92
+ return
93
+ # No local .git found – auto-initialize
94
+ try:
95
+ subprocess.run(["git", "init"], check=True, capture_output=True) # nosec B603 B607
96
+ except subprocess.CalledProcessError:
97
+ logger.debug("git init failed", exc_info=True)
98
+ return
99
+ typer.secho(
100
+ "✓ Git repository initialized",
101
+ fg=typer.colors.GREEN,
102
+ err=True,
103
+ )
104
+
105
+
106
+ def _run_health_checks() -> None:
107
+ """Run all startup health checks and display notifications if needed."""
108
+ _ensure_commit_hooks()
109
+ _check_git_initialized()
110
+
111
+
23
112
  def _determine_session_name(name: Optional[str], message: Optional[str]) -> str:
24
113
  """Determine session name (slugify message if name is missing)."""
25
114
  if name:
@@ -78,6 +167,34 @@ def _orchestrate_session_loop(
78
167
  break
79
168
 
80
169
 
170
+ def _display_update_notification(cache_path: Path) -> None:
171
+ """Check the update cache and display a non-blocking notification
172
+ if a newer version is available. Called on session startup, after
173
+ the background check thread has been started."""
174
+ try:
175
+ cache = read_update_cache(cache_path)
176
+ if cache is None:
177
+ return
178
+ latest = cache.get("latest_version", "")
179
+ if not latest:
180
+ return
181
+ current = get_current_version()
182
+ if compare_versions(current, latest):
183
+ typer.echo(
184
+ typer.style(
185
+ f"ℹ A new version {latest} is available. "
186
+ "To upgrade, run: pip install --upgrade teddy-cli\n"
187
+ " To apply prompt updates: delete .teddy/prompts/ and run 'teddy init'",
188
+ fg=typer.colors.YELLOW,
189
+ )
190
+ )
191
+ except Exception:
192
+ import logging
193
+
194
+ logger = logging.getLogger(__name__)
195
+ logger.debug("Failed to display update notification", exc_info=True)
196
+
197
+
81
198
  def handle_new_session( # noqa: PLR0913
82
199
  container: Container,
83
200
  name: Optional[str],
@@ -91,11 +208,26 @@ def handle_new_session( # noqa: PLR0913
91
208
  api_key: Optional[str] = None,
92
209
  ):
93
210
  """Logic for the 'start' command."""
211
+ import threading
212
+
213
+ # Start non-blocking background version check
214
+ cache_path = find_project_root() / ".teddy" / ".update_cache.json"
215
+ thread = threading.Thread(
216
+ target=background_check,
217
+ args=(cache_path,),
218
+ daemon=True,
219
+ )
220
+ thread.start()
221
+ _display_update_notification(cache_path)
222
+
94
223
  try:
95
224
  # 0. Ensure project is initialized
96
225
  container.resolve(IInitUseCase).ensure_initialized()
97
226
 
98
- # 1. Pre-flight checks (Fail-fast before user interaction)
227
+ # 1. Health checks (advisory, non-blocking)
228
+ _run_health_checks()
229
+
230
+ # 2. Pre-flight checks (Fail-fast before user interaction)
99
231
  typer.echo("Checking configurations...", err=True)
100
232
  _run_cli_preflight_check(container, agent=agent)
101
233
  _echo_config_success(container, agent, model=model)
@@ -326,8 +458,23 @@ def handle_resume_session( # noqa: PLR0913
326
458
  api_key: Optional[str] = None,
327
459
  ):
328
460
  """Logic for the 'resume' command."""
461
+ import threading
462
+
463
+ # Start non-blocking background version check
464
+ cache_path = find_project_root() / ".teddy" / ".update_cache.json"
465
+ thread = threading.Thread(
466
+ target=background_check,
467
+ args=(cache_path,),
468
+ daemon=True,
469
+ )
470
+ thread.start()
471
+ _display_update_notification(cache_path)
472
+
329
473
  try:
330
- # 1. Pre-flight checks
474
+ # 1. Health checks (advisory, non-blocking)
475
+ _run_health_checks()
476
+
477
+ # 2. Pre-flight checks
331
478
  typer.echo("Checking configurations...", err=True)
332
479
  _run_cli_preflight_check(container)
333
480
 
@@ -154,6 +154,9 @@ async def orchestrate_execution(app: ReviewerApp, node: Any, update_fn: Any) ->
154
154
  action.state = ExecutionStatus.RUNNING
155
155
  app._refresh_node(node)
156
156
 
157
+ # Harvest modified content from pending_temp_file before execution
158
+ harvest_action_content(action, app.INSTRUCTION_MARKER)
159
+
157
160
  try:
158
161
  import anyio
159
162
 
@@ -39,7 +39,7 @@ class SystemEnvironmentInspector(IEnvironmentInspector):
39
39
 
40
40
  def get_git_status(self) -> Optional[str]:
41
41
  """
42
- Gathers the current Git status of the working directory.
42
+ Gathers the current Git status of the working directory (short format).
43
43
  """
44
44
  try:
45
45
  result = self._run_func( # nosec B603 B607
@@ -52,3 +52,19 @@ class SystemEnvironmentInspector(IEnvironmentInspector):
52
52
  return result.stdout.rstrip() if result.stdout else ""
53
53
  except (subprocess.CalledProcessError, FileNotFoundError):
54
54
  return None
55
+
56
+ def get_full_git_status(self) -> Optional[str]:
57
+ """
58
+ Gathers the full Git status of the working directory (including branch info).
59
+ """
60
+ try:
61
+ result = self._run_func( # nosec B603 B607
62
+ ["git", "status"],
63
+ capture_output=True,
64
+ text=True,
65
+ check=True,
66
+ stdin=subprocess.DEVNULL,
67
+ )
68
+ return result.stdout.rstrip() if result.stdout else ""
69
+ except (subprocess.CalledProcessError, FileNotFoundError):
70
+ return None
@@ -0,0 +1,47 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class IInitUseCase(ABC):
5
+ """
6
+ Inbound port for project initialization.
7
+ """
8
+
9
+ @abstractmethod
10
+ def ensure_initialized(self) -> str:
11
+ """
12
+ Ensures the .teddy/ directory and its essential configuration
13
+ files are present in the current project root.
14
+
15
+ Returns:
16
+ A human-readable summary string (e.g., "Config: unchanged. Prompts: updated (3 files).").
17
+ """
18
+ pass
19
+
20
+ @abstractmethod
21
+ def ensure_prompts_initialized(self, overwrite: bool = False) -> str:
22
+ """
23
+ Ensures prompt XML files are present in the .teddy/prompts/ directory.
24
+
25
+ Args:
26
+ overwrite: If True, always overwrite existing prompt files with defaults.
27
+ If False (default), only write missing files.
28
+
29
+ Returns:
30
+ A human-readable status string (e.g., "Prompts overwritten (6 files).").
31
+ """
32
+ pass
33
+
34
+ @abstractmethod
35
+ def ensure_config_initialized(self, overwrite: bool = False) -> str:
36
+ """
37
+ Ensures configuration files (config.yaml, .gitignore, init.context) are present
38
+ in the .teddy/ directory.
39
+
40
+ Args:
41
+ overwrite: If True, always overwrite existing config files with defaults.
42
+ If False (default), only write missing files.
43
+
44
+ Returns:
45
+ A human-readable status string (e.g., "Configuration files overwritten (3 files).").
46
+ """
47
+ pass
@@ -22,9 +22,18 @@ class IEnvironmentInspector(Protocol):
22
22
 
23
23
  def get_git_status(self) -> Optional[str]:
24
24
  """
25
- Gathers the current Git status of the working directory.
25
+ Gathers the current Git status of the working directory (short format).
26
26
 
27
27
  Returns:
28
28
  Optional[str]: The output of 'git status -s' or None if not a git repo.
29
29
  """
30
30
  ...
31
+
32
+ def get_full_git_status(self) -> Optional[str]:
33
+ """
34
+ Gathers the full Git status of the working directory (including branch info).
35
+
36
+ Returns:
37
+ Optional[str]: The output of 'git status' or None if not a git repo.
38
+ """
39
+ ...
@@ -39,7 +39,7 @@ class ContextService(IGetContextUseCase):
39
39
  self._llm_client = llm_client
40
40
  self._web_scraper = web_scraper
41
41
 
42
- def get_context(
42
+ def get_context( # noqa: PLR0913
43
43
  self,
44
44
  context_files: Optional[Dict[str, Sequence[str]]] = None,
45
45
  include_tokens: bool = True,
@@ -53,7 +53,8 @@ class ContextService(IGetContextUseCase):
53
53
  Gathers all project context information by orchestrating its dependencies.
54
54
  """
55
55
  system_info = self._environment_inspector.get_environment_info()
56
- git_status = self._environment_inspector.get_git_status()
56
+ short_git_status = self._environment_inspector.get_git_status()
57
+ full_git_status = self._environment_inspector.get_full_git_status()
57
58
  repo_tree = self._repo_tree_generator.generate_tree()
58
59
 
59
60
  scoped_paths, all_resolved_paths = self._resolve_scoped_paths(context_files)
@@ -79,7 +80,7 @@ class ContextService(IGetContextUseCase):
79
80
  file_contents[url] = None
80
81
 
81
82
  content = self._format_content(
82
- repo_tree, scoped_paths, file_contents, git_status
83
+ repo_tree, scoped_paths, file_contents, full_git_status
83
84
  )
84
85
  content_tokens = (
85
86
  self._llm_client.get_text_token_count(content) if include_tokens else 0
@@ -89,9 +90,9 @@ class ContextService(IGetContextUseCase):
89
90
  header=self._format_header(system_info, current_turn),
90
91
  content=content,
91
92
  scoped_paths=scoped_paths,
92
- git_status=git_status,
93
+ git_status=full_git_status,
93
94
  items=self._collect_items(
94
- scoped_paths, file_contents, git_status, include_tokens
95
+ scoped_paths, file_contents, short_git_status, include_tokens
95
96
  ),
96
97
  agent_name=agent_name,
97
98
  total_window=total_window,
@@ -303,10 +304,6 @@ class ContextService(IGetContextUseCase):
303
304
  git_status: Optional[str] = None,
304
305
  ) -> str:
305
306
  """Formats the main content section of the context report."""
306
- display_status = git_status
307
- if git_status == "":
308
- display_status = "nothing to commit, working tree clean"
309
-
310
307
  # Gather all unique paths
311
308
  all_paths: List[str] = []
312
309
  for paths in scoped_paths.values():
@@ -320,7 +317,7 @@ class ContextService(IGetContextUseCase):
320
317
 
321
318
  content_parts = [
322
319
  "\n## Git Status",
323
- display_status if display_status is not None else "",
320
+ git_status if isinstance(git_status, str) else "",
324
321
  "\n## Project Structure",
325
322
  f"```\n{repo_tree}\n```",
326
323
  ]