gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/cli/utils.py ADDED
@@ -0,0 +1,513 @@
1
+ """
2
+ Shared utilities for CLI commands.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import signal
8
+ import time
9
+ from pathlib import Path
10
+
11
+ import click
12
+ import psutil
13
+
14
+ from gobby.config.app import load_config
15
+ from gobby.storage.database import LocalDatabase
16
+ from gobby.storage.projects import LocalProjectManager
17
+ from gobby.storage.sessions import LocalSessionManager
18
+ from gobby.utils.project_context import get_project_context
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def get_gobby_home() -> Path:
24
+ """Get gobby home directory, respecting GOBBY_HOME env var.
25
+
26
+ Returns:
27
+ Path to gobby home (~/.gobby by default, or GOBBY_HOME if set)
28
+ """
29
+ gobby_home = os.environ.get("GOBBY_HOME")
30
+ if gobby_home:
31
+ return Path(gobby_home)
32
+ return Path.home() / ".gobby"
33
+
34
+
35
+ def get_resources_dir(project_path: str | None = None) -> Path:
36
+ """Get the resources directory for storing media files.
37
+
38
+ If a project path is provided, returns the project-local resources directory
39
+ (.gobby/resources/ within the project). Otherwise, returns the global
40
+ resources directory (~/.gobby/resources/).
41
+
42
+ The directory is created if it doesn't exist.
43
+
44
+ Args:
45
+ project_path: Optional project root path for project-local resources
46
+
47
+ Returns:
48
+ Path to the resources directory
49
+ """
50
+ if project_path:
51
+ resources_dir = Path(project_path) / ".gobby" / "resources"
52
+ else:
53
+ resources_dir = get_gobby_home() / "resources"
54
+
55
+ # Ensure directory exists
56
+ resources_dir.mkdir(parents=True, exist_ok=True)
57
+ return resources_dir
58
+
59
+
60
+ def resolve_project_ref(project_ref: str | None, exit_on_not_found: bool = True) -> str | None:
61
+ """Resolve a project reference (name or UUID) to project ID.
62
+
63
+ Accepts:
64
+ - Project name (e.g., "gobby")
65
+ - Project UUID
66
+ - None (returns current project from context)
67
+
68
+ Args:
69
+ project_ref: Project name, UUID, or None
70
+ exit_on_not_found: If True (default), exit the CLI when an explicit
71
+ project_ref is provided but not found
72
+
73
+ Returns:
74
+ Project ID string, or None if not found/no context
75
+ """
76
+ if not project_ref:
77
+ # Use current project context
78
+ ctx = get_project_context()
79
+ return ctx.get("id") if ctx else None
80
+
81
+ db = LocalDatabase()
82
+ try:
83
+ manager = LocalProjectManager(db)
84
+
85
+ # Try as direct UUID first
86
+ project = manager.get(project_ref)
87
+ if project:
88
+ return project.id
89
+
90
+ # Try as project name
91
+ project = manager.get_by_name(project_ref)
92
+ if project:
93
+ return project.id
94
+ finally:
95
+ db.close()
96
+
97
+ return None
98
+
99
+
100
+ def get_active_session_id(db: LocalDatabase | None = None) -> str | None:
101
+ """Get the most recent active session ID."""
102
+ close_db = False
103
+ if db is None:
104
+ db = LocalDatabase()
105
+ close_db = True
106
+
107
+ try:
108
+ # SELECT id FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1
109
+ # Using format compatible with the rest of the codebase (raw SQL) to avoid circular imports
110
+ # if using session manager directly which might pull in other things.
111
+ # But we import LocalSessionManager at top, so let's use it if possible or raw SQL for speed.
112
+ row = db.fetchone(
113
+ "SELECT id FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1"
114
+ )
115
+ return row["id"] if row else None
116
+ finally:
117
+ if close_db:
118
+ db.close()
119
+
120
+
121
+ def resolve_session_id(session_ref: str | None) -> str:
122
+ """
123
+ Resolve session reference to UUID.
124
+
125
+ Centralized logic used by all CLI commands.
126
+
127
+ Args:
128
+ session_ref: User input string (UUID, #N, N, prefix) or None
129
+
130
+ Returns:
131
+ Resolved UUID string
132
+
133
+ Raises:
134
+ click.ClickException: If session not found or ambiguous
135
+ """
136
+ db = LocalDatabase()
137
+ try:
138
+ # If no reference provided, try to find active session
139
+ if not session_ref:
140
+ active_id = get_active_session_id(db)
141
+ if not active_id:
142
+ raise click.ClickException("No active session found. Specify --session.")
143
+ return active_id
144
+
145
+ # Use SessionManager for resolution logic
146
+ manager = LocalSessionManager(db)
147
+ try:
148
+ return manager.resolve_session_reference(session_ref)
149
+ except ValueError as e:
150
+ raise click.ClickException(str(e)) from None
151
+ finally:
152
+ db.close()
153
+
154
+
155
+ def list_project_names() -> list[str]:
156
+ """List all project names for shell completion."""
157
+ db = LocalDatabase()
158
+ try:
159
+ manager = LocalProjectManager(db)
160
+ return [p.name for p in manager.list()]
161
+ finally:
162
+ db.close()
163
+
164
+
165
+ def setup_logging(verbose: bool = False) -> None:
166
+ """
167
+ Configure logging for CLI.
168
+
169
+ Args:
170
+ verbose: If True, enable DEBUG level logging
171
+ """
172
+ log_level = logging.DEBUG if verbose else logging.INFO
173
+ logging.basicConfig(
174
+ level=log_level,
175
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
176
+ datefmt="%Y-%m-%d %H:%M:%S",
177
+ )
178
+
179
+ # Silence noisy third-party loggers
180
+ logging.getLogger("httpx").setLevel(logging.WARNING)
181
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
182
+
183
+
184
+ def format_uptime(seconds: float) -> str:
185
+ """
186
+ Format uptime in human-readable format.
187
+
188
+ Args:
189
+ seconds: Uptime in seconds
190
+
191
+ Returns:
192
+ Formatted string like "1h 23m 45s"
193
+ """
194
+ hours = int(seconds // 3600)
195
+ minutes = int((seconds % 3600) // 60)
196
+ secs = int(seconds % 60)
197
+
198
+ parts = []
199
+ if hours > 0:
200
+ parts.append(f"{hours}h")
201
+ if minutes > 0:
202
+ parts.append(f"{minutes}m")
203
+ if secs > 0 or not parts:
204
+ parts.append(f"{secs}s")
205
+
206
+ return " ".join(parts)
207
+
208
+
209
+ def is_port_available(port: int, host: str = "localhost") -> bool:
210
+ """
211
+ Check if a port is available for binding.
212
+
213
+ Args:
214
+ port: Port number to check
215
+ host: Host address to bind to
216
+
217
+ Returns:
218
+ True if port is available, False otherwise
219
+ """
220
+ import socket
221
+
222
+ # Try to bind to the port
223
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
224
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
225
+
226
+ try:
227
+ sock.bind((host, port))
228
+ sock.close()
229
+ return True
230
+ except OSError:
231
+ sock.close()
232
+ return False
233
+
234
+
235
+ def wait_for_port_available(port: int, host: str = "localhost", timeout: float = 5.0) -> bool:
236
+ """
237
+ Wait for a port to become available.
238
+
239
+ Args:
240
+ port: Port number to check
241
+ host: Host address to bind to
242
+ timeout: Maximum time to wait in seconds
243
+
244
+ Returns:
245
+ True if port became available, False if timeout
246
+ """
247
+ start_time = time.time()
248
+
249
+ while (time.time() - start_time) < timeout:
250
+ if is_port_available(port, host):
251
+ return True
252
+ time.sleep(0.1)
253
+
254
+ return False
255
+
256
+
257
+ def kill_all_gobby_daemons() -> int:
258
+ """
259
+ Find and kill all gobby DAEMON processes (not CLI commands).
260
+
261
+ Only kills processes that are actually running daemon servers,
262
+ not CLI invocations or other tools.
263
+
264
+ Detection methods:
265
+ 1. Matches gobby.runner (the main daemon process)
266
+ 2. Matches processes listening on daemon ports (8765/8766)
267
+
268
+ Returns:
269
+ Number of processes killed
270
+ """
271
+ # Load config to get the configured ports
272
+ try:
273
+ config = load_config(create_default=False)
274
+ http_port = config.daemon_port
275
+ ws_port = config.websocket.port
276
+ except Exception:
277
+ # Fallback to defaults if config can't be loaded
278
+ http_port = 8765
279
+ ws_port = 8766
280
+
281
+ killed_count = 0
282
+ current_pid = os.getpid()
283
+ parent_pid = os.getppid()
284
+
285
+ # Get our parent process tree to avoid killing it
286
+ parent_pids = {current_pid, parent_pid}
287
+ try:
288
+ parent_proc = psutil.Process(parent_pid)
289
+ while parent_proc.parent() is not None:
290
+ parent_proc = parent_proc.parent()
291
+ parent_pids.add(parent_proc.pid)
292
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
293
+ pass
294
+
295
+ # Find all gobby daemon processes
296
+ for proc in psutil.process_iter(["pid", "name", "cmdline"]):
297
+ try:
298
+ # Skip our own process and parent tree
299
+ if proc.pid in parent_pids:
300
+ continue
301
+
302
+ # Check if this is a gobby daemon process
303
+ cmdline = proc.cmdline()
304
+ cmdline_str = " ".join(cmdline)
305
+
306
+ # Match gobby.runner which is the actual daemon process
307
+ # Started via: python -m gobby.runner
308
+ is_gobby_daemon = (
309
+ "python" in cmdline_str.lower()
310
+ and (
311
+ # Match gobby.runner (new package)
312
+ "gobby.runner" in cmdline_str
313
+ # Also match legacy gobby_client.runner if it exists
314
+ or "gobby_client.runner" in cmdline_str
315
+ )
316
+ # Exclude CLI invocations
317
+ and "gobby.cli" not in cmdline_str
318
+ and "gobby_client.cli" not in cmdline_str
319
+ )
320
+
321
+ # Also check for processes that might be old daemon instances
322
+ # by checking if they're listening on our ports
323
+ if not is_gobby_daemon:
324
+ try:
325
+ # Check if process has connections on daemon ports
326
+ connections = proc.connections()
327
+ for conn in connections:
328
+ if hasattr(conn, "laddr") and conn.laddr:
329
+ if conn.laddr.port in [http_port, ws_port]:
330
+ # Only consider it a daemon if it's a Python process
331
+ # to avoid killing unrelated services
332
+ if "python" in cmdline_str.lower():
333
+ is_gobby_daemon = True
334
+ break
335
+ except (psutil.AccessDenied, psutil.NoSuchProcess):
336
+ pass
337
+
338
+ if is_gobby_daemon:
339
+ click.echo(f"Found gobby daemon (PID {proc.pid}): {cmdline_str[:100]}")
340
+
341
+ # Try graceful shutdown first (SIGTERM)
342
+ try:
343
+ proc.send_signal(signal.SIGTERM)
344
+ # Wait up to 5 seconds for graceful shutdown
345
+ proc.wait(timeout=5)
346
+ click.echo(f"Gracefully stopped PID {proc.pid}")
347
+ killed_count += 1
348
+ except psutil.TimeoutExpired:
349
+ # Force kill if graceful shutdown fails
350
+ click.echo(f"Process {proc.pid} didn't stop gracefully, force killing...")
351
+ proc.kill()
352
+ proc.wait(timeout=2)
353
+ click.echo(f"Force killed PID {proc.pid}")
354
+ killed_count += 1
355
+
356
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
357
+ # Process already gone or we can't access it
358
+ pass
359
+ except Exception as e:
360
+ click.echo(f"Warning: Error checking process {proc.pid}: {e}", err=True)
361
+
362
+ return killed_count
363
+
364
+
365
+ def init_local_storage() -> None:
366
+ """Initialize hub SQLite storage and run migrations."""
367
+ from gobby.storage.database import LocalDatabase
368
+ from gobby.storage.migrations import run_migrations
369
+
370
+ config = load_config(create_default=False)
371
+ hub_db_path = Path(config.database_path).expanduser()
372
+
373
+ # Ensure hub db directory exists
374
+ hub_db_path.parent.mkdir(parents=True, exist_ok=True)
375
+
376
+ hub_db = LocalDatabase(hub_db_path)
377
+ run_migrations(hub_db)
378
+ logger.debug(f"Database: {hub_db_path}")
379
+
380
+
381
+ def get_install_dir() -> Path:
382
+ """Get the gobby install directory.
383
+
384
+ Checks for source directory (development mode) first,
385
+ falls back to package directory.
386
+
387
+ Returns:
388
+ Path to the install directory
389
+ """
390
+ import gobby
391
+
392
+ package_install_dir = Path(gobby.__file__).parent / "install"
393
+
394
+ # Try to find source directory (project root)
395
+ current = Path(gobby.__file__).resolve()
396
+ source_install_dir = None
397
+
398
+ for parent in current.parents:
399
+ potential_source = parent / "src" / "gobby" / "install"
400
+ if potential_source.exists():
401
+ source_install_dir = potential_source
402
+ break
403
+
404
+ if source_install_dir and source_install_dir.exists():
405
+ return source_install_dir
406
+ return package_install_dir
407
+
408
+
409
+ def _is_process_alive(pid: int) -> bool:
410
+ """Check if a process is truly alive (not zombie, not dead).
411
+
412
+ Uses psutil to check process status, which handles zombies correctly.
413
+ os.kill(pid, 0) succeeds on zombie processes, but they're effectively dead.
414
+
415
+ Args:
416
+ pid: Process ID to check
417
+
418
+ Returns:
419
+ True only if process exists and is not a zombie
420
+ """
421
+ try:
422
+ proc = psutil.Process(pid)
423
+ return bool(proc.status() != psutil.STATUS_ZOMBIE)
424
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
425
+ return False
426
+
427
+
428
+ def stop_daemon(quiet: bool = False) -> bool:
429
+ """Stop the daemon process. Returns True on success, False on failure.
430
+
431
+ Args:
432
+ quiet: If True, suppress output messages
433
+
434
+ Returns:
435
+ True if daemon was stopped successfully or wasn't running, False on error
436
+ """
437
+ pid_file = get_gobby_home() / "gobby.pid"
438
+
439
+ # Read PID from file
440
+ if not pid_file.exists():
441
+ if not quiet:
442
+ click.echo("Gobby daemon is not running (no PID file found)")
443
+ return True
444
+
445
+ try:
446
+ with open(pid_file) as f:
447
+ pid = int(f.read().strip())
448
+ except Exception as e:
449
+ if not quiet:
450
+ click.echo(f"Error reading PID file: {e}", err=True)
451
+ pid_file.unlink(missing_ok=True)
452
+ return False
453
+
454
+ # Check if process is actually running (handles zombies correctly)
455
+ if not _is_process_alive(pid):
456
+ if not quiet:
457
+ click.echo(f"Gobby daemon is not running (stale PID file with PID {pid})")
458
+ pid_file.unlink(missing_ok=True)
459
+ return True
460
+
461
+ try:
462
+ # Send SIGTERM signal for graceful shutdown
463
+ os.kill(pid, signal.SIGTERM)
464
+ if not quiet:
465
+ click.echo(f"Sent shutdown signal to Gobby daemon (PID {pid})")
466
+
467
+ # Wait for graceful shutdown
468
+ max_wait = 5
469
+ for _ in range(max_wait * 10):
470
+ time.sleep(0.1)
471
+ if not _is_process_alive(pid):
472
+ if not quiet:
473
+ click.echo("Gobby daemon stopped successfully")
474
+ pid_file.unlink(missing_ok=True)
475
+ return True
476
+
477
+ # Process didn't stop gracefully - try force kill
478
+ if not quiet:
479
+ click.echo(f"Process didn't stop gracefully after {max_wait}s, force killing...")
480
+
481
+ try:
482
+ os.kill(pid, signal.SIGKILL)
483
+ time.sleep(0.5)
484
+ except ProcessLookupError:
485
+ pass # Already dead
486
+
487
+ # Final check
488
+ if not _is_process_alive(pid):
489
+ if not quiet:
490
+ click.echo("Gobby daemon force killed successfully")
491
+ pid_file.unlink(missing_ok=True)
492
+ return True
493
+
494
+ if not quiet:
495
+ click.echo("Warning: Failed to stop process", err=True)
496
+ return False
497
+
498
+ except PermissionError:
499
+ if not quiet:
500
+ click.echo(f"Error: Permission denied to stop process (PID {pid})", err=True)
501
+ return False
502
+
503
+ except ProcessLookupError:
504
+ # Process died between our check and sending signal - that's fine
505
+ if not quiet:
506
+ click.echo("Gobby daemon stopped")
507
+ pid_file.unlink(missing_ok=True)
508
+ return True
509
+
510
+ except Exception as e:
511
+ if not quiet:
512
+ click.echo(f"Error stopping daemon: {e}", err=True)
513
+ return False