shotgun-sh 0.1.12.dev4__tar.gz → 0.1.13__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 shotgun-sh might be problematic. Click here for more details.

Files changed (134) hide show
  1. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/PKG-INFO +1 -1
  2. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/pyproject.toml +1 -1
  3. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/agent_manager.py +30 -1
  4. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/build_constants.py +2 -2
  5. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/codebase/commands.py +71 -2
  6. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/ingestor.py +113 -4
  7. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/manager.py +151 -2
  8. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/models.py +25 -0
  9. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/service.py +10 -2
  10. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +13 -0
  11. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/sdk/codebase.py +11 -2
  12. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/app.py +20 -0
  13. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/chat.py +80 -0
  14. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/.gitignore +0 -0
  15. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/LICENSE +0 -0
  16. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/README.md +0 -0
  17. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/hatch_build.py +0 -0
  18. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/__init__.py +0 -0
  19. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/__init__.py +0 -0
  20. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/common.py +0 -0
  21. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/config/__init__.py +0 -0
  22. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/config/constants.py +0 -0
  23. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/config/manager.py +0 -0
  24. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/config/models.py +0 -0
  25. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/config/provider.py +0 -0
  26. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/conversation_history.py +0 -0
  27. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/conversation_manager.py +0 -0
  28. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/export.py +0 -0
  29. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/__init__.py +0 -0
  30. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/compaction.py +0 -0
  31. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/constants.py +0 -0
  32. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/context_extraction.py +0 -0
  33. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/history_building.py +0 -0
  34. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/history_processors.py +0 -0
  35. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/message_utils.py +0 -0
  36. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/token_counting.py +0 -0
  37. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/history/token_estimation.py +0 -0
  38. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/messages.py +0 -0
  39. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/models.py +0 -0
  40. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/plan.py +0 -0
  41. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/research.py +0 -0
  42. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/specify.py +0 -0
  43. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tasks.py +0 -0
  44. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/__init__.py +0 -0
  45. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  46. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  47. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  48. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  49. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/models.py +0 -0
  50. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  51. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  52. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/file_management.py +0 -0
  53. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/user_interaction.py +0 -0
  54. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  55. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  56. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  57. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  58. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  59. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/__init__.py +0 -0
  60. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/codebase/__init__.py +0 -0
  61. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/codebase/models.py +0 -0
  62. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/config.py +0 -0
  63. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/export.py +0 -0
  64. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/models.py +0 -0
  65. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/plan.py +0 -0
  66. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/research.py +0 -0
  67. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/specify.py +0 -0
  68. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/tasks.py +0 -0
  69. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/update.py +0 -0
  70. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/cli/utils.py +0 -0
  71. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/__init__.py +0 -0
  72. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/__init__.py +0 -0
  73. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/change_detector.py +0 -0
  74. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  75. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/cypher_models.py +0 -0
  76. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/language_config.py +0 -0
  77. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/nl_query.py +0 -0
  78. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/codebase/core/parser_loader.py +0 -0
  79. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/logging_config.py +0 -0
  80. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/main.py +0 -0
  81. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/posthog_telemetry.py +0 -0
  82. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/__init__.py +0 -0
  83. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/__init__.py +0 -0
  84. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/export.j2 +0 -0
  85. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  86. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  87. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  88. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  89. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/plan.j2 +0 -0
  90. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/research.j2 +0 -0
  91. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/specify.j2 +0 -0
  92. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  93. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  94. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  95. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/__init__.py +0 -0
  96. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  97. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  98. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  99. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  100. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  101. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/history/__init__.py +0 -0
  102. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  103. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/history/summarization.j2 +0 -0
  104. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/prompts/loader.py +0 -0
  105. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/py.typed +0 -0
  106. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/sdk/__init__.py +0 -0
  107. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/sdk/exceptions.py +0 -0
  108. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/sdk/models.py +0 -0
  109. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/sdk/services.py +0 -0
  110. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/sentry_telemetry.py +0 -0
  111. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/telemetry.py +0 -0
  112. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/__init__.py +0 -0
  113. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/commands/__init__.py +0 -0
  114. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/components/prompt_input.py +0 -0
  115. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/components/spinner.py +0 -0
  116. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/components/splash.py +0 -0
  117. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/components/vertical_tail.py +0 -0
  118. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/filtered_codebase_service.py +0 -0
  119. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/chat.tcss +0 -0
  120. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  121. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
  122. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  123. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/chat_screen/history.py +0 -0
  124. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/directory_setup.py +0 -0
  125. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/provider_config.py +0 -0
  126. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/screens/splash.py +0 -0
  127. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/styles.tcss +0 -0
  128. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/utils/__init__.py +0 -0
  129. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/tui/utils/mode_progress.py +0 -0
  130. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/utils/__init__.py +0 -0
  131. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/utils/env_utils.py +0 -0
  132. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/utils/file_system_utils.py +0 -0
  133. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/utils/source_detection.py +0 -0
  134. {shotgun_sh-0.1.12.dev4 → shotgun_sh-0.1.13}/src/shotgun/utils/update_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.12.dev4
