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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Codex notify script - forwards interactive Codex events to gobby.
|
|
3
|
+
|
|
4
|
+
Codex supports a `notify = [...]` command in `~/.codex/config.toml`. This script is
|
|
5
|
+
intended to be installed to `~/.gobby/hooks/codex/notify.py` and configured as that
|
|
6
|
+
notify command.
|
|
7
|
+
|
|
8
|
+
Codex notify is currently treated as fire-and-forget; this script should never
|
|
9
|
+
block the CLI for long or fail the Codex command on daemon errors.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
DEFAULT_DAEMON_PORT = 8765
|
|
23
|
+
DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
|
|
24
|
+
DEBUG_ENV_VAR = "GOBBY_CODEX_NOTIFY_DEBUG"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _silence_output() -> None:
|
|
28
|
+
"""Prevent notify script output from polluting the interactive Codex UI."""
|
|
29
|
+
if os.environ.get(DEBUG_ENV_VAR):
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
devnull = open(os.devnull, "w", encoding="utf-8") # noqa: SIM115
|
|
34
|
+
sys.stdout = devnull
|
|
35
|
+
sys.stderr = devnull
|
|
36
|
+
except Exception:
|
|
37
|
+
# nosec B110 - if silencing fails, still avoid raising/printing
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_daemon_url() -> str:
|
|
42
|
+
config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
|
|
43
|
+
|
|
44
|
+
port = DEFAULT_DAEMON_PORT
|
|
45
|
+
if config_path.exists():
|
|
46
|
+
try:
|
|
47
|
+
import yaml
|
|
48
|
+
|
|
49
|
+
with config_path.open(encoding="utf-8") as f:
|
|
50
|
+
config = yaml.safe_load(f) or {}
|
|
51
|
+
port = int(config.get("daemon_port", DEFAULT_DAEMON_PORT))
|
|
52
|
+
except Exception:
|
|
53
|
+
port = DEFAULT_DAEMON_PORT
|
|
54
|
+
|
|
55
|
+
return f"http://localhost:{port}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_event_from_stdin() -> dict[str, Any] | None:
|
|
59
|
+
try:
|
|
60
|
+
raw = sys.stdin.read()
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
if not raw.strip():
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
parsed = json.loads(raw)
|
|
69
|
+
except Exception:
|
|
70
|
+
return {"raw": raw}
|
|
71
|
+
|
|
72
|
+
if isinstance(parsed, dict):
|
|
73
|
+
return parsed
|
|
74
|
+
return {"event": parsed}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _extract_text_from_messages(messages: Any) -> str:
|
|
78
|
+
if not isinstance(messages, list):
|
|
79
|
+
return ""
|
|
80
|
+
for message in reversed(messages):
|
|
81
|
+
if not isinstance(message, dict):
|
|
82
|
+
continue
|
|
83
|
+
text = message.get("text") or message.get("content")
|
|
84
|
+
if isinstance(text, str) and text:
|
|
85
|
+
return text
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _normalize_input_data(event: dict[str, Any] | None) -> dict[str, Any]:
|
|
90
|
+
event = event or {}
|
|
91
|
+
|
|
92
|
+
thread_id = (
|
|
93
|
+
event.get("session_id")
|
|
94
|
+
or event.get("thread_id")
|
|
95
|
+
or event.get("threadId")
|
|
96
|
+
or event.get("conversation_id")
|
|
97
|
+
or event.get("conversationId")
|
|
98
|
+
)
|
|
99
|
+
if not thread_id and isinstance(event.get("thread"), dict):
|
|
100
|
+
thread_id = event["thread"].get("id")
|
|
101
|
+
if not thread_id and isinstance(event.get("session"), dict):
|
|
102
|
+
thread_id = event["session"].get("id")
|
|
103
|
+
|
|
104
|
+
messages = event.get("input_messages") or event.get("inputMessages") or event.get("messages")
|
|
105
|
+
last_message = (
|
|
106
|
+
event.get("last_message")
|
|
107
|
+
or event.get("lastMessage")
|
|
108
|
+
or event.get("message")
|
|
109
|
+
or _extract_text_from_messages(messages)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
event_type = (
|
|
113
|
+
event.get("event_type")
|
|
114
|
+
or event.get("eventType")
|
|
115
|
+
or event.get("type")
|
|
116
|
+
or event.get("name")
|
|
117
|
+
or "agent-turn-complete"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"session_id": thread_id or "",
|
|
122
|
+
"event_type": event_type,
|
|
123
|
+
"last_message": last_message or "",
|
|
124
|
+
"input_messages": messages if isinstance(messages, list) else [],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main() -> int:
|
|
129
|
+
_silence_output()
|
|
130
|
+
|
|
131
|
+
event = _read_event_from_stdin()
|
|
132
|
+
input_data = _normalize_input_data(event)
|
|
133
|
+
|
|
134
|
+
daemon_url = _get_daemon_url()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
httpx.post(
|
|
138
|
+
f"{daemon_url}/hooks/execute",
|
|
139
|
+
json={
|
|
140
|
+
"hook_type": "AgentTurnComplete",
|
|
141
|
+
"input_data": input_data,
|
|
142
|
+
"source": "codex",
|
|
143
|
+
},
|
|
144
|
+
timeout=2.0,
|
|
145
|
+
)
|
|
146
|
+
except Exception:
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Remember
|
|
2
|
+
|
|
3
|
+
Store a memory for future sessions using gobby-memory MCP tools.
|
|
4
|
+
|
|
5
|
+
Use the `remember` tool on the `gobby-memory` server with the content provided.
|
|
6
|
+
|
|
7
|
+
**Memory types:**
|
|
8
|
+
- `preference` - User preferences and coding style choices
|
|
9
|
+
- `fact` - Project facts and technical details
|
|
10
|
+
- `pattern` - Recurring code patterns
|
|
11
|
+
- `context` - Background project context
|
|
12
|
+
|
|
13
|
+
After storing, confirm the memory was saved and show its ID.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "httpx",
|
|
6
|
+
# "pyyaml",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
"""Hook Dispatcher - Routes Gemini CLI hooks to HookManager.
|
|
10
|
+
|
|
11
|
+
This is a thin wrapper script that receives hook calls from Gemini CLI
|
|
12
|
+
and routes them to the appropriate handler via HookManager.
|
|
13
|
+
|
|
14
|
+
Gemini CLI invokes hooks with JSON input on stdin and expects JSON output
|
|
15
|
+
on stdout. Exit codes: 0 = allow, 2 = deny.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
hook_dispatcher.py --type SessionStart < input.json > output.json
|
|
19
|
+
hook_dispatcher.py --type BeforeTool --debug < input.json > output.json
|
|
20
|
+
|
|
21
|
+
Exit Codes:
|
|
22
|
+
0 - Success / Allow
|
|
23
|
+
1 - General error (logged, continues)
|
|
24
|
+
2 - Deny / Block
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# Default daemon configuration
|
|
33
|
+
DEFAULT_DAEMON_PORT = 8765
|
|
34
|
+
DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_daemon_url() -> str:
|
|
38
|
+
"""Get the daemon HTTP URL from config file.
|
|
39
|
+
|
|
40
|
+
Reads daemon_port from ~/.gobby/config.yaml if it exists,
|
|
41
|
+
otherwise uses the default port 8765.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Full daemon URL like http://localhost:8765
|
|
45
|
+
"""
|
|
46
|
+
config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
|
|
47
|
+
|
|
48
|
+
if config_path.exists():
|
|
49
|
+
try:
|
|
50
|
+
import yaml
|
|
51
|
+
|
|
52
|
+
with open(config_path) as f:
|
|
53
|
+
config = yaml.safe_load(f) or {}
|
|
54
|
+
port = config.get("daemon_port", DEFAULT_DAEMON_PORT)
|
|
55
|
+
except Exception:
|
|
56
|
+
# If config read fails, use default
|
|
57
|
+
port = DEFAULT_DAEMON_PORT
|
|
58
|
+
else:
|
|
59
|
+
port = DEFAULT_DAEMON_PORT
|
|
60
|
+
|
|
61
|
+
return f"http://localhost:{port}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_arguments() -> argparse.Namespace:
|
|
65
|
+
"""Parse command line arguments.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Parsed arguments with type and debug flags
|
|
69
|
+
"""
|
|
70
|
+
parser = argparse.ArgumentParser(description="Gemini CLI Hook Dispatcher")
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--type",
|
|
73
|
+
required=True,
|
|
74
|
+
help="Hook type (e.g., SessionStart, BeforeTool)",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--debug",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Enable debug logging",
|
|
80
|
+
)
|
|
81
|
+
return parser.parse_args()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_daemon_running(timeout: float = 0.5) -> bool:
|
|
85
|
+
"""Check if gobby daemon is active and responding.
|
|
86
|
+
|
|
87
|
+
Performs a quick health check to verify the HTTP server is running
|
|
88
|
+
before processing hooks. This prevents hook execution when the daemon
|
|
89
|
+
is stopped, avoiding long timeouts and confusing error messages.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
timeout: Maximum time to wait for response in seconds (default: 0.5)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if daemon is running and responding, False otherwise
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import httpx
|
|
99
|
+
|
|
100
|
+
daemon_url = get_daemon_url()
|
|
101
|
+
response = httpx.get(
|
|
102
|
+
f"{daemon_url}/admin/status",
|
|
103
|
+
timeout=timeout,
|
|
104
|
+
follow_redirects=False,
|
|
105
|
+
)
|
|
106
|
+
return response.status_code == 200
|
|
107
|
+
except Exception:
|
|
108
|
+
# Any error (connection refused, timeout, etc.) means client is not running
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
"""Main dispatcher execution.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Exit code (0=allow, 1=error, 2=deny)
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
# Parse arguments
|
|
120
|
+
args = parse_arguments()
|
|
121
|
+
except (argparse.ArgumentError, SystemExit):
|
|
122
|
+
# Argument parsing failed - return empty dict and exit 1
|
|
123
|
+
print(json.dumps({}))
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
hook_type = args.type # PascalCase: SessionStart, BeforeTool, etc.
|
|
127
|
+
debug_mode = args.debug
|
|
128
|
+
|
|
129
|
+
# Check if gobby daemon is running before processing hooks
|
|
130
|
+
if not check_daemon_running():
|
|
131
|
+
# Daemon is not running - return gracefully without processing
|
|
132
|
+
print(
|
|
133
|
+
json.dumps({"status": "daemon_not_running", "message": "gobby daemon is not running"})
|
|
134
|
+
)
|
|
135
|
+
return 0 # Exit 0 (allow) - this is expected behavior, not an error
|
|
136
|
+
|
|
137
|
+
# Setup logger for dispatcher (not HookManager)
|
|
138
|
+
import logging
|
|
139
|
+
|
|
140
|
+
logger = logging.getLogger("gobby.hooks.gemini.dispatcher")
|
|
141
|
+
if debug_mode:
|
|
142
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
143
|
+
else:
|
|
144
|
+
logging.basicConfig(level=logging.INFO)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Read JSON input from stdin
|
|
148
|
+
input_data = json.load(sys.stdin)
|
|
149
|
+
|
|
150
|
+
# Log what Gemini CLI sends us (for debugging hook data issues)
|
|
151
|
+
logger.info(f"[{hook_type}] Received input keys: {list(input_data.keys())}")
|
|
152
|
+
|
|
153
|
+
# Log hook-specific critical fields
|
|
154
|
+
if hook_type == "SessionStart":
|
|
155
|
+
logger.info(f"[SessionStart] session_id={input_data.get('session_id')}")
|
|
156
|
+
elif hook_type == "SessionEnd":
|
|
157
|
+
logger.info(
|
|
158
|
+
f"[SessionEnd] session_id={input_data.get('session_id')}, "
|
|
159
|
+
f"reason={input_data.get('reason')}"
|
|
160
|
+
)
|
|
161
|
+
elif hook_type == "BeforeAgent":
|
|
162
|
+
prompt = input_data.get("prompt", "")
|
|
163
|
+
prompt_preview = prompt[:100] + "..." if len(prompt) > 100 else prompt
|
|
164
|
+
logger.info(
|
|
165
|
+
f"[BeforeAgent] session_id={input_data.get('session_id')}, prompt={prompt_preview}"
|
|
166
|
+
)
|
|
167
|
+
elif hook_type == "BeforeTool":
|
|
168
|
+
tool_name = input_data.get("tool_name") or input_data.get("function_name", "unknown")
|
|
169
|
+
logger.info(
|
|
170
|
+
f"[BeforeTool] tool_name={tool_name}, session_id={input_data.get('session_id')}"
|
|
171
|
+
)
|
|
172
|
+
elif hook_type == "AfterTool":
|
|
173
|
+
tool_name = input_data.get("tool_name") or input_data.get("function_name", "unknown")
|
|
174
|
+
logger.info(
|
|
175
|
+
f"[AfterTool] tool_name={tool_name}, session_id={input_data.get('session_id')}"
|
|
176
|
+
)
|
|
177
|
+
elif hook_type == "BeforeToolSelection":
|
|
178
|
+
logger.info(f"[BeforeToolSelection] session_id={input_data.get('session_id')}")
|
|
179
|
+
elif hook_type == "BeforeModel":
|
|
180
|
+
logger.info(
|
|
181
|
+
f"[BeforeModel] session_id={input_data.get('session_id')}, "
|
|
182
|
+
f"model={input_data.get('model', 'unknown')}"
|
|
183
|
+
)
|
|
184
|
+
elif hook_type == "AfterModel":
|
|
185
|
+
logger.info(f"[AfterModel] session_id={input_data.get('session_id')}")
|
|
186
|
+
elif hook_type == "PreCompress":
|
|
187
|
+
logger.info(f"[PreCompress] session_id={input_data.get('session_id')}")
|
|
188
|
+
elif hook_type == "Notification":
|
|
189
|
+
logger.info(
|
|
190
|
+
f"[Notification] session_id={input_data.get('session_id')}, "
|
|
191
|
+
f"message={input_data.get('message')}"
|
|
192
|
+
)
|
|
193
|
+
elif hook_type == "AfterAgent":
|
|
194
|
+
logger.info(f"[AfterAgent] session_id={input_data.get('session_id')}")
|
|
195
|
+
|
|
196
|
+
if debug_mode:
|
|
197
|
+
logger.debug(f"Input data: {input_data}")
|
|
198
|
+
|
|
199
|
+
except json.JSONDecodeError as e:
|
|
200
|
+
# Invalid JSON input - return empty dict and exit 1
|
|
201
|
+
if debug_mode:
|
|
202
|
+
logger.error(f"JSON decode error: {e}")
|
|
203
|
+
print(json.dumps({}))
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
# Call daemon HTTP endpoint
|
|
207
|
+
import httpx
|
|
208
|
+
|
|
209
|
+
daemon_url = get_daemon_url()
|
|
210
|
+
try:
|
|
211
|
+
response = httpx.post(
|
|
212
|
+
f"{daemon_url}/hooks/execute",
|
|
213
|
+
json={
|
|
214
|
+
"hook_type": hook_type, # PascalCase for Gemini
|
|
215
|
+
"input_data": input_data,
|
|
216
|
+
"source": "gemini", # Required: identifies CLI source
|
|
217
|
+
},
|
|
218
|
+
timeout=30.0, # Generous timeout for hook processing
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if response.status_code == 200:
|
|
222
|
+
# Success - daemon returns result directly
|
|
223
|
+
result = response.json()
|
|
224
|
+
|
|
225
|
+
if debug_mode:
|
|
226
|
+
logger.debug(f"Output data: {result}")
|
|
227
|
+
|
|
228
|
+
# Determine exit code based on decision
|
|
229
|
+
decision = result.get("decision", "allow")
|
|
230
|
+
|
|
231
|
+
# Print JSON output for Gemini CLI
|
|
232
|
+
if result and result != {}:
|
|
233
|
+
print(json.dumps(result))
|
|
234
|
+
|
|
235
|
+
# Exit code: 0 = allow, 2 = deny
|
|
236
|
+
if decision == "deny":
|
|
237
|
+
return 2
|
|
238
|
+
return 0
|
|
239
|
+
else:
|
|
240
|
+
# HTTP error from daemon
|
|
241
|
+
error_detail = response.text
|
|
242
|
+
logger.error(
|
|
243
|
+
f"Daemon returned error: status={response.status_code}, detail={error_detail}"
|
|
244
|
+
)
|
|
245
|
+
print(json.dumps({"status": "error", "message": f"Daemon error: {error_detail}"}))
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
except httpx.ConnectError:
|
|
249
|
+
# Daemon not reachable
|
|
250
|
+
logger.error("Failed to connect to daemon (unreachable)")
|
|
251
|
+
print(json.dumps({"status": "error", "message": "Daemon unreachable"}))
|
|
252
|
+
return 1
|
|
253
|
+
|
|
254
|
+
except httpx.TimeoutException:
|
|
255
|
+
# Hook processing took too long
|
|
256
|
+
logger.error(f"Hook execution timeout: {hook_type}")
|
|
257
|
+
print(json.dumps({"status": "error", "message": "Hook execution timeout"}))
|
|
258
|
+
return 1
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
# General error - log and return 1
|
|
262
|
+
logger.error(f"Hook execution failed: {e}", exc_info=True)
|
|
263
|
+
print(json.dumps({"status": "error", "message": str(e)}))
|
|
264
|
+
return 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if __name__ == "__main__":
|
|
268
|
+
sys.exit(main())
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"name": "gobby-session-start",
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=SessionStart",
|
|
10
|
+
"timeout": 30000
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SessionEnd": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"name": "gobby-session-end",
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=SessionEnd",
|
|
22
|
+
"timeout": 30000
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"BeforeAgent": [
|
|
28
|
+
{
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"name": "gobby-before-agent",
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeAgent",
|
|
34
|
+
"timeout": 30000
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"AfterAgent": [
|
|
40
|
+
{
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"name": "gobby-after-agent",
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=AfterAgent",
|
|
46
|
+
"timeout": 30000
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"BeforeTool": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "*",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"name": "gobby-before-tool",
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeTool",
|
|
59
|
+
"timeout": 30000
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"AfterTool": [
|
|
65
|
+
{
|
|
66
|
+
"matcher": "*",
|
|
67
|
+
"hooks": [
|
|
68
|
+
{
|
|
69
|
+
"name": "gobby-after-tool",
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=AfterTool",
|
|
72
|
+
"timeout": 30000
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"BeforeToolSelection": [
|
|
78
|
+
{
|
|
79
|
+
"hooks": [
|
|
80
|
+
{
|
|
81
|
+
"name": "gobby-before-tool-selection",
|
|
82
|
+
"type": "command",
|
|
83
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeToolSelection",
|
|
84
|
+
"timeout": 30000
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"BeforeModel": [
|
|
90
|
+
{
|
|
91
|
+
"hooks": [
|
|
92
|
+
{
|
|
93
|
+
"name": "gobby-before-model",
|
|
94
|
+
"type": "command",
|
|
95
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeModel",
|
|
96
|
+
"timeout": 30000
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"AfterModel": [
|
|
102
|
+
{
|
|
103
|
+
"hooks": [
|
|
104
|
+
{
|
|
105
|
+
"name": "gobby-after-model",
|
|
106
|
+
"type": "command",
|
|
107
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=AfterModel",
|
|
108
|
+
"timeout": 30000
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"PreCompress": [
|
|
114
|
+
{
|
|
115
|
+
"hooks": [
|
|
116
|
+
{
|
|
117
|
+
"name": "gobby-pre-compress",
|
|
118
|
+
"type": "command",
|
|
119
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=PreCompress",
|
|
120
|
+
"timeout": 30000
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"Notification": [
|
|
126
|
+
{
|
|
127
|
+
"hooks": [
|
|
128
|
+
{
|
|
129
|
+
"name": "gobby-notification",
|
|
130
|
+
"type": "command",
|
|
131
|
+
"command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=Notification",
|
|
132
|
+
"timeout": 30000
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
}
|