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/artifacts.py ADDED
@@ -0,0 +1,266 @@
1
+ """CLI commands for session artifacts."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import click
7
+
8
+ from gobby.storage.artifacts import Artifact, LocalArtifactManager
9
+ from gobby.storage.database import LocalDatabase
10
+
11
+
12
+ def get_artifact_manager() -> LocalArtifactManager:
13
+ """Get the artifact manager."""
14
+ db = LocalDatabase()
15
+ return LocalArtifactManager(db)
16
+
17
+
18
+ @click.group()
19
+ def artifacts() -> None:
20
+ """Manage session artifacts (code, diffs, errors)."""
21
+ pass
22
+
23
+
24
+ @artifacts.command()
25
+ @click.argument("query")
26
+ @click.option("--session", "-s", "session_id", help="Filter by session ID")
27
+ @click.option("--type", "-t", "artifact_type", help="Filter by artifact type (code, diff, error)")
28
+ @click.option("--limit", "-n", default=50, help="Maximum results")
29
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
30
+ def search(
31
+ query: str,
32
+ session_id: str | None,
33
+ artifact_type: str | None,
34
+ limit: int,
35
+ output_json: bool,
36
+ ) -> None:
37
+ """Search artifacts by content.
38
+
39
+ Uses full-text search to find matching artifacts.
40
+ """
41
+ manager = get_artifact_manager()
42
+ artifacts_list = manager.search_artifacts(
43
+ query_text=query,
44
+ session_id=session_id,
45
+ artifact_type=artifact_type,
46
+ limit=limit,
47
+ )
48
+
49
+ if not artifacts_list:
50
+ if output_json:
51
+ click.echo(json.dumps({"artifacts": [], "count": 0}))
52
+ else:
53
+ click.echo("No artifacts found")
54
+ return
55
+
56
+ if output_json:
57
+ click.echo(
58
+ json.dumps(
59
+ {
60
+ "artifacts": [a.to_dict() for a in artifacts_list],
61
+ "count": len(artifacts_list),
62
+ },
63
+ indent=2,
64
+ )
65
+ )
66
+ else:
67
+ _display_artifact_list(artifacts_list)
68
+
69
+
70
+ @artifacts.command("list")
71
+ @click.option("--session", "-s", "session_id", help="Filter by session ID")
72
+ @click.option("--type", "-t", "artifact_type", help="Filter by artifact type (code, diff, error)")
73
+ @click.option("--limit", "-n", default=100, help="Maximum results")
74
+ @click.option("--offset", "-o", default=0, help="Offset for pagination")
75
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
76
+ def list_artifacts(
77
+ session_id: str | None,
78
+ artifact_type: str | None,
79
+ limit: int,
80
+ offset: int,
81
+ output_json: bool,
82
+ ) -> None:
83
+ """List artifacts with optional filters."""
84
+ manager = get_artifact_manager()
85
+ artifacts_list = manager.list_artifacts(
86
+ session_id=session_id,
87
+ artifact_type=artifact_type,
88
+ limit=limit,
89
+ offset=offset,
90
+ )
91
+
92
+ if output_json:
93
+ click.echo(
94
+ json.dumps(
95
+ {
96
+ "artifacts": [a.to_dict() for a in artifacts_list],
97
+ "count": len(artifacts_list),
98
+ },
99
+ indent=2,
100
+ )
101
+ )
102
+ else:
103
+ if not artifacts_list:
104
+ click.echo("No artifacts found")
105
+ return
106
+ _display_artifact_list(artifacts_list)
107
+
108
+
109
+ @artifacts.command()
110
+ @click.argument("artifact_id")
111
+ @click.option("--verbose", "-v", is_flag=True, help="Show full metadata")
112
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
113
+ def show(artifact_id: str, verbose: bool, output_json: bool) -> None:
114
+ """Display a single artifact by ID."""
115
+ manager = get_artifact_manager()
116
+ artifact = manager.get_artifact(artifact_id)
117
+
118
+ if artifact is None:
119
+ if output_json:
120
+ click.echo(json.dumps({"error": f"Artifact '{artifact_id}' not found"}))
121
+ else:
122
+ click.echo(f"Artifact not found: {artifact_id}", err=True)
123
+ raise SystemExit(1)
124
+
125
+ if output_json:
126
+ click.echo(json.dumps(artifact.to_dict(), indent=2))
127
+ else:
128
+ _display_artifact_detail(artifact, verbose)
129
+
130
+
131
+ @artifacts.command()
132
+ @click.argument("session_id")
133
+ @click.option("--type", "-t", "artifact_type", help="Filter by artifact type")
134
+ @click.option("--limit", "-n", default=100, help="Maximum results")
135
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
136
+ def timeline(
137
+ session_id: str,
138
+ artifact_type: str | None,
139
+ limit: int,
140
+ output_json: bool,
141
+ ) -> None:
142
+ """Show artifacts for a session in chronological order.
143
+
144
+ Displays artifacts from oldest to newest.
145
+ """
146
+ manager = get_artifact_manager()
147
+ artifacts_list = manager.list_artifacts(
148
+ session_id=session_id,
149
+ artifact_type=artifact_type,
150
+ limit=limit,
151
+ offset=0,
152
+ )
153
+
154
+ # Reverse to get chronological order (oldest first)
155
+ artifacts_list = list(reversed(artifacts_list))
156
+
157
+ if output_json:
158
+ click.echo(
159
+ json.dumps(
160
+ {
161
+ "session_id": session_id,
162
+ "artifacts": [a.to_dict() for a in artifacts_list],
163
+ "count": len(artifacts_list),
164
+ },
165
+ indent=2,
166
+ )
167
+ )
168
+ else:
169
+ if not artifacts_list:
170
+ click.echo(f"No artifacts found for session: {session_id}")
171
+ return
172
+ click.echo(f"Timeline for session: {session_id}")
173
+ click.echo("-" * 60)
174
+ for artifact in artifacts_list:
175
+ _display_timeline_entry(artifact)
176
+
177
+
178
+ def _display_artifact_list(artifacts_list: list[Any]) -> None:
179
+ """Display a list of artifacts in table format."""
180
+ # Header
181
+ click.echo(f"{'ID':<12} {'Type':<8} {'Source':<20} {'Created':<20}")
182
+ click.echo("-" * 60)
183
+
184
+ for artifact in artifacts_list:
185
+ artifact_id = artifact.id[:12] if len(artifact.id) > 12 else artifact.id
186
+ source = artifact.source_file or "-"
187
+ if len(source) > 18:
188
+ source = "..." + source[-15:]
189
+ created = artifact.created_at[:19] if artifact.created_at else "-"
190
+ click.echo(f"{artifact_id:<12} {artifact.artifact_type:<8} {source:<20} {created:<20}")
191
+
192
+
193
+ def _display_artifact_detail(artifact: Artifact, verbose: bool) -> None:
194
+ """Display a single artifact with optional verbosity."""
195
+ click.echo(f"ID: {artifact.id}")
196
+ click.echo(f"Type: {artifact.artifact_type}")
197
+ click.echo(f"Session: {artifact.session_id}")
198
+
199
+ if artifact.source_file:
200
+ location = artifact.source_file
201
+ if artifact.line_start:
202
+ location += f":{artifact.line_start}"
203
+ if artifact.line_end and artifact.line_end != artifact.line_start:
204
+ location += f"-{artifact.line_end}"
205
+ click.echo(f"Source: {location}")
206
+
207
+ click.echo(f"Created: {artifact.created_at}")
208
+
209
+ if verbose and artifact.metadata:
210
+ click.echo(f"Metadata: {json.dumps(artifact.metadata, indent=2)}")
211
+
212
+ click.echo("")
213
+ click.echo("-" * 60)
214
+
215
+ # Display content with syntax highlighting for code
216
+ _display_content(artifact.content, artifact.artifact_type, artifact.metadata)
217
+
218
+
219
+ def _display_content(content: str, artifact_type: str, metadata: dict[str, Any] | None) -> None:
220
+ """Display content with appropriate formatting."""
221
+ # Try to use rich for syntax highlighting if available
222
+ try:
223
+ from rich.console import Console
224
+ from rich.syntax import Syntax
225
+
226
+ console = Console()
227
+
228
+ # Determine language for syntax highlighting
229
+ language = None
230
+ if metadata and "language" in metadata:
231
+ language = metadata["language"]
232
+ elif artifact_type == "code":
233
+ # Default to python if no language specified
234
+ language = "python"
235
+ elif artifact_type == "diff":
236
+ language = "diff"
237
+ elif artifact_type == "error":
238
+ language = "text"
239
+
240
+ if language:
241
+ syntax = Syntax(content, language, theme="monokai", line_numbers=True)
242
+ console.print(syntax)
243
+ else:
244
+ click.echo(content)
245
+
246
+ except ImportError:
247
+ # Fall back to plain text
248
+ click.echo(content)
249
+
250
+
251
+ def _display_timeline_entry(artifact: Artifact) -> None:
252
+ """Display a single timeline entry."""
253
+ click.echo(f"[{artifact.created_at[:19]}] {artifact.artifact_type.upper()}: {artifact.id}")
254
+ if artifact.source_file:
255
+ click.echo(f" Source: {artifact.source_file}")
256
+
257
+ # Show a preview of the content (first 2 lines)
258
+ lines = artifact.content.split("\n")[:2]
259
+ for line in lines:
260
+ if len(line) > 60:
261
+ line = line[:57] + "..."
262
+ click.echo(f" | {line}")
263
+
264
+ if len(artifact.content.split("\n")) > 2:
265
+ click.echo(f" | ... ({len(artifact.content.split(chr(10)))} lines total)")
266
+ click.echo("")
gobby/cli/daemon.py ADDED
@@ -0,0 +1,329 @@
1
+ """
2
+ Daemon management commands.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import subprocess # nosec B404 - subprocess needed for daemon management
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import click
14
+ import httpx
15
+ import psutil
16
+
17
+ from gobby.utils.status import fetch_rich_status, format_status_message
18
+
19
+ from .utils import (
20
+ format_uptime,
21
+ get_gobby_home,
22
+ init_local_storage,
23
+ is_port_available,
24
+ kill_all_gobby_daemons,
25
+ setup_logging,
26
+ wait_for_port_available,
27
+ )
28
+ from .utils import (
29
+ stop_daemon as stop_daemon_util,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ @click.command()
36
+ @click.option(
37
+ "--verbose",
38
+ "-v",
39
+ is_flag=True,
40
+ help="Enable verbose debug output",
41
+ )
42
+ @click.pass_context
43
+ def start(ctx: click.Context, verbose: bool) -> None:
44
+ """Start the Gobby daemon."""
45
+ # Get config object
46
+ config = ctx.obj["config"]
47
+
48
+ # Get paths from config (respects GOBBY_HOME env var)
49
+ gobby_dir = get_gobby_home()
50
+ pid_file = gobby_dir / "gobby.pid"
51
+ log_file = Path(config.logging.client).expanduser()
52
+ error_log_file = Path(config.logging.client_error).expanduser()
53
+
54
+ gobby_dir.mkdir(parents=True, exist_ok=True)
55
+ log_file.parent.mkdir(parents=True, exist_ok=True)
56
+ error_log_file.parent.mkdir(parents=True, exist_ok=True)
57
+
58
+ # Initialize local storage before starting daemon
59
+ click.echo("Initializing local storage...")
60
+ init_local_storage()
61
+
62
+ # Check if already running
63
+ if pid_file.exists():
64
+ try:
65
+ with open(pid_file) as f:
66
+ pid = int(f.read().strip())
67
+
68
+ # Check if process is actually running
69
+ try:
70
+ os.kill(pid, 0)
71
+ click.echo(f"Gobby daemon is already running (PID: {pid})", err=True)
72
+ sys.exit(1)
73
+ except ProcessLookupError:
74
+ # Stale PID file
75
+ click.echo(f"Removing stale PID file (PID: {pid})")
76
+ pid_file.unlink()
77
+ except Exception:
78
+ pid_file.unlink()
79
+
80
+ # Kill any existing gobby processes
81
+ click.echo("Checking for existing gobby processes...")
82
+ killed_count = kill_all_gobby_daemons()
83
+ if killed_count > 0:
84
+ click.echo(f"Stopped {killed_count} existing process(es)")
85
+ time.sleep(2.0) # Wait for ports to be released
86
+ else:
87
+ click.echo("No existing processes found")
88
+
89
+ # Check ports
90
+ http_port = config.daemon_port
91
+ ws_port = config.websocket.port
92
+
93
+ if not is_port_available(http_port):
94
+ click.echo(f"Waiting for HTTP port {http_port} to become available...", err=True)
95
+ if not wait_for_port_available(http_port, timeout=5.0):
96
+ click.echo(f"Error: Port {http_port} is still in use", err=True)
97
+ sys.exit(1)
98
+
99
+ if not is_port_available(ws_port):
100
+ click.echo(f"Waiting for WebSocket port {ws_port} to become available...", err=True)
101
+ if not wait_for_port_available(ws_port, timeout=5.0):
102
+ click.echo(f"Error: Port {ws_port} is still in use", err=True)
103
+ sys.exit(1)
104
+
105
+ click.echo(f"Ports available (HTTP: {http_port}, WebSocket: {ws_port})")
106
+
107
+ # Start the runner as a subprocess
108
+ click.echo("Starting Gobby daemon...")
109
+
110
+ # Build command
111
+ cmd = [sys.executable, "-m", "gobby.runner"]
112
+ if verbose:
113
+ cmd.append("--verbose")
114
+
115
+ # Open log files
116
+ log_f = open(log_file, "a")
117
+ error_log_f = open(error_log_file, "a")
118
+
119
+ try:
120
+ # Start detached subprocess
121
+ process = subprocess.Popen( # nosec B603 - cmd built from sys.executable and module path
122
+ cmd,
123
+ stdout=log_f,
124
+ stderr=error_log_f,
125
+ stdin=subprocess.DEVNULL,
126
+ start_new_session=True, # Detach from terminal
127
+ env=os.environ.copy(), # Inherit parent's environment (including PATH)
128
+ )
129
+
130
+ # Write PID file
131
+ with open(pid_file, "w") as f:
132
+ f.write(str(process.pid))
133
+
134
+ # Give it a moment to start
135
+ time.sleep(1.0)
136
+
137
+ # Check if still running
138
+ if process.poll() is not None:
139
+ click.echo("Process exited immediately", err=True)
140
+ click.echo(f" Check logs: {error_log_file}", err=True)
141
+ sys.exit(1)
142
+
143
+ # Give server time to fully start
144
+ time.sleep(2.0)
145
+
146
+ # Display formatted status
147
+ # Try to verify daemon is responding
148
+ daemon_healthy = False
149
+ start_time = time.time()
150
+ max_wait = 15.0
151
+
152
+ while (time.time() - start_time) < max_wait:
153
+ try:
154
+ response = httpx.get(f"http://localhost:{http_port}/admin/status", timeout=1.0)
155
+ if response.status_code == 200:
156
+ daemon_healthy = True
157
+ break
158
+ except (httpx.ConnectError, httpx.TimeoutException):
159
+ time.sleep(0.5)
160
+ continue
161
+
162
+ # Format and display status
163
+ status_kwargs = {
164
+ "running": daemon_healthy,
165
+ "pid": process.pid,
166
+ "pid_file": str(pid_file),
167
+ "log_files": str(log_file.parent),
168
+ "http_port": http_port,
169
+ "websocket_port": ws_port,
170
+ }
171
+
172
+ # Fetch rich status if daemon is healthy
173
+ # Brief delay to allow stats to be computed
174
+ if daemon_healthy:
175
+ time.sleep(1.0)
176
+ rich_status = fetch_rich_status(http_port, timeout=2.0)
177
+ status_kwargs.update(rich_status)
178
+
179
+ message = format_status_message(**status_kwargs)
180
+ click.echo("")
181
+ click.echo(message)
182
+ click.echo("")
183
+
184
+ if not daemon_healthy:
185
+ click.echo("Warning: Daemon started but health check failed")
186
+ click.echo(f" Check logs: {error_log_file}")
187
+
188
+ except Exception as e:
189
+ click.echo(f"Error starting daemon: {e}", err=True)
190
+ sys.exit(1)
191
+ finally:
192
+ log_f.close()
193
+ error_log_f.close()
194
+
195
+
196
+ @click.command()
197
+ @click.pass_context
198
+ def stop(ctx: click.Context) -> None:
199
+ """Stop the Gobby daemon."""
200
+ success = stop_daemon_util(quiet=False)
201
+ sys.exit(0 if success else 1)
202
+
203
+
204
+ @click.command()
205
+ @click.option(
206
+ "--verbose",
207
+ "-v",
208
+ is_flag=True,
209
+ help="Enable verbose debug output",
210
+ )
211
+ @click.pass_context
212
+ def restart(ctx: click.Context, verbose: bool) -> None:
213
+ """Restart the Gobby daemon (stop then start)."""
214
+ setup_logging(verbose)
215
+
216
+ click.echo("Restarting Gobby daemon...")
217
+
218
+ # Stop daemon using helper function (doesn't call sys.exit)
219
+ if not stop_daemon_util(quiet=False):
220
+ click.echo("Failed to stop daemon, aborting restart", err=True)
221
+ sys.exit(1)
222
+
223
+ # Wait for cleanup and port release (TIME_WAIT state)
224
+ time.sleep(3)
225
+
226
+ # Call start command
227
+ ctx.invoke(start, verbose=verbose)
228
+
229
+
230
+ @click.command()
231
+ @click.pass_context
232
+ def status(ctx: click.Context) -> None:
233
+ """Show Gobby daemon status and information."""
234
+ config = ctx.obj["config"]
235
+ pid_file = get_gobby_home() / "gobby.pid"
236
+ log_dir = Path(config.logging.client).expanduser().parent
237
+
238
+ # Read PID from file
239
+ if not pid_file.exists():
240
+ message = format_status_message(running=False)
241
+ click.echo(message)
242
+ sys.exit(0)
243
+
244
+ try:
245
+ with open(pid_file) as f:
246
+ pid = int(f.read().strip())
247
+ except Exception:
248
+ message = format_status_message(running=False)
249
+ click.echo(message)
250
+ sys.exit(0)
251
+
252
+ # Check if process is actually running
253
+ try:
254
+ os.kill(pid, 0)
255
+ except ProcessLookupError:
256
+ message = format_status_message(running=False)
257
+ click.echo(message)
258
+ click.echo(f"Note: Stale PID file found (PID {pid})")
259
+ sys.exit(0)
260
+
261
+ # Get process info for uptime (fallback)
262
+ try:
263
+ process = psutil.Process(pid)
264
+ uptime_seconds = time.time() - process.create_time()
265
+ uptime_str = format_uptime(uptime_seconds)
266
+ except Exception:
267
+ uptime_str = None
268
+
269
+ http_port = config.daemon_port
270
+ websocket_port = config.websocket.port
271
+
272
+ # Build status kwargs
273
+ status_kwargs: dict[str, Any] = {
274
+ "running": True,
275
+ "pid": pid,
276
+ "pid_file": str(pid_file),
277
+ "log_files": str(log_dir),
278
+ "uptime": uptime_str,
279
+ "http_port": http_port,
280
+ "websocket_port": websocket_port,
281
+ }
282
+
283
+ # Fetch rich status from daemon API
284
+ rich_status = fetch_rich_status(http_port, timeout=2.0)
285
+ status_kwargs.update(rich_status)
286
+
287
+ # Format and display status
288
+ message = format_status_message(**status_kwargs)
289
+ click.echo(message)
290
+ sys.exit(0)
291
+
292
+
293
+ def get_merge_status() -> dict[str, Any]:
294
+ """
295
+ Get the current merge status for status output.
296
+
297
+ Returns:
298
+ Dict with merge status info:
299
+ - active: bool - Whether there's an active merge
300
+ - resolution_id: str | None - ID of active resolution
301
+ - source_branch: str | None - Source branch being merged
302
+ - target_branch: str | None - Target branch
303
+ - pending_conflicts: int - Number of unresolved conflicts
304
+ """
305
+ try:
306
+ from gobby.storage.database import LocalDatabase
307
+ from gobby.storage.merge_resolutions import MergeResolutionManager
308
+
309
+ db = LocalDatabase()
310
+ manager = MergeResolutionManager(db)
311
+
312
+ resolution = manager.get_active_resolution()
313
+ if not resolution:
314
+ return {"active": False}
315
+
316
+ conflicts = manager.list_conflicts(resolution_id=resolution.id)
317
+ pending_count = sum(1 for c in conflicts if c.status == "pending")
318
+
319
+ return {
320
+ "active": True,
321
+ "resolution_id": resolution.id,
322
+ "source_branch": resolution.source_branch,
323
+ "target_branch": resolution.target_branch,
324
+ "pending_conflicts": pending_count,
325
+ "total_conflicts": len(conflicts),
326
+ }
327
+ except Exception as e:
328
+ logger.debug(f"Error getting merge status: {e}")
329
+ return {"active": False, "error": str(e)}