3
+ Version: 0.1.13
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.12.dev4"
3
+ version = "0.1.13"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -31,6 +31,7 @@ from pydantic_ai.messages import (
31
31
  SystemPromptPart,
32
32
  ToolCallPart,
33
33
  ToolCallPartDelta,
34
+ ToolReturnPart,
34
35
  )
35
36
  from textual.message import Message
36
37
  from textual.widget import Widget
@@ -44,7 +45,7 @@ from shotgun.utils.source_detection import detect_source
44
45
  from .export import create_export_agent
45
46
  from .history.compaction import apply_persistent_compaction
46
47
  from .messages import AgentSystemPrompt
47
- from .models import AgentDeps, AgentRuntimeOptions
48
+ from .models import AgentDeps, AgentRuntimeOptions, UserAnswer
48
49
  from .plan import create_plan_agent
49
50
  from .research import create_research_agent
50
51
  from .specify import create_specify_agent
@@ -272,6 +273,8 @@ class AgentManager(Widget):
272
273
  # Use merged deps (shared state + agent-specific system prompt) if not provided
273
274
  if deps is None:
274
275
  deps = self._create_merged_deps(self._current_agent_type)
276
+ if not deferred_tool_results:
277
+ self.ensure_agent_canecelled_safely()
275
278
 
276
279
  # Ensure deps is not None
277
280
  if deps is None:
@@ -674,6 +677,32 @@ class AgentManager(Widget):
674
677
  self.ui_message_history.append(message)
675
678
  self._post_messages_updated()
676
679
 
680
+ def ensure_agent_canecelled_safely(self) -> None:
681
+ if not self.message_history:
682
+ return
683
+ self.last_response = self.message_history[-1]
684
+ ## we're searching for unanswered ask_user parts
685
+ found_tool = None
686
+ for part in self.message_history[-1].parts:
687
+ if isinstance(part, ToolCallPart) and part.tool_name == "ask_user":
688
+ found_tool = part
689
+ break
690
+ if not found_tool:
691
+ return
692
+ tool_result = ModelRequest(
693
+ parts=[
694
+ ToolReturnPart(
695
+ tool_call_id=found_tool.tool_call_id,
696
+ tool_name=found_tool.tool_name,
697
+ content=UserAnswer(
698
+ answer="⚠️ Operation cancelled by user",
699
+ tool_call_id=found_tool.tool_call_id,
700
+ ),
701
+ )
702
+ ]
703
+ )
704
+ self.message_history.append(tool_result)
705
+
677
706
 
678
707
  # Re-export AgentType for backward compatibility
