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/utils/logging.py ADDED
@@ -0,0 +1,376 @@
1
+ """
2
+ Logging utilities for request tracking and structured logging.
3
+
4
+ Provides request ID tracking, context propagation, custom log adapters,
5
+ and file-based logging configuration.
6
+ """
7
+
8
+ import contextvars
9
+ import logging
10
+ import logging.handlers
11
+ import uuid
12
+ from collections.abc import MutableMapping
13
+ from pathlib import Path
14
+ from typing import Any, ClassVar
15
+
16
+ # Context variable for tracking request IDs across async operations
17
+ request_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
18
+ "request_id", default=None
19
+ )
20
+
21
+
22
+ class RequestIDFilter(logging.Filter):
23
+ """Add request ID to log records if available."""
24
+
25
+ def filter(self, record: logging.LogRecord) -> bool:
26
+ """Add request_id attribute to log record."""
27
+ request_id = request_id_var.get()
28
+ record.request_id = request_id if request_id else "-"
29
+ return True
30
+
31
+
32
+ class ContextLogger(logging.LoggerAdapter[logging.Logger]):
33
+ """
34
+ Logger adapter that adds contextual information to log records.
35
+
36
+ Supports adding request_id, operation, duration_ms, and other metadata.
37
+ """
38
+
39
+ def process(
40
+ self, msg: str, kwargs: MutableMapping[str, Any]
41
+ ) -> tuple[str, MutableMapping[str, Any]]:
42
+ """Add extra context to log record."""
43
+ extra = kwargs.get("extra", {})
44
+
45
+ # Add request ID if available
46
+ request_id = request_id_var.get()
47
+ if request_id:
48
+ extra["request_id"] = request_id
49
+
50
+ # Merge with any existing extra data
51
+ if self.extra:
52
+ extra.update(self.extra)
53
+
54
+ kwargs["extra"] = extra
55
+ return msg, kwargs
56
+
57
+
58
+ def generate_request_id() -> str:
59
+ """Generate a unique request ID."""
60
+ return str(uuid.uuid4())
61
+
62
+
63
+ def set_request_id(request_id: str | None = None) -> str:
64
+ """
65
+ Set the request ID for the current context.
66
+
67
+ Args:
68
+ request_id: Request ID to set. If None, generates a new one.
69
+
70
+ Returns:
71
+ The request ID that was set.
72
+ """
73
+ if request_id is None:
74
+ request_id = generate_request_id()
75
+ request_id_var.set(request_id)
76
+ return request_id
77
+
78
+
79
+ def get_request_id() -> str | None:
80
+ """Get the current request ID from context."""
81
+ return request_id_var.get()
82
+
83
+
84
+ def clear_request_id() -> None:
85
+ """Clear the request ID from context."""
86
+ request_id_var.set(None)
87
+
88
+
89
+ def get_context_logger(name: str, extra: dict[str, Any] | None = None) -> ContextLogger:
90
+ """
91
+ Get a logger with context support.
92
+
93
+ Args:
94
+ name: Logger name (usually __name__)
95
+ extra: Additional context to include in all log messages
96
+
97
+ Returns:
98
+ ContextLogger instance
99
+ """
100
+ logger = logging.getLogger(name)
101
+ return ContextLogger(logger, extra or {})
102
+
103
+
104
+ class ExtraFieldsFormatter(logging.Formatter):
105
+ """
106
+ Custom formatter that includes extra fields in log output.
107
+
108
+ Formats log records to include any extra fields passed via the 'extra'
109
+ parameter, making debugging easier by showing all context.
110
+ """
111
+
112
+ # Standard logging record attributes to exclude from extra fields
113
+ STANDARD_ATTRS: ClassVar[set[str]] = {
114
+ "name",
115
+ "msg",
116
+ "args",
117
+ "created",
118
+ "filename",
119
+ "funcName",
120
+ "levelname",
121
+ "levelno",
122
+ "lineno",
123
+ "module",
124
+ "msecs",
125
+ "message",
126
+ "pathname",
127
+ "process",
128
+ "processName",
129
+ "relativeCreated",
130
+ "thread",
131
+ "threadName",
132
+ "exc_info",
133
+ "exc_text",
134
+ "stack_info",
135
+ "asctime",
136
+ "request_id",
137
+ "short_name",
138
+ }
139
+
140
+ def format(self, record: logging.LogRecord) -> str:
141
+ """Format log record including extra fields."""
142
+ # Strip gobby. prefix from logger name for cleaner output
143
+ # e.g., "gobby.http_server" -> "http_server"
144
+ # Use short_name attribute to avoid mutating record.name (which leaks to other handlers)
145
+ if record.name.startswith("gobby."):
146
+ record.short_name = record.name[6:] # len("gobby.") = 6
147
+ else:
148
+ record.short_name = record.name
149
+
150
+ # Format the base message
151
+ base_msg = super().format(record)
152
+
153
+ # Collect extra fields
154
+ extra_fields = {}
155
+ for key, value in record.__dict__.items():
156
+ if key not in self.STANDARD_ATTRS and not key.startswith("_"):
157
+ extra_fields[key] = value
158
+
159
+ # Append extra fields if any exist
160
+ if extra_fields:
161
+ extra_str = " | ".join(f"{k}={v}" for k, v in extra_fields.items())
162
+ return f"{base_msg} | {extra_str}"
163
+
164
+ return base_msg
165
+
166
+
167
+ def setup_file_logging(verbose: bool = False) -> None:
168
+ """
169
+ Configure rotating file handlers for logging.
170
+
171
+ Sets up two log files:
172
+ 1. Main log: All messages (level from config, or DEBUG if verbose flag set)
173
+ 2. Error log: Only ERROR and CRITICAL messages
174
+
175
+ Args:
176
+ verbose: If True, override config level to DEBUG
177
+
178
+ Log file paths and rotation settings are loaded from ~/.gobby/config.yaml:
179
+ - logging.level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
180
+ - logging.format: Log format (text or json)
181
+ - logging.client: Main log file path
182
+ - logging.client_error: Error log file path
183
+ - logging.max_size_mb: Max file size before rotation
184
+ - logging.backup_count: Number of backup files to keep
185
+ """
186
+ # Load config to get log file paths and rotation settings
187
+ from gobby.config.app import load_config
188
+
189
+ config = load_config()
190
+
191
+ # Expand paths and ensure log directory exists
192
+ log_file_path = Path(config.logging.client).expanduser()
193
+ error_log_file_path = Path(config.logging.client_error).expanduser()
194
+
195
+ log_file_path.parent.mkdir(parents=True, exist_ok=True)
196
+ error_log_file_path.parent.mkdir(parents=True, exist_ok=True)
197
+
198
+ # Get rotation settings from config
199
+ max_bytes = config.logging.max_size_mb * 1024 * 1024
200
+ backup_count = config.logging.backup_count
201
+
202
+ # Get the gobby logger (package-level)
203
+ pkg_logger = logging.getLogger("gobby")
204
+
205
+ # Get logging level from config (verbose flag overrides to DEBUG)
206
+ if verbose:
207
+ log_level = logging.DEBUG
208
+ else:
209
+ config_level = getattr(config.logging, "level", "INFO").upper()
210
+ log_level = getattr(logging, config_level, logging.INFO)
211
+ pkg_logger.setLevel(log_level)
212
+
213
+ # Remove any existing handlers to avoid duplicates
214
+ for handler in pkg_logger.handlers[:]:
215
+ handler.close()
216
+ pkg_logger.removeHandler(handler)
217
+
218
+ # Get log format from config (text or json)
219
+ log_format_type = getattr(config.logging, "format", "text").lower()
220
+
221
+ # Create formatter based on config format
222
+ if log_format_type == "json":
223
+ # JSON format for structured logging
224
+ log_format = '{"time": "%(asctime)s", "level": "%(levelname)s", "module": "%(short_name)s", "func": "%(funcName)s", "message": "%(message)s"}'
225
+ formatter = ExtraFieldsFormatter(log_format, datefmt="%Y-%m-%dT%H:%M:%S")
226
+ else:
227
+ # Text format (default) - human readable
228
+ log_format = "%(asctime)s - %(levelname)-8s - %(short_name)s.%(funcName)s - %(message)s"
229
+ formatter = ExtraFieldsFormatter(log_format, datefmt="%Y-%m-%d %H:%M:%S")
230
+
231
+ # Create request ID filter
232
+ request_id_filter = RequestIDFilter()
233
+
234
+ # Create main log handler with rotation
235
+ main_handler = logging.handlers.RotatingFileHandler(
236
+ filename=str(log_file_path),
237
+ maxBytes=max_bytes,
238
+ backupCount=backup_count,
239
+ encoding="utf-8",
240
+ )
241
+ main_handler.setLevel(log_level)
242
+ main_handler.setFormatter(formatter)
243
+ main_handler.addFilter(request_id_filter)
244
+ pkg_logger.addHandler(main_handler)
245
+
246
+ # Create error log handler (ERROR and above only)
247
+ error_handler = logging.handlers.RotatingFileHandler(
248
+ filename=str(error_log_file_path),
249
+ maxBytes=max_bytes,
250
+ backupCount=backup_count,
251
+ encoding="utf-8",
252
+ )
253
+ error_handler.setLevel(logging.ERROR)
254
+ error_handler.setFormatter(formatter)
255
+ error_handler.addFilter(request_id_filter)
256
+ pkg_logger.addHandler(error_handler)
257
+
258
+ # Prevent propagation to root logger to avoid duplicate logs
259
+ pkg_logger.propagate = False
260
+
261
+ # Log setup confirmation
262
+ logger = logging.getLogger(__name__)
263
+ logger.debug(
264
+ f"File logging configured (level={logging.getLevelName(log_level)}, "
265
+ f"main_log={log_file_path}, error_log={error_log_file_path})"
266
+ )
267
+
268
+
269
+ def setup_mcp_logging(verbose: bool = False) -> tuple[logging.Logger, logging.Logger]:
270
+ """
271
+ Configure separate loggers for MCP server and client operations.
272
+
273
+ Sets up dedicated log files for:
274
+ 1. MCP Server: Logs for starting/stopping the MCP server, tool registration
275
+ 2. MCP Client: Logs for connecting to downstream servers, proxy operations
276
+
277
+ Args:
278
+ verbose: If True, override config level to DEBUG
279
+
280
+ Returns:
281
+ Tuple of (mcp_server_logger, mcp_client_logger)
282
+
283
+ Log file paths are loaded from ~/.gobby/config.yaml:
284
+ - logging.mcp_server: MCP server log file path
285
+ - logging.mcp_client: MCP client log file path
286
+ """
287
+ from gobby.config.app import load_config
288
+
289
+ config = load_config()
290
+
291
+ # Get log file paths from config
292
+ mcp_server_log_path = Path(config.logging.mcp_server).expanduser()
293
+ mcp_client_log_path = Path(config.logging.mcp_client).expanduser()
294
+
295
+ # Ensure directories exist
296
+ mcp_server_log_path.parent.mkdir(parents=True, exist_ok=True)
297
+ mcp_client_log_path.parent.mkdir(parents=True, exist_ok=True)
298
+
299
+ # Get rotation settings from config
300
+ max_bytes = config.logging.max_size_mb * 1024 * 1024
301
+ backup_count = config.logging.backup_count
302
+
303
+ # Get logging level from config (verbose flag overrides to DEBUG)
304
+ if verbose:
305
+ log_level = logging.DEBUG
306
+ else:
307
+ config_level = getattr(config.logging, "level", "INFO").upper()
308
+ log_level = getattr(logging, config_level, logging.INFO)
309
+
310
+ # Get log format from config
311
+ log_format_type = getattr(config.logging, "format", "text").lower()
312
+
313
+ if log_format_type == "json":
314
+ log_format = '{"time": "%(asctime)s", "level": "%(levelname)s", "name": "%(name)s", "message": "%(message)s"}'
315
+ date_format = "%Y-%m-%dT%H:%M:%S"
316
+ else:
317
+ log_format = "%(asctime)s - %(levelname)-8s - %(name)s - %(message)s"
318
+ date_format = "%Y-%m-%d %H:%M:%S"
319
+
320
+ formatter = logging.Formatter(log_format, datefmt=date_format)
321
+
322
+ # Setup MCP Server logger
323
+ mcp_server_logger = logging.getLogger("gobby.mcp.server")
324
+ mcp_server_logger.setLevel(log_level)
325
+
326
+ # Clear existing handlers to avoid duplicates
327
+ for handler in mcp_server_logger.handlers[:]:
328
+ handler.close()
329
+ mcp_server_logger.removeHandler(handler)
330
+
331
+ mcp_server_handler = logging.handlers.RotatingFileHandler(
332
+ filename=str(mcp_server_log_path),
333
+ maxBytes=max_bytes,
334
+ backupCount=backup_count,
335
+ encoding="utf-8",
336
+ )
337
+ mcp_server_handler.setLevel(log_level)
338
+ mcp_server_handler.setFormatter(formatter)
339
+ mcp_server_logger.addHandler(mcp_server_handler)
340
+ mcp_server_logger.propagate = False # Don't propagate to avoid duplicate logs
341
+
342
+ # Setup MCP Client logger
343
+ mcp_client_logger = logging.getLogger("gobby.mcp.client")
344
+ mcp_client_logger.setLevel(log_level)
345
+
346
+ # Clear existing handlers to avoid duplicates
347
+ for handler in mcp_client_logger.handlers[:]:
348
+ handler.close()
349
+ mcp_client_logger.removeHandler(handler)
350
+
351
+ mcp_client_handler = logging.handlers.RotatingFileHandler(
352
+ filename=str(mcp_client_log_path),
353
+ maxBytes=max_bytes,
354
+ backupCount=backup_count,
355
+ encoding="utf-8",
356
+ )
357
+ mcp_client_handler.setLevel(log_level)
358
+ mcp_client_handler.setFormatter(formatter)
359
+ mcp_client_logger.addHandler(mcp_client_handler)
360
+ mcp_client_logger.propagate = False # Don't propagate to avoid duplicate logs
361
+
362
+ # Log setup confirmation
363
+ mcp_server_logger.debug(f"MCP server logging configured (path={mcp_server_log_path})")
364
+ mcp_client_logger.debug(f"MCP client logging configured (path={mcp_client_log_path})")
365
+
366
+ return mcp_server_logger, mcp_client_logger
367
+
368
+
369
+ def get_mcp_server_logger() -> logging.Logger:
370
+ """Get the MCP server logger (creates if not configured)."""
371
+ return logging.getLogger("gobby.mcp.server")
372
+
373
+
374
+ def get_mcp_client_logger() -> logging.Logger:
375
+ """Get the MCP client logger (creates if not configured)."""
376
+ return logging.getLogger("gobby.mcp.client")
@@ -0,0 +1,135 @@
1
+ """Machine ID utility.
2
+
3
+ Provides stable machine identification stored in ~/.gobby/machine_id.
4
+ Uses py-machineid for hardware-based IDs with UUID fallback.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import threading
10
+ import uuid
11
+ from pathlib import Path
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Thread-safe cache
16
+ _cache_lock = threading.Lock()
17
+ _cached_machine_id: str | None = None
18
+
19
+ # Default location for machine ID file
20
+ MACHINE_ID_FILE = Path("~/.gobby/machine_id").expanduser()
21
+
22
+
23
+ def get_machine_id() -> str | None:
24
+ """Get stable machine ID from ~/.gobby/machine_id.
25
+
26
+ Strategy:
27
+ 1. Return cached ID if available
28
+ 2. Check ~/.gobby/machine_id file
29
+ 3. If not present, generate ID and save to file
30
+
31
+ Returns:
32
+ Machine ID as string, or None if operations fail
33
+
34
+ Raises:
35
+ OSError: If file operations fail
36
+ """
37
+ global _cached_machine_id
38
+
39
+ # Fast path: Return cached ID
40
+ with _cache_lock:
41
+ if _cached_machine_id is not None:
42
+ return _cached_machine_id
43
+
44
+ try:
45
+ machine_id = _get_or_create_machine_id()
46
+ if machine_id:
47
+ with _cache_lock:
48
+ _cached_machine_id = machine_id
49
+ return machine_id
50
+ except OSError as e:
51
+ # Let OSError propagate for file system issues
52
+ raise OSError(f"Failed to retrieve or create machine ID: {e}") from e
53
+
54
+ return None
55
+
56
+
57
+ def _get_or_create_machine_id() -> str:
58
+ """Get or create machine ID from ~/.gobby/machine_id.
59
+
60
+ Strategy:
61
+ 1. Read from file if present
62
+ 2. Migrate from config.yaml if present there (one-time migration)
63
+ 3. Generate new ID and save to file
64
+
65
+ Returns:
66
+ Machine ID string
67
+
68
+ Raises:
69
+ OSError: If file operations fail
70
+ """
71
+ # Ensure directory exists
72
+ MACHINE_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
73
+
74
+ # Check if file exists and has content
75
+ if MACHINE_ID_FILE.exists():
76
+ content = MACHINE_ID_FILE.read_text().strip()
77
+ if content:
78
+ return content
79
+
80
+ # Generate new ID and save with atomic permissions
81
+ new_id = _generate_machine_id()
82
+ _write_file_secure(MACHINE_ID_FILE, new_id)
83
+
84
+ return new_id
85
+
86
+
87
+ def _write_file_secure(path: Path, content: str) -> None:
88
+ """Write content to file with restrictive permissions atomically.
89
+
90
+ Uses os.open with O_CREAT to set permissions at creation time,
91
+ avoiding TOCTOU race condition with write_text()/chmod() pattern.
92
+
93
+ Args:
94
+ path: File path to write to
95
+ content: Content to write
96
+
97
+ Raises:
98
+ OSError: If file operations fail
99
+ """
100
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
101
+ try:
102
+ os.write(fd, content.encode())
103
+ finally:
104
+ os.close(fd)
105
+
106
+
107
+ def _generate_machine_id() -> str:
108
+ """Generate a new machine ID.
109
+
110
+ Uses py-machineid for hardware-based ID, falls back to UUID4.
111
+
112
+ Returns:
113
+ Generated machine ID string
114
+ """
115
+ try:
116
+ import machineid
117
+
118
+ return str(machineid.id())
119
+ except ImportError:
120
+ # Library not available, use UUID fallback
121
+ return str(uuid.uuid4())
122
+ except Exception as e:
123
+ # machineid library failed (hardware access issues, etc.)
124
+ logger.debug(f"machineid.id() failed, using UUID fallback: {e}")
125
+ return str(uuid.uuid4())
126
+
127
+
128
+ def clear_cache() -> None:
129
+ """Clear the cached machine ID.
130
+
131
+ Useful for testing or when machine ID needs to be refreshed.
132
+ """
133
+ global _cached_machine_id
134
+ with _cache_lock:
135
+ _cached_machine_id = None