omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__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.
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_handler_source_mode.py +16 -2
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
- omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
- omnibase_infra/event_bus/event_bus_kafka.py +105 -47
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
- omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/handlers/handler_consul.py +2 -0
- omnibase_infra/handlers/mixins/__init__.py +5 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/mixin_node_introspection.py +189 -19
- omnibase_infra/models/__init__.py +8 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/discovery/model_introspection_config.py +25 -17
- omnibase_infra/models/dispatch/__init__.py +8 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
- omnibase_infra/models/registration/__init__.py +9 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
- omnibase_infra/models/runtime/__init__.py +9 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
- omnibase_infra/nodes/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
- omnibase_infra/nodes/reducers/models/__init__.py +7 -2
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
- omnibase_infra/protocols/__init__.py +3 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/runtime/__init__.py +60 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_source_resolver.py +43 -2
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/models/__init__.py +13 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/service_kernel.py +76 -6
- omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
- omnibase_infra/runtime/service_runtime_host_process.py +770 -20
- omnibase_infra/runtime/transition_notification_publisher.py +3 -2
- omnibase_infra/runtime/util_wiring.py +206 -62
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
- omnibase_infra/services/session/config_consumer.py +25 -8
- omnibase_infra/services/session/config_store.py +2 -2
- omnibase_infra/services/session/consumer.py +1 -1
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/typed_dict/__init__.py +9 -1
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/utils/__init__.py +9 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/validation/infra_validators.py +18 -1
- omnibase_infra/validation/validation_exemptions.yaml +192 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Command-Line Interface for Hook Event Emit Daemon.
|
|
4
|
+
|
|
5
|
+
This module provides the CLI for managing and testing the emit daemon.
|
|
6
|
+
The daemon provides a Unix socket interface for Claude Code hooks to emit
|
|
7
|
+
events to Kafka without blocking hook execution.
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
start - Start the daemon (foreground or daemonized)
|
|
11
|
+
stop - Stop the running daemon
|
|
12
|
+
status - Check daemon status and queue metrics
|
|
13
|
+
emit - Emit a test event
|
|
14
|
+
config - Show resolved configuration
|
|
15
|
+
|
|
16
|
+
Usage Examples:
|
|
17
|
+
# Start daemon in foreground
|
|
18
|
+
emit-daemon start --kafka-servers localhost:9092
|
|
19
|
+
|
|
20
|
+
# Start daemon in background
|
|
21
|
+
emit-daemon start --kafka-servers localhost:9092 --daemonize
|
|
22
|
+
|
|
23
|
+
# Check status
|
|
24
|
+
emit-daemon status
|
|
25
|
+
|
|
26
|
+
# Emit a test event
|
|
27
|
+
emit-daemon emit --event-type prompt.submitted --payload '{"session_id": "test"}'
|
|
28
|
+
|
|
29
|
+
# Stop daemon
|
|
30
|
+
emit-daemon stop
|
|
31
|
+
|
|
32
|
+
Related Tickets:
|
|
33
|
+
- OMN-1610: Hook Event Daemon MVP
|
|
34
|
+
|
|
35
|
+
.. versionadded:: 0.2.6
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import asyncio
|
|
42
|
+
import json
|
|
43
|
+
import logging
|
|
44
|
+
import os
|
|
45
|
+
import signal
|
|
46
|
+
import socket
|
|
47
|
+
import sys
|
|
48
|
+
import time
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
|
|
51
|
+
import yaml
|
|
52
|
+
|
|
53
|
+
from omnibase_core.errors import OnexError
|
|
54
|
+
from omnibase_infra import __version__
|
|
55
|
+
from omnibase_infra.runtime.emit_daemon.config import ModelEmitDaemonConfig
|
|
56
|
+
from omnibase_infra.runtime.emit_daemon.daemon import EmitDaemon
|
|
57
|
+
|
|
58
|
+
# Default timeout for socket operations (seconds)
|
|
59
|
+
DEFAULT_SOCKET_TIMEOUT: float = 5.0
|
|
60
|
+
|
|
61
|
+
# Default graceful shutdown wait time (seconds)
|
|
62
|
+
DEFAULT_SHUTDOWN_WAIT: float = 30.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _setup_logging(verbose: bool = False) -> None:
|
|
66
|
+
"""Configure logging for CLI operations.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
verbose: If True, set DEBUG level; otherwise INFO level.
|
|
70
|
+
"""
|
|
71
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
72
|
+
logging.basicConfig(
|
|
73
|
+
level=level,
|
|
74
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
75
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_config_file(config_path: Path) -> dict[str, object]:
|
|
80
|
+
"""Load configuration from YAML file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config_path: Path to the YAML configuration file.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary of configuration values. Values are str, int, float, or bool
|
|
87
|
+
as parsed from YAML. Pydantic handles final type validation.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
SystemExit: If file cannot be read or parsed.
|
|
91
|
+
"""
|
|
92
|
+
if not config_path.exists():
|
|
93
|
+
print(f"Error: Config file not found: {config_path}", file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
98
|
+
data = yaml.safe_load(f)
|
|
99
|
+
if not isinstance(data, dict):
|
|
100
|
+
print("Error: Config file must be a YAML mapping", file=sys.stderr)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
# Filter out None values, keep all YAML-parsed types (str, int, float, bool)
|
|
103
|
+
# Pydantic handles final type validation and conversion
|
|
104
|
+
result: dict[str, object] = {}
|
|
105
|
+
for key, value in data.items():
|
|
106
|
+
if value is not None:
|
|
107
|
+
result[key] = value
|
|
108
|
+
return result
|
|
109
|
+
except yaml.YAMLError as e:
|
|
110
|
+
print(f"Error: Invalid YAML in config file: {e}", file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
except OSError as e:
|
|
113
|
+
print(f"Error: Cannot read config file: {e}", file=sys.stderr)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _build_config(args: argparse.Namespace) -> ModelEmitDaemonConfig:
|
|
118
|
+
"""Build configuration from CLI arguments and config file.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
args: Parsed CLI arguments.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Validated configuration model.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
SystemExit: If configuration is invalid.
|
|
128
|
+
"""
|
|
129
|
+
# Start with empty config dict (object allows all config value types)
|
|
130
|
+
config_dict: dict[str, object] = {}
|
|
131
|
+
|
|
132
|
+
# Load from config file if specified
|
|
133
|
+
config_file = getattr(args, "config", None)
|
|
134
|
+
if config_file:
|
|
135
|
+
config_dict.update(_load_config_file(Path(config_file)))
|
|
136
|
+
|
|
137
|
+
# Apply CLI overrides (these take precedence over config file)
|
|
138
|
+
if hasattr(args, "kafka_servers") and args.kafka_servers:
|
|
139
|
+
config_dict["kafka_bootstrap_servers"] = args.kafka_servers
|
|
140
|
+
|
|
141
|
+
if hasattr(args, "socket_path") and args.socket_path:
|
|
142
|
+
config_dict["socket_path"] = Path(args.socket_path)
|
|
143
|
+
|
|
144
|
+
if hasattr(args, "pid_path") and args.pid_path:
|
|
145
|
+
config_dict["pid_path"] = Path(args.pid_path)
|
|
146
|
+
|
|
147
|
+
if hasattr(args, "spool_dir") and args.spool_dir:
|
|
148
|
+
config_dict["spool_dir"] = Path(args.spool_dir)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Use with_env_overrides to also pick up environment variables
|
|
152
|
+
return ModelEmitDaemonConfig.with_env_overrides(**config_dict)
|
|
153
|
+
except ValueError as e:
|
|
154
|
+
print(f"Error: Invalid configuration: {e}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _send_socket_message(
|
|
159
|
+
socket_path: Path,
|
|
160
|
+
message: dict[str, object],
|
|
161
|
+
timeout: float = DEFAULT_SOCKET_TIMEOUT,
|
|
162
|
+
) -> dict[str, object]:
|
|
163
|
+
"""Send a message to the daemon via Unix socket.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
socket_path: Path to the Unix domain socket.
|
|
167
|
+
message: JSON-serializable message to send.
|
|
168
|
+
timeout: Socket timeout in seconds.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Parsed JSON response from daemon.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ConnectionError: If cannot connect to daemon.
|
|
175
|
+
TimeoutError: If operation times out.
|
|
176
|
+
OnexError: If response is invalid JSON.
|
|
177
|
+
"""
|
|
178
|
+
if not socket_path.exists():
|
|
179
|
+
raise ConnectionError(f"Socket not found: {socket_path}")
|
|
180
|
+
|
|
181
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
182
|
+
sock.settimeout(timeout)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
sock.connect(str(socket_path))
|
|
186
|
+
|
|
187
|
+
# Send message (newline-delimited JSON)
|
|
188
|
+
request = json.dumps(message) + "\n"
|
|
189
|
+
sock.sendall(request.encode("utf-8"))
|
|
190
|
+
|
|
191
|
+
# Read response
|
|
192
|
+
response_data = b""
|
|
193
|
+
while True:
|
|
194
|
+
chunk = sock.recv(4096)
|
|
195
|
+
if not chunk:
|
|
196
|
+
break
|
|
197
|
+
response_data += chunk
|
|
198
|
+
if b"\n" in response_data:
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
if not response_data:
|
|
202
|
+
raise ConnectionError("Empty response from daemon")
|
|
203
|
+
|
|
204
|
+
# Parse response
|
|
205
|
+
response_str = response_data.decode("utf-8").strip()
|
|
206
|
+
result = json.loads(response_str)
|
|
207
|
+
if not isinstance(result, dict):
|
|
208
|
+
raise OnexError("Response must be a JSON object")
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
except TimeoutError as e:
|
|
212
|
+
raise TimeoutError(f"Socket operation timed out: {e}") from e
|
|
213
|
+
except json.JSONDecodeError as e:
|
|
214
|
+
raise OnexError(f"Invalid JSON response: {e}") from e
|
|
215
|
+
except OSError as e:
|
|
216
|
+
raise ConnectionError(f"Socket error: {e}") from e
|
|
217
|
+
finally:
|
|
218
|
+
sock.close()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _read_pid_file(pid_path: Path) -> int | None:
|
|
222
|
+
"""Read PID from daemon PID file.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
pid_path: Path to the PID file.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The PID if file exists and is valid, None otherwise.
|
|
229
|
+
"""
|
|
230
|
+
if not pid_path.exists():
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
pid_str = pid_path.read_text().strip()
|
|
235
|
+
return int(pid_str)
|
|
236
|
+
except (OSError, ValueError):
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _is_process_running(pid: int) -> bool:
|
|
241
|
+
"""Check if a process with the given PID is running.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
pid: Process ID to check.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if process is running, False otherwise.
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
os.kill(pid, 0)
|
|
251
|
+
return True
|
|
252
|
+
except ProcessLookupError:
|
|
253
|
+
return False
|
|
254
|
+
except PermissionError:
|
|
255
|
+
# Process exists but we can't signal it
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _daemonize() -> None:
|
|
260
|
+
"""Fork the current process into a daemon.
|
|
261
|
+
|
|
262
|
+
Double-fork to prevent zombie processes and detach from terminal.
|
|
263
|
+
After this call, the parent process should exit.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
SystemExit: On Windows (os.fork() is not available).
|
|
267
|
+
"""
|
|
268
|
+
# Check platform - fork() is Unix-only
|
|
269
|
+
if sys.platform == "win32":
|
|
270
|
+
print("Error: --daemonize is not supported on Windows", file=sys.stderr)
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
|
|
273
|
+
# First fork
|
|
274
|
+
pid = os.fork()
|
|
275
|
+
if pid > 0:
|
|
276
|
+
# Parent process - exit
|
|
277
|
+
sys.exit(0)
|
|
278
|
+
|
|
279
|
+
# Become session leader
|
|
280
|
+
os.setsid()
|
|
281
|
+
|
|
282
|
+
# Second fork (prevent acquiring a controlling terminal)
|
|
283
|
+
pid = os.fork()
|
|
284
|
+
if pid > 0:
|
|
285
|
+
# First child - exit
|
|
286
|
+
sys.exit(0)
|
|
287
|
+
|
|
288
|
+
# We're now in the grandchild (daemon) process
|
|
289
|
+
|
|
290
|
+
# Change working directory to root
|
|
291
|
+
os.chdir("/")
|
|
292
|
+
|
|
293
|
+
# Reset file creation mask to secure default (rw-r--r-- for files, rwxr-xr-x for dirs)
|
|
294
|
+
os.umask(0o022)
|
|
295
|
+
|
|
296
|
+
# Redirect standard file descriptors to /dev/null
|
|
297
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
298
|
+
os.dup2(devnull, sys.stdin.fileno())
|
|
299
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
300
|
+
os.dup2(devnull, sys.stderr.fileno())
|
|
301
|
+
os.close(devnull)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def cmd_start(args: argparse.Namespace) -> None:
|
|
305
|
+
"""Handle the 'start' command.
|
|
306
|
+
|
|
307
|
+
Start the emit daemon in foreground or background mode.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
args: Parsed CLI arguments.
|
|
311
|
+
"""
|
|
312
|
+
_setup_logging(verbose=args.verbose)
|
|
313
|
+
|
|
314
|
+
# Build configuration
|
|
315
|
+
config = _build_config(args)
|
|
316
|
+
|
|
317
|
+
# Check if daemon is already running
|
|
318
|
+
pid = _read_pid_file(config.pid_path)
|
|
319
|
+
if pid is not None and _is_process_running(pid):
|
|
320
|
+
print(f"Error: Daemon already running with PID {pid}", file=sys.stderr)
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
|
|
323
|
+
# Daemonize if requested
|
|
324
|
+
if args.daemonize:
|
|
325
|
+
log_dir = config.spool_dir.parent
|
|
326
|
+
log_file = log_dir / "emit-daemon.log"
|
|
327
|
+
# Create log directory BEFORE daemonizing so errors are visible to user
|
|
328
|
+
try:
|
|
329
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
except OSError as e:
|
|
331
|
+
print(f"Error: Cannot create log directory: {e}", file=sys.stderr)
|
|
332
|
+
sys.exit(1)
|
|
333
|
+
print("Starting emit-daemon in background...")
|
|
334
|
+
print(f" Socket: {config.socket_path}")
|
|
335
|
+
print(f" PID file: {config.pid_path}")
|
|
336
|
+
print(f" Kafka: {config.kafka_bootstrap_servers}")
|
|
337
|
+
print(f" Log file: {log_file}")
|
|
338
|
+
_daemonize()
|
|
339
|
+
# NOTE: After daemonize, stdio is redirected to /dev/null.
|
|
340
|
+
# Startup errors will appear in the log file above.
|
|
341
|
+
# Clear any existing handlers to ensure reconfiguration works
|
|
342
|
+
# (logging.basicConfig() is a no-op if handlers already exist)
|
|
343
|
+
root_logger = logging.getLogger()
|
|
344
|
+
for handler in root_logger.handlers[:]:
|
|
345
|
+
root_logger.removeHandler(handler)
|
|
346
|
+
# Set up file-based logging (directory already exists from above)
|
|
347
|
+
logging.basicConfig(
|
|
348
|
+
level=logging.INFO,
|
|
349
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
350
|
+
filename=str(log_file),
|
|
351
|
+
filemode="a",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Run the daemon
|
|
355
|
+
async def run_daemon() -> None:
|
|
356
|
+
daemon = EmitDaemon(config)
|
|
357
|
+
try:
|
|
358
|
+
await daemon.start()
|
|
359
|
+
if not args.daemonize:
|
|
360
|
+
print(f"Emit daemon started (PID: {os.getpid()})")
|
|
361
|
+
print(f" Socket: {config.socket_path}")
|
|
362
|
+
print(f" Kafka: {config.kafka_bootstrap_servers}")
|
|
363
|
+
print("Press Ctrl+C to stop...")
|
|
364
|
+
await daemon.run_until_shutdown()
|
|
365
|
+
except OnexError as e:
|
|
366
|
+
if not args.daemonize:
|
|
367
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
368
|
+
logging.getLogger(__name__).exception("Daemon error")
|
|
369
|
+
sys.exit(1)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
asyncio.run(run_daemon())
|
|
373
|
+
except KeyboardInterrupt:
|
|
374
|
+
if not args.daemonize:
|
|
375
|
+
print("\nShutting down...")
|
|
376
|
+
sys.exit(0)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def cmd_stop(args: argparse.Namespace) -> None:
|
|
380
|
+
"""Handle the 'stop' command.
|
|
381
|
+
|
|
382
|
+
Stop the running emit daemon gracefully.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
args: Parsed CLI arguments.
|
|
386
|
+
"""
|
|
387
|
+
# Get PID file path (intentional /tmp default for daemon PID tracking)
|
|
388
|
+
pid_path = Path(getattr(args, "pid_path", None) or "/tmp/omniclaude-emit.pid") # noqa: S108
|
|
389
|
+
|
|
390
|
+
# If config file provided, use its pid_path
|
|
391
|
+
if args.config:
|
|
392
|
+
config_data = _load_config_file(Path(args.config))
|
|
393
|
+
if "pid_path" in config_data:
|
|
394
|
+
pid_path = Path(str(config_data["pid_path"]))
|
|
395
|
+
|
|
396
|
+
# Read PID
|
|
397
|
+
pid = _read_pid_file(pid_path)
|
|
398
|
+
if pid is None:
|
|
399
|
+
print("Daemon not running (no PID file)", file=sys.stderr)
|
|
400
|
+
sys.exit(1)
|
|
401
|
+
|
|
402
|
+
if not _is_process_running(pid):
|
|
403
|
+
print(f"Daemon not running (stale PID file for PID {pid})")
|
|
404
|
+
# Clean up stale PID file
|
|
405
|
+
try:
|
|
406
|
+
pid_path.unlink()
|
|
407
|
+
print(f"Removed stale PID file: {pid_path}")
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
sys.exit(0)
|
|
411
|
+
|
|
412
|
+
# Send SIGTERM for graceful shutdown
|
|
413
|
+
print(f"Stopping emit-daemon (PID: {pid})...")
|
|
414
|
+
try:
|
|
415
|
+
os.kill(pid, signal.SIGTERM)
|
|
416
|
+
except ProcessLookupError:
|
|
417
|
+
print("Daemon already stopped")
|
|
418
|
+
sys.exit(0)
|
|
419
|
+
except PermissionError:
|
|
420
|
+
print(f"Error: Permission denied to stop daemon (PID: {pid})", file=sys.stderr)
|
|
421
|
+
sys.exit(1)
|
|
422
|
+
|
|
423
|
+
# Wait for graceful shutdown
|
|
424
|
+
wait_time = DEFAULT_SHUTDOWN_WAIT
|
|
425
|
+
for _ in range(int(wait_time * 10)): # Check every 0.1s
|
|
426
|
+
if not _is_process_running(pid):
|
|
427
|
+
print("Daemon stopped successfully")
|
|
428
|
+
# Clean up PID file if daemon didn't
|
|
429
|
+
if pid_path.exists():
|
|
430
|
+
try:
|
|
431
|
+
pid_path.unlink()
|
|
432
|
+
except OSError:
|
|
433
|
+
pass
|
|
434
|
+
sys.exit(0)
|
|
435
|
+
time.sleep(0.1)
|
|
436
|
+
|
|
437
|
+
# Process didn't stop gracefully - warn user
|
|
438
|
+
print(
|
|
439
|
+
f"Warning: Daemon (PID: {pid}) did not stop within {wait_time}s",
|
|
440
|
+
file=sys.stderr,
|
|
441
|
+
)
|
|
442
|
+
print(f"You may need to force kill with: kill -9 {pid}", file=sys.stderr)
|
|
443
|
+
sys.exit(1)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
447
|
+
"""Handle the 'status' command.
|
|
448
|
+
|
|
449
|
+
Check daemon status via ping command.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
args: Parsed CLI arguments.
|
|
453
|
+
"""
|
|
454
|
+
# Get socket path (intentional /tmp default for daemon socket)
|
|
455
|
+
socket_path = Path(
|
|
456
|
+
getattr(args, "socket_path", None) or "/tmp/omniclaude-emit.sock" # noqa: S108
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# If config file provided, use its socket_path
|
|
460
|
+
if args.config:
|
|
461
|
+
config_data = _load_config_file(Path(args.config))
|
|
462
|
+
if "socket_path" in config_data:
|
|
463
|
+
socket_path = Path(str(config_data["socket_path"]))
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
response = _send_socket_message(socket_path, {"command": "ping"})
|
|
467
|
+
|
|
468
|
+
if response.get("status") == "ok":
|
|
469
|
+
print("Daemon is running")
|
|
470
|
+
print(f" Queue size (memory): {response.get('queue_size', 'unknown')}")
|
|
471
|
+
print(f" Queue size (spool): {response.get('spool_size', 'unknown')}")
|
|
472
|
+
|
|
473
|
+
# Print as JSON if requested
|
|
474
|
+
if args.json:
|
|
475
|
+
print(json.dumps(response, indent=2))
|
|
476
|
+
|
|
477
|
+
sys.exit(0)
|
|
478
|
+
else:
|
|
479
|
+
print(f"Daemon returned unexpected status: {response}", file=sys.stderr)
|
|
480
|
+
sys.exit(1)
|
|
481
|
+
|
|
482
|
+
except ConnectionError as e:
|
|
483
|
+
print(f"Daemon not running: {e}", file=sys.stderr)
|
|
484
|
+
sys.exit(1)
|
|
485
|
+
except TimeoutError as e:
|
|
486
|
+
print(f"Daemon not responding: {e}", file=sys.stderr)
|
|
487
|
+
sys.exit(1)
|
|
488
|
+
except OnexError as e:
|
|
489
|
+
print(f"Invalid response from daemon: {e}", file=sys.stderr)
|
|
490
|
+
sys.exit(1)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def cmd_emit(args: argparse.Namespace) -> None:
|
|
494
|
+
"""Handle the 'emit' command.
|
|
495
|
+
|
|
496
|
+
Emit a test event to the daemon.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
args: Parsed CLI arguments.
|
|
500
|
+
"""
|
|
501
|
+
# Get socket path (intentional /tmp default for daemon socket)
|
|
502
|
+
socket_path = Path(
|
|
503
|
+
getattr(args, "socket_path", None) or "/tmp/omniclaude-emit.sock" # noqa: S108
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# If config file provided, use its socket_path
|
|
507
|
+
if args.config:
|
|
508
|
+
config_data = _load_config_file(Path(args.config))
|
|
509
|
+
if "socket_path" in config_data:
|
|
510
|
+
socket_path = Path(str(config_data["socket_path"]))
|
|
511
|
+
|
|
512
|
+
# Parse payload JSON
|
|
513
|
+
try:
|
|
514
|
+
payload = json.loads(args.payload)
|
|
515
|
+
if not isinstance(payload, dict):
|
|
516
|
+
print("Error: Payload must be a JSON object", file=sys.stderr)
|
|
517
|
+
sys.exit(1)
|
|
518
|
+
except json.JSONDecodeError as e:
|
|
519
|
+
print(f"Error: Invalid JSON payload: {e}", file=sys.stderr)
|
|
520
|
+
sys.exit(1)
|
|
521
|
+
|
|
522
|
+
# Build event message
|
|
523
|
+
message: dict[str, object] = {
|
|
524
|
+
"event_type": args.event_type,
|
|
525
|
+
"payload": payload,
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
response = _send_socket_message(socket_path, message)
|
|
530
|
+
|
|
531
|
+
if response.get("status") == "queued":
|
|
532
|
+
event_id = response.get("event_id", "unknown")
|
|
533
|
+
print("Event queued successfully")
|
|
534
|
+
print(f" Event ID: {event_id}")
|
|
535
|
+
print(f" Type: {args.event_type}")
|
|
536
|
+
|
|
537
|
+
if args.json:
|
|
538
|
+
print(json.dumps(response, indent=2))
|
|
539
|
+
|
|
540
|
+
sys.exit(0)
|
|
541
|
+
elif response.get("status") == "error":
|
|
542
|
+
reason = response.get("reason", "Unknown error")
|
|
543
|
+
print(f"Error: {reason}", file=sys.stderr)
|
|
544
|
+
sys.exit(1)
|
|
545
|
+
else:
|
|
546
|
+
print(f"Unexpected response: {response}", file=sys.stderr)
|
|
547
|
+
sys.exit(1)
|
|
548
|
+
|
|
549
|
+
except ConnectionError as e:
|
|
550
|
+
print(f"Cannot connect to daemon: {e}", file=sys.stderr)
|
|
551
|
+
sys.exit(1)
|
|
552
|
+
except TimeoutError as e:
|
|
553
|
+
print(f"Daemon not responding: {e}", file=sys.stderr)
|
|
554
|
+
sys.exit(1)
|
|
555
|
+
except OnexError as e:
|
|
556
|
+
print(f"Invalid response from daemon: {e}", file=sys.stderr)
|
|
557
|
+
sys.exit(1)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def cmd_config(args: argparse.Namespace) -> None:
|
|
561
|
+
"""Handle the 'config' command.
|
|
562
|
+
|
|
563
|
+
Show resolved configuration.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
args: Parsed CLI arguments.
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
config = _build_config(args)
|
|
570
|
+
except SystemExit:
|
|
571
|
+
# If config building fails, show what we can
|
|
572
|
+
if args.config:
|
|
573
|
+
config_data = _load_config_file(Path(args.config))
|
|
574
|
+
print("# Configuration from file (not validated):")
|
|
575
|
+
print(yaml.dump(config_data, default_flow_style=False))
|
|
576
|
+
else:
|
|
577
|
+
print("# Default configuration:")
|
|
578
|
+
print("# (Requires kafka_bootstrap_servers to be set)")
|
|
579
|
+
print()
|
|
580
|
+
print("socket_path: /tmp/omniclaude-emit.sock")
|
|
581
|
+
print("pid_path: /tmp/omniclaude-emit.pid")
|
|
582
|
+
print("spool_dir: ~/.omniclaude/emit-spool")
|
|
583
|
+
print("kafka_bootstrap_servers: <REQUIRED>")
|
|
584
|
+
print("kafka_client_id: emit-daemon")
|
|
585
|
+
print("max_payload_bytes: 1048576")
|
|
586
|
+
print("max_memory_queue: 100")
|
|
587
|
+
print("max_spool_messages: 1000")
|
|
588
|
+
print("max_spool_bytes: 10485760")
|
|
589
|
+
print("socket_timeout_seconds: 5.0")
|
|
590
|
+
print("kafka_timeout_seconds: 30.0")
|
|
591
|
+
print("shutdown_drain_seconds: 10.0")
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
# Convert config to dict for YAML output
|
|
595
|
+
config_dict = {
|
|
596
|
+
"socket_path": str(config.socket_path),
|
|
597
|
+
"pid_path": str(config.pid_path),
|
|
598
|
+
"spool_dir": str(config.spool_dir),
|
|
599
|
+
"kafka_bootstrap_servers": config.kafka_bootstrap_servers,
|
|
600
|
+
"kafka_client_id": config.kafka_client_id,
|
|
601
|
+
"max_payload_bytes": config.max_payload_bytes,
|
|
602
|
+
"max_memory_queue": config.max_memory_queue,
|
|
603
|
+
"max_spool_messages": config.max_spool_messages,
|
|
604
|
+
"max_spool_bytes": config.max_spool_bytes,
|
|
605
|
+
"socket_timeout_seconds": config.socket_timeout_seconds,
|
|
606
|
+
"kafka_timeout_seconds": config.kafka_timeout_seconds,
|
|
607
|
+
"shutdown_drain_seconds": config.shutdown_drain_seconds,
|
|
608
|
+
"spooling_enabled": config.spooling_enabled,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
print("# Resolved configuration:")
|
|
612
|
+
print(yaml.dump(config_dict, default_flow_style=False))
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
616
|
+
"""Create the argument parser with all subcommands.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Configured argument parser.
|
|
620
|
+
"""
|
|
621
|
+
parser = argparse.ArgumentParser(
|
|
622
|
+
prog="emit-daemon",
|
|
623
|
+
description="Hook Event Emit Daemon - Persistent Kafka event emission for Claude Code hooks",
|
|
624
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
625
|
+
epilog="""
|
|
626
|
+
Examples:
|
|
627
|
+
# Show version
|
|
628
|
+
emit-daemon --version
|
|
629
|
+
|
|
630
|
+
# Start daemon in foreground
|
|
631
|
+
emit-daemon start --kafka-servers localhost:9092
|
|
632
|
+
|
|
633
|
+
# Start daemon in background
|
|
634
|
+
emit-daemon start --kafka-servers localhost:9092 --daemonize
|
|
635
|
+
|
|
636
|
+
# Start with config file
|
|
637
|
+
emit-daemon start --config /path/to/config.yaml
|
|
638
|
+
|
|
639
|
+
# Check daemon status
|
|
640
|
+
emit-daemon status
|
|
641
|
+
|
|
642
|
+
# Emit a test event
|
|
643
|
+
emit-daemon emit --event-type prompt.submitted --payload '{"session_id": "test"}'
|
|
644
|
+
|
|
645
|
+
# Stop the daemon
|
|
646
|
+
emit-daemon stop
|
|
647
|
+
|
|
648
|
+
# Show resolved configuration
|
|
649
|
+
emit-daemon config --config /path/to/config.yaml
|
|
650
|
+
""",
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
parser.add_argument(
|
|
654
|
+
"--version",
|
|
655
|
+
action="version",
|
|
656
|
+
version=f"%(prog)s {__version__}",
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
subparsers = parser.add_subparsers(
|
|
660
|
+
dest="command",
|
|
661
|
+
title="commands",
|
|
662
|
+
description="Available commands",
|
|
663
|
+
required=True,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# -------------------------------------------------------------------------
|
|
667
|
+
# start command
|
|
668
|
+
# -------------------------------------------------------------------------
|
|
669
|
+
start_parser = subparsers.add_parser(
|
|
670
|
+
"start",
|
|
671
|
+
help="Start the emit daemon",
|
|
672
|
+
description="Start the emit daemon in foreground or background mode.",
|
|
673
|
+
)
|
|
674
|
+
start_parser.add_argument(
|
|
675
|
+
"--config",
|
|
676
|
+
"-c",
|
|
677
|
+
metavar="FILE",
|
|
678
|
+
help="Path to YAML configuration file",
|
|
679
|
+
)
|
|
680
|
+
start_parser.add_argument(
|
|
681
|
+
"--kafka-servers",
|
|
682
|
+
"-k",
|
|
683
|
+
metavar="SERVERS",
|
|
684
|
+
help="Kafka bootstrap servers (e.g., localhost:9092). Overrides config file.",
|
|
685
|
+
)
|
|
686
|
+
start_parser.add_argument(
|
|
687
|
+
"--socket-path",
|
|
688
|
+
"-s",
|
|
689
|
+
metavar="PATH",
|
|
690
|
+
help="Unix socket path. Overrides config file.",
|
|
691
|
+
)
|
|
692
|
+
start_parser.add_argument(
|
|
693
|
+
"--pid-path",
|
|
694
|
+
"-p",
|
|
695
|
+
metavar="PATH",
|
|
696
|
+
help="PID file path. Overrides config file.",
|
|
697
|
+
)
|
|
698
|
+
start_parser.add_argument(
|
|
699
|
+
"--spool-dir",
|
|
700
|
+
metavar="DIR",
|
|
701
|
+
help="Spool directory path. Overrides config file.",
|
|
702
|
+
)
|
|
703
|
+
start_parser.add_argument(
|
|
704
|
+
"--daemonize",
|
|
705
|
+
"-d",
|
|
706
|
+
action="store_true",
|
|
707
|
+
help="Run daemon in background (fork and exit)",
|
|
708
|
+
)
|
|
709
|
+
start_parser.add_argument(
|
|
710
|
+
"--verbose",
|
|
711
|
+
"-v",
|
|
712
|
+
action="store_true",
|
|
713
|
+
help="Enable verbose (DEBUG) logging",
|
|
714
|
+
)
|
|
715
|
+
start_parser.set_defaults(func=cmd_start)
|
|
716
|
+
|
|
717
|
+
# -------------------------------------------------------------------------
|
|
718
|
+
# stop command
|
|
719
|
+
# -------------------------------------------------------------------------
|
|
720
|
+
stop_parser = subparsers.add_parser(
|
|
721
|
+
"stop",
|
|
722
|
+
help="Stop the running daemon",
|
|
723
|
+
description="Stop the running emit daemon gracefully.",
|
|
724
|
+
)
|
|
725
|
+
stop_parser.add_argument(
|
|
726
|
+
"--config",
|
|
727
|
+
"-c",
|
|
728
|
+
metavar="FILE",
|
|
729
|
+
help="Path to YAML configuration file (to find PID file)",
|
|
730
|
+
)
|
|
731
|
+
stop_parser.add_argument(
|
|
732
|
+
"--pid-path",
|
|
733
|
+
"-p",
|
|
734
|
+
metavar="PATH",
|
|
735
|
+
help="PID file path (default: /tmp/omniclaude-emit.pid)",
|
|
736
|
+
)
|
|
737
|
+
stop_parser.set_defaults(func=cmd_stop)
|
|
738
|
+
|
|
739
|
+
# -------------------------------------------------------------------------
|
|
740
|
+
# status command
|
|
741
|
+
# -------------------------------------------------------------------------
|
|
742
|
+
status_parser = subparsers.add_parser(
|
|
743
|
+
"status",
|
|
744
|
+
help="Check daemon status",
|
|
745
|
+
description="Check if the daemon is running and show queue metrics.",
|
|
746
|
+
)
|
|
747
|
+
status_parser.add_argument(
|
|
748
|
+
"--config",
|
|
749
|
+
"-c",
|
|
750
|
+
metavar="FILE",
|
|
751
|
+
help="Path to YAML configuration file (to find socket path)",
|
|
752
|
+
)
|
|
753
|
+
status_parser.add_argument(
|
|
754
|
+
"--socket-path",
|
|
755
|
+
"-s",
|
|
756
|
+
metavar="PATH",
|
|
757
|
+
help="Unix socket path (default: /tmp/omniclaude-emit.sock)",
|
|
758
|
+
)
|
|
759
|
+
status_parser.add_argument(
|
|
760
|
+
"--json",
|
|
761
|
+
"-j",
|
|
762
|
+
action="store_true",
|
|
763
|
+
help="Output full status as JSON",
|
|
764
|
+
)
|
|
765
|
+
status_parser.set_defaults(func=cmd_status)
|
|
766
|
+
|
|
767
|
+
# -------------------------------------------------------------------------
|
|
768
|
+
# emit command
|
|
769
|
+
# -------------------------------------------------------------------------
|
|
770
|
+
emit_parser = subparsers.add_parser(
|
|
771
|
+
"emit",
|
|
772
|
+
help="Emit a test event",
|
|
773
|
+
description="Emit an event to the daemon for publishing to Kafka.",
|
|
774
|
+
)
|
|
775
|
+
emit_parser.add_argument(
|
|
776
|
+
"--event-type",
|
|
777
|
+
"-t",
|
|
778
|
+
required=True,
|
|
779
|
+
metavar="TYPE",
|
|
780
|
+
help="Event type (e.g., prompt.submitted)",
|
|
781
|
+
)
|
|
782
|
+
emit_parser.add_argument(
|
|
783
|
+
"--payload",
|
|
784
|
+
"-p",
|
|
785
|
+
required=True,
|
|
786
|
+
metavar="JSON",
|
|
787
|
+
help='Event payload as JSON string (e.g., \'{"key": "value"}\')',
|
|
788
|
+
)
|
|
789
|
+
emit_parser.add_argument(
|
|
790
|
+
"--config",
|
|
791
|
+
"-c",
|
|
792
|
+
metavar="FILE",
|
|
793
|
+
help="Path to YAML configuration file (to find socket path)",
|
|
794
|
+
)
|
|
795
|
+
emit_parser.add_argument(
|
|
796
|
+
"--socket-path",
|
|
797
|
+
"-s",
|
|
798
|
+
metavar="PATH",
|
|
799
|
+
help="Unix socket path (default: /tmp/omniclaude-emit.sock)",
|
|
800
|
+
)
|
|
801
|
+
emit_parser.add_argument(
|
|
802
|
+
"--json",
|
|
803
|
+
"-j",
|
|
804
|
+
action="store_true",
|
|
805
|
+
help="Output full response as JSON",
|
|
806
|
+
)
|
|
807
|
+
emit_parser.set_defaults(func=cmd_emit)
|
|
808
|
+
|
|
809
|
+
# -------------------------------------------------------------------------
|
|
810
|
+
# config command
|
|
811
|
+
# -------------------------------------------------------------------------
|
|
812
|
+
config_parser = subparsers.add_parser(
|
|
813
|
+
"config",
|
|
814
|
+
help="Show configuration",
|
|
815
|
+
description="Show the resolved configuration as YAML.",
|
|
816
|
+
)
|
|
817
|
+
config_parser.add_argument(
|
|
818
|
+
"--config",
|
|
819
|
+
"-c",
|
|
820
|
+
metavar="FILE",
|
|
821
|
+
help="Path to YAML configuration file",
|
|
822
|
+
)
|
|
823
|
+
config_parser.add_argument(
|
|
824
|
+
"--kafka-servers",
|
|
825
|
+
"-k",
|
|
826
|
+
metavar="SERVERS",
|
|
827
|
+
help="Kafka bootstrap servers (required if no config file)",
|
|
828
|
+
)
|
|
829
|
+
config_parser.set_defaults(func=cmd_config)
|
|
830
|
+
|
|
831
|
+
return parser
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def main() -> None:
|
|
835
|
+
"""CLI entry point."""
|
|
836
|
+
parser = create_parser()
|
|
837
|
+
args = parser.parse_args()
|
|
838
|
+
|
|
839
|
+
# Call the appropriate command handler
|
|
840
|
+
args.func(args)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
if __name__ == "__main__":
|
|
844
|
+
main()
|