679
708
  __all__ = [
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
12
12
  POSTHOG_PROJECT_ID = '191396'
13
13
 
14
14
  # Logfire configuration embedded at build time (only for dev builds)
15
- LOGFIRE_ENABLED = 'true'
16
- LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
15
+ LOGFIRE_ENABLED = ''
16
+ LOGFIRE_TOKEN = ''
17
17
 
18
18
  # Build metadata
19
19
  BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
@@ -6,8 +6,21 @@ from pathlib import Path
6
6
  from typing import Annotated
7
7
 
8
8
  import typer
9
+ from rich.console import Console
10
+ from rich.progress import (
11
+ BarColumn,
12
+ Progress,
13
+ SpinnerColumn,
14
+ TaskProgressColumn,
15
+ TextColumn,
16
+ TimeElapsedColumn,
17
+ )
9
18
 
10
- from shotgun.codebase.models import CodebaseGraph, QueryType
19
+ from shotgun.codebase.models import (
20
+ CodebaseGraph,
21
+ IndexProgress,
22
+ QueryType,
23
+ )
11
24
  from shotgun.logging_config import get_logger
12
25
  from shotgun.sdk.codebase import CodebaseSDK
13
26
  from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
@@ -59,10 +72,66 @@ def index(
59
72
  ) -> None:
60
73
  """Index a new codebase."""
61
74
  sdk = CodebaseSDK()
75
+ console = Console()
76
+
77
+ # Create progress display
78
+ progress = Progress(
79
+ SpinnerColumn(),
80
+ TextColumn("[bold blue]{task.description}"),
81
+ BarColumn(),
82
+ TaskProgressColumn(),
83
+ TimeElapsedColumn(),
84
+ console=console,
85
+ )
86
+
87
+ # Track tasks by phase
88
+ tasks = {}
89
+
90
+ def progress_callback(progress_info: IndexProgress) -> None:
91
+ """Update progress display based on indexing phase."""
92
+ phase = progress_info.phase
93
+
94
+ # Create task if it doesn't exist
95
+ if phase not in tasks:
96
+ if progress_info.total is not None:
97
+ tasks[phase] = progress.add_task(
98
+ progress_info.phase_name, total=progress_info.total
99
+ )
100
+ else:
101
+ # Indeterminate progress (spinner only)
102
+ tasks[phase] = progress.add_task(progress_info.phase_name, total=None)
103
+
104
+ task_id = tasks[phase]
105
+
106
+ # Update task
107
+ if progress_info.total is not None:
108
+ progress.update(
109
+ task_id,
110
+ completed=progress_info.current,
111
+ total=progress_info.total,
112
+ description=f"[bold blue]{progress_info.phase_name}",
113
+ )
114
+ else:
115
+ # Just update description for indeterminate tasks
116
+ progress.update(
117
+ task_id,
118
+ description=f"[bold blue]{progress_info.phase_name} ({progress_info.current} items)",
119
+ )
120
+
121
+ # Mark as complete if phase is done
122
+ if progress_info.phase_complete:
123
+ if progress_info.total is not None:
124
+ progress.update(task_id, completed=progress_info.total)
62
125
 
63
126
  try:
64
127
  repo_path = Path(path)
65
- result = asyncio.run(sdk.index_codebase(repo_path, name))
128
+
129
+ # Run indexing with progress display
130
+ with progress:
131
+ result = asyncio.run(
132
+ sdk.index_codebase(repo_path, name, progress_callback=progress_callback)
133
+ )
134
+
66
135
  output_result(result, format_type)
67
136
  except InvalidPathError as e:
68
137
  error_result = ErrorResult(error_message=str(e))
@@ -535,6 +535,7 @@ class SimpleGraphBuilder:
535
535
  parsers: dict[str, Parser],
536
536
  queries: dict[str, Any],
537
537
  exclude_patterns: list[str] | None = None,
538
+ progress_callback: Any | None = None,
538
539
  ):
539
540
  self.ingestor = ingestor
540
541
  self.repo_path = repo_path
@@ -544,6 +545,7 @@ class SimpleGraphBuilder:
544
545
  self.ignore_dirs = IGNORE_PATTERNS
545
546
  if exclude_patterns:
546
547
  self.ignore_dirs = self.ignore_dirs.union(set(exclude_patterns))
548
+ self.progress_callback = progress_callback
547
549
 
548
550
  # Caches
549
551
  self.structural_elements: dict[Path, str | None] = {}
@@ -552,6 +554,34 @@ class SimpleGraphBuilder:
552
554
  self.simple_name_lookup: dict[str, set[str]] = defaultdict(set)
553
555
  self.class_inheritance: dict[str, list[str]] = {} # class_qn -> [parent_qns]
554
556
 
557
+ def _report_progress(
558
+ self,
559
+ phase: str,
560
+ phase_name: str,
561
+ current: int,
562
+ total: int | None = None,
563
+ phase_complete: bool = False,
564
+ ) -> None:
565
+ """Report progress via callback if available."""
566
+ if not self.progress_callback:
567
+ return
568
+
569
+ try:
570
+ # Import here to avoid circular dependency
571
+ from shotgun.codebase.models import IndexProgress, ProgressPhase
572
+
573
+ progress = IndexProgress(
574
+ phase=ProgressPhase(phase),
575
+ phase_name=phase_name,
576
+ current=current,
577
+ total=total,
578
+ phase_complete=phase_complete,
579
+ )
580
+ self.progress_callback(progress)
581
+ except Exception as e:
582
+ # Don't let progress callback errors crash the build
583
+ logger.debug(f"Progress callback error: {e}")
584
+
555
585
  def run(self) -> None:
556
586
  """Run the three-pass graph building process."""
557
587
  logger.info(f"Building graph for project: {self.project_name}")
@@ -575,6 +605,7 @@ class SimpleGraphBuilder:
575
605
 
576
606
  def _identify_structure(self) -> None:
577
607
  """First pass: Walk directory to find packages and folders."""
608
+ dir_count = 0
578
609
  for root_str, dirs, _ in os.walk(self.repo_path, topdown=True):
579
610
  dirs[:] = [d for d in dirs if d not in self.ignore_dirs]
580
611
  root = Path(root_str)
@@ -584,6 +615,13 @@ class SimpleGraphBuilder:
584
615
  if root == self.repo_path:
585
616
  continue
586
617
 
618
+ dir_count += 1
619
+ # Report progress every 10 directories
620
+ if dir_count % 10 == 0:
621
+ self._report_progress(
622
+ "structure", "Identifying packages and folders", dir_count
623
+ )
624
+
587
625
  parent_rel_path = relative_root.parent
588
626
  parent_container_qn = self.structural_elements.get(parent_rel_path)
589
627
 
@@ -686,8 +724,34 @@ class SimpleGraphBuilder:
686
724
 
687
725
  self.structural_elements[relative_root] = None
688
726
 
727
+ # Report phase completion
728
+ self._report_progress(
729
+ "structure",
730
+ "Identifying packages and folders",
731
+ dir_count,
732
+ phase_complete=True,
733
+ )
734
+
689
735
  def _process_files(self) -> None:
690
736
  """Second pass: Process files and extract definitions."""
737
+ # First pass: Count total files
738
+ total_files = 0
739
+ for root_str, _, files in os.walk(self.repo_path):
740
+ root = Path(root_str)
741
+
742
+ # Skip ignored directories
743
+ if any(part in self.ignore_dirs for part in root.parts):
744
+ continue
745
+
746
+ for filename in files:
747
+ filepath = root / filename
748
+ ext = filepath.suffix
749
+ lang_config = get_language_config(ext)
750
+
751
+ if lang_config and lang_config.name in self.parsers:
752
+ total_files += 1
753
+
754
+ # Second pass: Process files with progress reporting
691
755
  file_count = 0
692
756
  for root_str, _, files in os.walk(self.repo_path):
693
757
  root = Path(root_str)
@@ -707,10 +771,27 @@ class SimpleGraphBuilder:
707
771
  self._process_single_file(filepath, lang_config.name)
708
772
  file_count += 1
709
773
 
774
+ # Report progress after each file
775
+ self._report_progress(
776
+ "definitions",
777
+ "Processing files and extracting definitions",
778
+ file_count,
779
+ total_files,
780
+ )
781
+
710
782
  if file_count % 100 == 0:
711
- logger.info(f" Processed {file_count} files...")
783
+ logger.info(f" Processed {file_count}/{total_files} files...")
784
+
785
+ logger.info(f" Total files processed: {file_count}/{total_files}")
712
786
 
713
- logger.info(f" Total files processed: {file_count}")
787
+ # Report phase completion
788
+ self._report_progress(
789
+ "definitions",
790
+ "Processing files and extracting definitions",
791
+ file_count,
792
+ total_files,
793
+ phase_complete=True,
794
+ )
714
795
 
715
796
  def _process_single_file(self, filepath: Path, language: str) -> None:
716
797
  """Process a single file."""
@@ -1143,7 +1224,8 @@ class SimpleGraphBuilder:
1143
1224
  self._process_inheritance()
1144
1225
 
1145
1226
  # Then process function calls
1146
- logger.info(f"Processing function calls for {len(self.ast_cache)} files...")
1227
+ total_files = len(self.ast_cache)
1228
+ logger.info(f"Processing function calls for {total_files} files...")
1147
1229
  logger.info(f"Function registry has {len(self.function_registry)} entries")
1148
1230
  logger.info(
1149
1231
  f"Simple name lookup has {len(self.simple_name_lookup)} unique names"
@@ -1157,10 +1239,29 @@ class SimpleGraphBuilder:
1157
1239
  f" Example: '{name}' -> {list(self.simple_name_lookup[name])[:3]}"
1158
1240
  )
1159
1241
 
1242
+ file_count = 0
1160
1243
  for filepath, (root_node, language) in self.ast_cache.items():
1161
1244
  self._process_calls(filepath, root_node, language)
1162
1245
  # NOTE: Add import processing. wtf does this mean?
1163
1246
 
1247
+ file_count += 1
1248
+ # Report progress after each file
1249
+ self._report_progress(
1250
+ "relationships",
1251
+ "Processing relationships (calls, imports)",
1252
+ file_count,
1253
+ total_files,
1254
+ )
1255
+
1256
+ # Report phase completion
1257
+ self._report_progress(
1258
+ "relationships",
1259
+ "Processing relationships (calls, imports)",
1260
+ file_count,
1261
+ total_files,
1262
+ phase_complete=True,
1263
+ )
1264
+
1164
1265
  def _process_inheritance(self) -> None:
1165
1266
  """Process inheritance relationships between classes."""
1166
1267
  logger.info("Processing inheritance relationships...")
@@ -1444,6 +1545,7 @@ class CodebaseIngestor:
1444
1545
  db_path: str,
1445
1546
  project_name: str | None = None,
1446
1547
  exclude_patterns: list[str] | None = None,
1548
+ progress_callback: Any | None = None,
1447
1549
  ):
1448
1550
  """Initialize the ingestor.
1449
1551
 
@@ -1451,10 +1553,12 @@ class CodebaseIngestor:
1451
1553
  db_path: Path to Kuzu database
1452
1554
  project_name: Optional project name
1453
1555
  exclude_patterns: Patterns to exclude from processing
1556
+ progress_callback: Optional callback for progress reporting
1454
1557
  """
1455
1558
  self.db_path = Path(db_path)
1456
1559
  self.project_name = project_name
1457
1560
  self.exclude_patterns = exclude_patterns or []
1561
+ self.progress_callback = progress_callback
1458
1562
 
1459
1563
  def build_graph_from_directory(self, repo_path: str) -> None:
1460
1564
  """Build a code knowledge graph from a directory.
@@ -1484,7 +1588,12 @@ class CodebaseIngestor:
1484
1588
 
1485
1589
  # Build graph
1486
1590
  builder = SimpleGraphBuilder(
1487
- ingestor, repo_path_obj, parsers, queries, self.exclude_patterns
1591
+ ingestor,
1592
+ repo_path_obj,
1593
+ parsers,
1594
+ queries,
1595
+ self.exclude_patterns,
1596
+ self.progress_callback,
1488
1597
  )
1489
1598
  if self.project_name:
1490
1599
  builder.project_name = self.project_name
@@ -329,6 +329,7 @@ class CodebaseGraphManager:
329
329
  languages: list[str] | None = None,
330
330
  exclude_patterns: list[str] | None = None,
331
331
  indexed_from_cwd: str | None = None,
332
+ progress_callback: Any | None = None,
332
333
  ) -> CodebaseGraph:
333
334
  """Build a new code knowledge graph.
334
335
 
@@ -337,6 +338,7 @@ class CodebaseGraphManager:
337
338
  name: Optional human-readable name
338
339
  languages: Languages to parse (default: all supported)
339
340
  exclude_patterns: Patterns to exclude
341
+ progress_callback: Optional callback for progress reporting
340
342
 
341
343
  Returns:
342
344
  Created graph metadata
@@ -391,6 +393,7 @@ class CodebaseGraphManager:
391
393
  db_path=str(graph_path),
392
394
  project_name=name,
393
395
  exclude_patterns=exclude_patterns or [],
396
+ progress_callback=progress_callback,
394
397
  )
395
398
 
396
399
  # Run build in thread pool
@@ -1205,6 +1208,136 @@ class CodebaseGraphManager:
1205
1208
  )
1206
1209
  return None
1207
1210
 
1211
+ async def cleanup_corrupted_databases(self) -> list[str]:
1212
+ """Detect and remove corrupted Kuzu databases.
1213
+
1214
+ This method iterates through all .kuzu files in the storage directory,
1215
+ attempts to open them, and removes any that are corrupted or unreadable.
1216
+
1217
+ Returns:
1218
+ List of graph_ids that were removed due to corruption
1219
+ """
1220
+ import shutil
1221
+
1222
+ removed_graphs = []
1223
+
1224
+ # Find all .kuzu files (can be files or directories)
1225
+ for path in self.storage_dir.glob("*.kuzu"):
1226
+ graph_id = path.stem
1227
+
1228
+ # If it's a plain file (not a directory), it's corrupted
1229
+ # Valid Kuzu databases are always directories
1230
+ if path.is_file():
1231
+ logger.warning(
1232
+ f"Detected corrupted database file (should be directory) at {path}, removing it"
1233
+ )
1234
+ try:
1235
+ await anyio.to_thread.run_sync(path.unlink)
1236
+ removed_graphs.append(graph_id)
1237
+ logger.info(f"Removed corrupted database file: {graph_id}")
1238
+ except Exception as e:
1239
+ logger.error(
1240
+ f"Failed to remove corrupted database file {graph_id}: {e}"
1241
+ )
1242
+ continue
1243
+
1244
+ try:
1245
+ # Try to open the database with a timeout to prevent hanging
1246
+ async def try_open_database(
1247
+ gid: str = graph_id, db_path: Path = path
1248
+ ) -> bool:
1249
+ lock = await self._get_lock()
1250
+ async with lock:
1251
+ # Close existing connections if any
1252
+ if gid in self._connections:
1253
+ try:
1254
+ self._connections[gid].close()
1255
+ except Exception as e:
1256
+ logger.debug(
1257
+ f"Failed to close connection for {gid}: {e}"
1258
+ )
1259
+ del self._connections[gid]
1260
+ if gid in self._databases:
1261
+ try:
1262
+ self._databases[gid].close()
1263
+ except Exception as e:
1264
+ logger.debug(f"Failed to close database for {gid}: {e}")
1265
+ del self._databases[gid]
1266
+
1267
+ # Try to open the database
1268
+ def _open_and_query(g: str = gid, p: Path = db_path) -> bool:
1269
+ db = kuzu.Database(str(p))
1270
+ conn = kuzu.Connection(db)
1271
+ try:
1272
+ result = conn.execute(
1273
+ "MATCH (p:Project {graph_id: $graph_id}) RETURN p",
1274
+ {"graph_id": g},
1275
+ )
1276
+ has_results = (
1277
+ result.has_next()
1278
+ if hasattr(result, "has_next")
1279
+ else False
1280
+ )
1281
+ return has_results
1282
+ finally:
1283
+ conn.close()
1284
+ db.close()
1285
+
1286
+ return await anyio.to_thread.run_sync(_open_and_query)
1287
+
1288
+ # Try to open with 5 second timeout
1289
+ has_project = await asyncio.wait_for(try_open_database(), timeout=5.0)
1290
+
1291
+ if not has_project:
1292
+ # Database exists but has no Project node - consider it corrupted
1293
+ raise ValueError("No Project node found in database")
1294
+
1295
+ except (Exception, asyncio.TimeoutError) as e:
1296
+ # Database is corrupted or timed out - remove it
1297
+ error_type = (
1298
+ "timed out" if isinstance(e, asyncio.TimeoutError) else "corrupted"
1299
+ )
1300
+ logger.warning(
1301
+ f"Detected {error_type} database at {path}, removing it. "
1302
+ f"Error: {str(e) if not isinstance(e, asyncio.TimeoutError) else 'Operation timed out after 5 seconds'}"
1303
+ )
1304
+
1305
+ try:
1306
+ # Clean up any open connections
1307
+ lock = await self._get_lock()
1308
+ async with lock:
1309
+ if graph_id in self._connections:
1310
+ try:
1311
+ self._connections[graph_id].close()
1312
+ except Exception as e:
1313
+ logger.debug(
1314
+ f"Failed to close connection during cleanup for {graph_id}: {e}"
1315
+ )
1316
+ del self._connections[graph_id]
1317
+ if graph_id in self._databases:
1318
+ try:
1319
+ self._databases[graph_id].close()
1320
+ except Exception as e:
1321
+ logger.debug(
1322
+ f"Failed to close database during cleanup for {graph_id}: {e}"
1323
+ )
1324
+ del self._databases[graph_id]
1325
+
1326
+ # Remove the database (could be file or directory)
1327
+ if path.is_dir():
1328
+ await anyio.to_thread.run_sync(shutil.rmtree, path)
1329
+ else:
1330
+ await anyio.to_thread.run_sync(path.unlink)
1331
+ removed_graphs.append(graph_id)
1332
+ logger.info(f"Removed {error_type} database: {graph_id}")
1333
+
1334
+ except Exception as cleanup_error:
1335
+ logger.error(
1336
+ f"Failed to remove corrupted database {graph_id}: {cleanup_error}"
1337
+ )
1338
+
1339
+ return removed_graphs
1340
+
1208
1341
  async def list_graphs(self) -> list[CodebaseGraph]:
1209
1342
  """List all available graphs.
1210
1343
 
@@ -1476,6 +1609,7 @@ class CodebaseGraphManager:
1476
1609
  languages: list[str] | None,
1477
1610
  exclude_patterns: list[str] | None,
1478
1611
  indexed_from_cwd: str | None = None,
1612
+ progress_callback: Any | None = None,
1479
1613
  ) -> CodebaseGraph:
1480
1614
  """Internal implementation of graph building (runs in background)."""
1481
1615
  operation_id = str(uuid.uuid4())
@@ -1499,7 +1633,13 @@ class CodebaseGraphManager:
1499
1633
 
1500
1634
  # Do the actual build work
1501
1635
  graph = await self._do_build_graph(
1502
- graph_id, repo_path, name, languages, exclude_patterns, indexed_from_cwd
1636
+ graph_id,
1637
+ repo_path,
1638
+ name,
1639
+ languages,
1640
+ exclude_patterns,
1641
+ indexed_from_cwd,
1642
+ progress_callback,
1503
1643
  )
1504
1644
 
1505
1645
  # Update operation stats
@@ -1548,6 +1688,7 @@ class CodebaseGraphManager:
1548
1688
  languages: list[str] | None,
1549
1689
  exclude_patterns: list[str] | None,
1550
1690
  indexed_from_cwd: str | None = None,
1691
+ progress_callback: Any | None = None,
1551
1692
  ) -> CodebaseGraph:
1552
1693
  """Execute the actual graph building logic (extracted from original build_graph)."""
1553
1694
  # The database and Project node already exist from _initialize_graph_metadata
@@ -1603,6 +1744,7 @@ class CodebaseGraphManager:
1603
1744
  parsers=parsers,
1604
1745
  queries=queries,
1605
1746
  exclude_patterns=exclude_patterns,
1747
+ progress_callback=progress_callback,
1606
1748
  )
1607
1749
 
1608
1750
  # Build the graph
@@ -1628,6 +1770,7 @@ class CodebaseGraphManager:
1628
1770
  languages: list[str] | None = None,
1629
1771
  exclude_patterns: list[str] | None = None,
1630
1772
  indexed_from_cwd: str | None = None,
1773
+ progress_callback: Any | None = None,
1631
1774
  ) -> str:
1632
1775
  """Start building a new code knowledge graph asynchronously.
1633
1776
 
@@ -1666,7 +1809,13 @@ class CodebaseGraphManager:
1666
1809
  # Start the build operation in background
1667
1810
  task = asyncio.create_task(
1668
1811
  self._build_graph_impl(
1669
- graph_id, repo_path, name, languages, exclude_patterns, indexed_from_cwd
1812
+ graph_id,
1813
+ repo_path,
1814
+ name,
1815
+ languages,
1816
+ exclude_patterns,
1817
+ indexed_from_cwd,
1818
+ progress_callback,
1670
1819
  )
1671
1820
  )
1672
1821
  self._operations[graph_id] = task
@@ -1,5 +1,6 @@
1
1
  """Data models for codebase service."""
2
2
 
3
+ from collections.abc import Callable
3
4
  from enum import Enum
4
5
  from typing import Any
5
6
 
@@ -22,6 +23,30 @@ class QueryType(str, Enum):
22
23
  CYPHER = "cypher"
23
24
 
24
25
 
26
+ class ProgressPhase(str, Enum):
27
+ """Phase of codebase indexing progress."""
28
+
29
+ STRUCTURE = "structure" # Identifying packages and folders
30
+ DEFINITIONS = "definitions" # Processing files and extracting definitions
31
+ RELATIONSHIPS = "relationships" # Processing relationships (calls, imports)
32
+
33
+
34
+ class IndexProgress(BaseModel):
35
+ """Progress information for codebase indexing."""
36
+
37
+ phase: ProgressPhase = Field(..., description="Current indexing phase")
38
+ phase_name: str = Field(..., description="Human-readable phase name")
39
+ current: int = Field(..., description="Current item count")
40
+ total: int | None = Field(None, description="Total items (None if unknown)")
41
+ phase_complete: bool = Field(
42
+ default=False, description="Whether this phase is complete"
43
+ )
44
+
45
+
46
+ # Type alias for progress callback function
47
+ ProgressCallback = Callable[[IndexProgress], None]
48
+
49
+
25
50
  class OperationStats(BaseModel):
26
51
  """Statistics for a graph operation (build/update)."""
27
52
 
@@ -77,7 +77,11 @@ class CodebaseService:
77
77
  return filtered_graphs
78
78
 
79
79
  async def create_graph(
80
- self, repo_path: str | Path, name: str, indexed_from_cwd: str | None = None
80
+ self,
81
+ repo_path: str | Path,
82
+ name: str,
83
+ indexed_from_cwd: str | None = None,
84
+ progress_callback: Any | None = None,
81
85
  ) -> CodebaseGraph:
82
86
  """Create and index a new graph from a repository.
83
87
 
@@ -85,12 +89,16 @@ class CodebaseService:
85
89
  repo_path: Path to the repository to index
86
90
  name: Human-readable name for the graph
87
91
  indexed_from_cwd: Working directory from which indexing was initiated
92
+ progress_callback: Optional callback for progress reporting
88
93
 
89
94
  Returns:
90
95
  The created CodebaseGraph
91
96
  """
92
97
  return await self.manager.build_graph(
93
- str(repo_path), name, indexed_from_cwd=indexed_from_cwd
98
+ str(repo_path),
99
+ name,
100
+ indexed_from_cwd=indexed_from_cwd,
101
+ progress_callback=progress_callback,
94
102
  )
95
103
 
96
104
  async def get_graph(self, graph_id: str) -> CodebaseGraph | None: