superkick 0.1.0

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 (199) hide show
  1. checksums.yaml +7 -0
  2. data/CLA.md +91 -0
  3. data/CLAUDE.md +2226 -0
  4. data/CONTRIBUTING.md +104 -0
  5. data/LICENSE +108 -0
  6. data/LICENSE-COMMERCIAL.md +39 -0
  7. data/PLAN.md +161 -0
  8. data/README.md +1155 -0
  9. data/exe/superkick +6 -0
  10. data/lib/superkick/agent/runtime.rb +82 -0
  11. data/lib/superkick/agent/runtimes/local.rb +74 -0
  12. data/lib/superkick/agent/runtimes.rb +4 -0
  13. data/lib/superkick/agent.rb +209 -0
  14. data/lib/superkick/agent_store.rb +85 -0
  15. data/lib/superkick/attach/client.rb +245 -0
  16. data/lib/superkick/attach/protocol.rb +71 -0
  17. data/lib/superkick/attach/server.rb +371 -0
  18. data/lib/superkick/budget_checker.rb +120 -0
  19. data/lib/superkick/buffer/client.rb +91 -0
  20. data/lib/superkick/buffer/server.rb +127 -0
  21. data/lib/superkick/cli/agent.rb +524 -0
  22. data/lib/superkick/cli/completion.rb +591 -0
  23. data/lib/superkick/cli/goal.rb +71 -0
  24. data/lib/superkick/cli/mcp.rb +34 -0
  25. data/lib/superkick/cli/monitor.rb +47 -0
  26. data/lib/superkick/cli/notifier.rb +39 -0
  27. data/lib/superkick/cli/repository.rb +46 -0
  28. data/lib/superkick/cli/server.rb +106 -0
  29. data/lib/superkick/cli/setup.rb +166 -0
  30. data/lib/superkick/cli/spawner.rb +85 -0
  31. data/lib/superkick/cli/team.rb +407 -0
  32. data/lib/superkick/cli.rb +175 -0
  33. data/lib/superkick/client_registry.rb +30 -0
  34. data/lib/superkick/configuration.rb +178 -0
  35. data/lib/superkick/connection.rb +56 -0
  36. data/lib/superkick/control/client.rb +78 -0
  37. data/lib/superkick/control/reply.rb +43 -0
  38. data/lib/superkick/control/server.rb +1271 -0
  39. data/lib/superkick/cost_accumulator.rb +53 -0
  40. data/lib/superkick/cost_extractor.rb +65 -0
  41. data/lib/superkick/cost_poller.rb +70 -0
  42. data/lib/superkick/driver/profile_source.rb +134 -0
  43. data/lib/superkick/driver.rb +179 -0
  44. data/lib/superkick/drivers/claude_code.rb +110 -0
  45. data/lib/superkick/drivers/codex.rb +57 -0
  46. data/lib/superkick/drivers/copilot.rb +75 -0
  47. data/lib/superkick/drivers/gemini.rb +86 -0
  48. data/lib/superkick/drivers/goose.rb +74 -0
  49. data/lib/superkick/drivers.rb +16 -0
  50. data/lib/superkick/drop.rb +80 -0
  51. data/lib/superkick/drops.rb +76 -0
  52. data/lib/superkick/environment_executor.rb +90 -0
  53. data/lib/superkick/goal.rb +95 -0
  54. data/lib/superkick/goals/agent_exit.rb +41 -0
  55. data/lib/superkick/goals/agent_signal.rb +42 -0
  56. data/lib/superkick/goals/command.rb +103 -0
  57. data/lib/superkick/history_buffer.rb +38 -0
  58. data/lib/superkick/hosted/attach/bridge.rb +52 -0
  59. data/lib/superkick/hosted/attach/client.rb +208 -0
  60. data/lib/superkick/hosted/attach/relay.rb +313 -0
  61. data/lib/superkick/hosted/attach/relay_store.rb +48 -0
  62. data/lib/superkick/hosted/bridge.rb +263 -0
  63. data/lib/superkick/hosted/buffer/bridge.rb +42 -0
  64. data/lib/superkick/hosted/buffer/client.rb +63 -0
  65. data/lib/superkick/hosted/buffer/relay.rb +126 -0
  66. data/lib/superkick/hosted/buffer/relay_store.rb +42 -0
  67. data/lib/superkick/hosted/control/client.rb +84 -0
  68. data/lib/superkick/hosted/mcp_proxy.rb +144 -0
  69. data/lib/superkick/inject_handler.rb +24 -0
  70. data/lib/superkick/injection_guard.rb +26 -0
  71. data/lib/superkick/injection_queue.rb +177 -0
  72. data/lib/superkick/injector.rb +65 -0
  73. data/lib/superkick/input_buffer.rb +171 -0
  74. data/lib/superkick/integrations/bugsnag/README.md +98 -0
  75. data/lib/superkick/integrations/bugsnag/spawner.rb +307 -0
  76. data/lib/superkick/integrations/bugsnag/templates/error_opened.liquid +17 -0
  77. data/lib/superkick/integrations/bugsnag.rb +7 -0
  78. data/lib/superkick/integrations/circleci/README.md +75 -0
  79. data/lib/superkick/integrations/circleci/monitor.rb +185 -0
  80. data/lib/superkick/integrations/circleci/probe.rb +36 -0
  81. data/lib/superkick/integrations/circleci/templates/ci_failure.liquid +8 -0
  82. data/lib/superkick/integrations/circleci/templates/ci_success.liquid +1 -0
  83. data/lib/superkick/integrations/circleci.rb +8 -0
  84. data/lib/superkick/integrations/datadog/README.md +253 -0
  85. data/lib/superkick/integrations/datadog/alert_goal.rb +94 -0
  86. data/lib/superkick/integrations/datadog/alert_monitor.rb +163 -0
  87. data/lib/superkick/integrations/datadog/alert_spawner.rb +201 -0
  88. data/lib/superkick/integrations/datadog/notification_templates/default.liquid +10 -0
  89. data/lib/superkick/integrations/datadog/notifier.rb +294 -0
  90. data/lib/superkick/integrations/datadog/spawner.rb +201 -0
  91. data/lib/superkick/integrations/datadog/templates/alert_changed.liquid +8 -0
  92. data/lib/superkick/integrations/datadog/templates/alert_escalated.liquid +8 -0
  93. data/lib/superkick/integrations/datadog/templates/alert_recovered.liquid +14 -0
  94. data/lib/superkick/integrations/datadog/templates/alert_triggered.liquid +29 -0
  95. data/lib/superkick/integrations/datadog/templates/error_opened.liquid +15 -0
  96. data/lib/superkick/integrations/datadog.rb +14 -0
  97. data/lib/superkick/integrations/docker/README.md +256 -0
  98. data/lib/superkick/integrations/docker/client.rb +295 -0
  99. data/lib/superkick/integrations/docker/runtime.rb +218 -0
  100. data/lib/superkick/integrations/docker.rb +4 -0
  101. data/lib/superkick/integrations/git/repository_source.rb +66 -0
  102. data/lib/superkick/integrations/git/version_control.rb +119 -0
  103. data/lib/superkick/integrations/git.rb +8 -0
  104. data/lib/superkick/integrations/github/README.md +300 -0
  105. data/lib/superkick/integrations/github/check_failed_spawner.rb +199 -0
  106. data/lib/superkick/integrations/github/drops.rb +114 -0
  107. data/lib/superkick/integrations/github/goal.rb +135 -0
  108. data/lib/superkick/integrations/github/issue_goal.rb +104 -0
  109. data/lib/superkick/integrations/github/issue_spawner.rb +160 -0
  110. data/lib/superkick/integrations/github/monitor.rb +251 -0
  111. data/lib/superkick/integrations/github/probe.rb +30 -0
  112. data/lib/superkick/integrations/github/repository_source.rb +228 -0
  113. data/lib/superkick/integrations/github/templates/check_failed.liquid +10 -0
  114. data/lib/superkick/integrations/github/templates/ci_failure.liquid +5 -0
  115. data/lib/superkick/integrations/github/templates/ci_success.liquid +1 -0
  116. data/lib/superkick/integrations/github/templates/issue_opened.liquid +20 -0
  117. data/lib/superkick/integrations/github/templates/pr_comment.liquid +2 -0
  118. data/lib/superkick/integrations/github/templates/pr_review.liquid +4 -0
  119. data/lib/superkick/integrations/github.rb +16 -0
  120. data/lib/superkick/integrations/honeybadger/README.md +97 -0
  121. data/lib/superkick/integrations/honeybadger/notification_templates/default.liquid +8 -0
  122. data/lib/superkick/integrations/honeybadger/notifier.rb +250 -0
  123. data/lib/superkick/integrations/honeybadger/spawner.rb +214 -0
  124. data/lib/superkick/integrations/honeybadger/templates/error_opened.liquid +17 -0
  125. data/lib/superkick/integrations/honeybadger.rb +9 -0
  126. data/lib/superkick/integrations/shell/README.md +83 -0
  127. data/lib/superkick/integrations/shell/monitor.rb +87 -0
  128. data/lib/superkick/integrations/shell/templates/shell_alert.liquid +6 -0
  129. data/lib/superkick/integrations/shell/templates/shell_success.liquid +6 -0
  130. data/lib/superkick/integrations/shell.rb +7 -0
  131. data/lib/superkick/integrations/shortcut/README.md +193 -0
  132. data/lib/superkick/integrations/shortcut/drops.rb +91 -0
  133. data/lib/superkick/integrations/shortcut/monitor.rb +582 -0
  134. data/lib/superkick/integrations/shortcut/probe.rb +34 -0
  135. data/lib/superkick/integrations/shortcut/spawner.rb +264 -0
  136. data/lib/superkick/integrations/shortcut/templates/related_story_changed.liquid +6 -0
  137. data/lib/superkick/integrations/shortcut/templates/story_blocker.liquid +8 -0
  138. data/lib/superkick/integrations/shortcut/templates/story_comment.liquid +5 -0
  139. data/lib/superkick/integrations/shortcut/templates/story_description_changed.liquid +19 -0
  140. data/lib/superkick/integrations/shortcut/templates/story_owner_changed.liquid +10 -0
  141. data/lib/superkick/integrations/shortcut/templates/story_ready.liquid +41 -0
  142. data/lib/superkick/integrations/shortcut/templates/story_state_changed.liquid +9 -0
  143. data/lib/superkick/integrations/shortcut/templates/story_unblocked.liquid +5 -0
  144. data/lib/superkick/integrations/shortcut.rb +11 -0
  145. data/lib/superkick/integrations/slack/README.md +297 -0
  146. data/lib/superkick/integrations/slack/drops.rb +70 -0
  147. data/lib/superkick/integrations/slack/notifier.rb +426 -0
  148. data/lib/superkick/integrations/slack/spawner.rb +251 -0
  149. data/lib/superkick/integrations/slack/templates/default.liquid +17 -0
  150. data/lib/superkick/integrations/slack/templates/slack_reply.liquid +3 -0
  151. data/lib/superkick/integrations/slack/templates/spawn/slack_message.liquid +10 -0
  152. data/lib/superkick/integrations/slack/thread_monitor.rb +161 -0
  153. data/lib/superkick/integrations/slack.rb +12 -0
  154. data/lib/superkick/liquid.rb +129 -0
  155. data/lib/superkick/local/repository_source.rb +148 -0
  156. data/lib/superkick/mcp_server.rb +596 -0
  157. data/lib/superkick/monitor.rb +215 -0
  158. data/lib/superkick/notification_dispatcher.rb +280 -0
  159. data/lib/superkick/notifier.rb +173 -0
  160. data/lib/superkick/notifier_state_store.rb +55 -0
  161. data/lib/superkick/notifier_template.rb +121 -0
  162. data/lib/superkick/notifiers/command.rb +124 -0
  163. data/lib/superkick/notifiers/terminal_bell.rb +41 -0
  164. data/lib/superkick/output_logger.rb +54 -0
  165. data/lib/superkick/poller.rb +126 -0
  166. data/lib/superkick/process_runner.rb +87 -0
  167. data/lib/superkick/pty_proxy.rb +403 -0
  168. data/lib/superkick/registry.rb +75 -0
  169. data/lib/superkick/repository_source.rb +195 -0
  170. data/lib/superkick/server.rb +211 -0
  171. data/lib/superkick/session_recorder.rb +154 -0
  172. data/lib/superkick/setup.rb +160 -0
  173. data/lib/superkick/spawn/agent_spawner.rb +311 -0
  174. data/lib/superkick/spawn/approval_store.rb +113 -0
  175. data/lib/superkick/spawn/handler.rb +144 -0
  176. data/lib/superkick/spawn/injector.rb +119 -0
  177. data/lib/superkick/spawn/workflow_executor.rb +196 -0
  178. data/lib/superkick/spawn/workflow_validator.rb +77 -0
  179. data/lib/superkick/spawner.rb +67 -0
  180. data/lib/superkick/supervisor.rb +516 -0
  181. data/lib/superkick/team/artifact_store.rb +92 -0
  182. data/lib/superkick/team/log.rb +140 -0
  183. data/lib/superkick/team/log_entry_drop.rb +34 -0
  184. data/lib/superkick/team/log_monitor.rb +84 -0
  185. data/lib/superkick/team/log_notifier.rb +96 -0
  186. data/lib/superkick/team/log_store.rb +40 -0
  187. data/lib/superkick/template_filters.rb +24 -0
  188. data/lib/superkick/template_renderer.rb +223 -0
  189. data/lib/superkick/templates/team_log/planning_agent.liquid +38 -0
  190. data/lib/superkick/templates/team_log/team_digest.liquid +45 -0
  191. data/lib/superkick/templates/team_log/teammate_message.liquid +7 -0
  192. data/lib/superkick/templates/team_log/worker_kickoff.liquid +37 -0
  193. data/lib/superkick/templates/workflow/workflow_triggered.liquid +22 -0
  194. data/lib/superkick/version.rb +5 -0
  195. data/lib/superkick/version_control.rb +135 -0
  196. data/lib/superkick/yaml_config.rb +302 -0
  197. data/lib/superkick.rb +198 -0
  198. data/plan.md +267 -0
  199. metadata +404 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ class Configuration
5
+ attr_accessor :base_dir,
6
+ :poll_interval,
7
+ :idle_threshold,
8
+ :inject_clear_delay,
9
+ :rate_limit_backoff,
10
+ :error_backoff,
11
+ :log_level,
12
+ :monitors,
13
+ :privileged_types,
14
+ :notifications,
15
+ :notification_privileged_types,
16
+ :spawners,
17
+ :attach_history_size,
18
+ :attach_escape_key,
19
+ :attach_rw_idle_timeout,
20
+ :attach_replay_buffer_size,
21
+ :attach_max_connections,
22
+ :budget,
23
+ :cost_poll_interval,
24
+ :cost_stale_after,
25
+ :repositories,
26
+ :max_workers_per_team,
27
+ :server,
28
+ :driver_profiles,
29
+ :runtime,
30
+ :session_recording_enabled,
31
+ :session_recording_max_size
32
+
33
+ attr_reader :context_documents
34
+
35
+ def context_documents=(value)
36
+ @context_documents = value
37
+ @resolved_context_document_patterns = nil
38
+ end
39
+
40
+ def initialize
41
+ @base_dir = ENV.fetch("SUPERKICK_DIR", File.join(Dir.home, ".superkick"))
42
+ @poll_interval = 30
43
+ @idle_threshold = 5.0
44
+ @inject_clear_delay = 0.15
45
+ @rate_limit_backoff = 60
46
+ @error_backoff = 10
47
+ @log_level = :info
48
+ @monitors = {}
49
+ @privileged_types = []
50
+ @notifications = []
51
+ @notification_privileged_types = []
52
+ @spawners = {}
53
+ @attach_history_size = 100 * 1024 # 100 KB
54
+ @attach_escape_key = "\x01" # Ctrl-A
55
+ @attach_rw_idle_timeout = 300 # 5 minutes
56
+ @attach_replay_buffer_size = 65_536 # 64 KB (relay-side history for remote attach)
57
+ @attach_max_connections = 10 # max concurrent user connections per agent at relay
58
+ @budget = {}
59
+ @cost_poll_interval = nil
60
+ @cost_stale_after = nil
61
+ @repositories = {}
62
+ @context_documents = []
63
+ @max_workers_per_team = 5
64
+ @server = {}
65
+ @driver_profiles = {}
66
+ @runtime = {}
67
+ @session_recording_enabled = true
68
+ @session_recording_max_size = 100 * 1024 * 1024 # 100 MB
69
+ end
70
+
71
+ # Resolved server type from server config.
72
+ # Inferred from the presence of :url when not explicitly set.
73
+ def server_type
74
+ explicit = @server[:type]&.to_sym
75
+ return explicit if explicit
76
+
77
+ @server[:url] ? :hosted : :local
78
+ end
79
+
80
+ # Is the named monitor instance privileged?
81
+ # Checks the per-monitor "privileged" flag in the monitors config.
82
+ def monitor_privileged?(name)
83
+ config = @monitors[name.to_sym]
84
+ config.is_a?(Hash) && config[:privileged] == true
85
+ end
86
+
87
+ # Is the given monitor type privileged?
88
+ def type_privileged?(type)
89
+ @privileged_types.include?(type.to_sym)
90
+ end
91
+
92
+ # Is the named notifier instance privileged?
93
+ # Checks the per-notifier "privileged" flag in the notifications config.
94
+ def notifier_privileged?(name)
95
+ @notifications.any? { |n| n.is_a?(Hash) && n[:name]&.to_sym == name.to_sym && n[:privileged] == true }
96
+ end
97
+
98
+ # Is the given notifier type privileged?
99
+ def notifier_type_privileged?(type)
100
+ @notification_privileged_types.include?(type.to_sym)
101
+ end
102
+
103
+ # Filesystem paths — all derived from base_dir.
104
+
105
+ def run_dir = File.join(base_dir, "run")
106
+ def socket_path = File.join(run_dir, "server.sock")
107
+ def pid_path = File.join(run_dir, "server.pid")
108
+ def log_path = File.join(base_dir, "superkick.log")
109
+ def agents_dir = File.join(base_dir, "sessions")
110
+ def templates_dir = File.join(base_dir, "templates")
111
+ def output_logs_dir = File.join(base_dir, "logs")
112
+ def recordings_dir = File.join(base_dir, "recordings")
113
+
114
+ def teams_dir = File.join(base_dir, "teams")
115
+ def workspaces_dir = File.join(base_dir, "workspaces")
116
+
117
+ attr_writer :repository_source
118
+
119
+ def repository_source
120
+ @repository_source ||= RepositorySource.build(@repositories)
121
+ end
122
+
123
+ # Resolve the global context_documents config into a { name => pattern } hash.
124
+ # Accepts bare strings/symbols (resolved from Repository::CONTEXT_DOCUMENT_PATTERNS)
125
+ # and custom entries with name: + pattern: keys.
126
+ def resolved_context_document_patterns
127
+ @resolved_context_document_patterns ||= resolve_context_document_config(@context_documents)
128
+ end
129
+
130
+ def driver_profile_source
131
+ @driver_profile_source ||= Driver::ProfileSource.build(@driver_profiles)
132
+ end
133
+
134
+ def agent_runtime
135
+ @agent_runtime ||= build_agent_runtime
136
+ end
137
+
138
+ private
139
+
140
+ def resolve_context_document_config(config)
141
+ return {} unless config.is_a?(Array) && config.any?
142
+
143
+ config.each_with_object({}) do |entry, h|
144
+ if entry.is_a?(Hash) && entry[:name] && entry[:pattern]
145
+ h[entry[:name].to_sym] = entry[:pattern].to_s
146
+ elsif entry.is_a?(String) || entry.is_a?(Symbol)
147
+ name = entry.to_sym
148
+ pattern = Repository::CONTEXT_DOCUMENT_PATTERNS[name]
149
+ h[name] = pattern if pattern
150
+ end
151
+ end
152
+ end
153
+
154
+ def build_agent_runtime
155
+ type = @runtime[:type]&.to_sym || :local
156
+ klass = Agent::Runtime.lookup(type)
157
+ runtime_config = @runtime[type] || {}
158
+ runtime_config = runtime_config.merge(server: build_runtime_server_context)
159
+ klass.new(**runtime_config)
160
+ end
161
+
162
+ def build_runtime_server_context
163
+ ctx = {type: server_type}
164
+ ctx[:base_dir] = base_dir if server_type == :local
165
+ ctx
166
+ end
167
+
168
+ public
169
+
170
+ def buffer_socket_path(agent_id)
171
+ File.join(run_dir, "buffer-#{agent_id}.sock")
172
+ end
173
+
174
+ def attach_socket_path(agent_id)
175
+ File.join(run_dir, "attach-#{agent_id}.sock")
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Superkick
6
+ # Bidirectional connection wrapper for Unix domain sockets.
7
+ # Adds newline-delimited JSON framing on top of a raw socket.
8
+ # Used by both the control plane (Control::Server, Control::Client)
9
+ # and the data plane (Buffer::Server, Buffer::Client, etc.).
10
+ # The underlying socket is managed by the caller — Connection does not own
11
+ # the socket lifecycle, it just closes it when asked.
12
+ class Connection
13
+ # Open a fresh UNIXSocket to +path+ and wrap it.
14
+ def self.open(path)
15
+ new(UNIXSocket.new(path))
16
+ end
17
+
18
+ def initialize(socket)
19
+ @socket = socket
20
+ end
21
+
22
+ # Encode +hash+ as newline-delimited JSON and write it to the socket.
23
+ # Used by clients to send requests.
24
+ def send_message(hash)
25
+ @socket.write("#{JSON.generate(hash)}\n")
26
+ @socket.flush
27
+ end
28
+
29
+ # Send a successful response. +payload+ is merged into { ok: true }.
30
+ def reply(payload = {})
31
+ @socket.write("#{JSON.generate({ok: true}.merge(payload))}\n")
32
+ @socket.flush
33
+ end
34
+
35
+ # Send an error response.
36
+ def error(message)
37
+ @socket.write("#{JSON.generate({ok: false, error: message})}\n")
38
+ @socket.flush
39
+ end
40
+
41
+ # Read one line from the socket and decode it. Returns nil if the peer
42
+ # closed the connection before sending a complete line.
43
+ def receive_message
44
+ line = @socket.gets
45
+ return nil unless line
46
+
47
+ JSON.parse(line.chomp, symbolize_names: true)
48
+ end
49
+
50
+ def close
51
+ @socket.close
52
+ rescue IOError, Errno::EBADF, Errno::ENOTCONN
53
+ nil
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Superkick
6
+ module Control
7
+ @clients = {}
8
+ extend ClientRegistry
9
+
10
+ # Control client for communicating with the Superkick server.
11
+ #
12
+ # The default implementation communicates over a Unix socket. In hosted
13
+ # mode, a subclass (Hosted::Control::Client) communicates over HTTPS.
14
+ #
15
+ # All call sites use Control.client_from to construct the right
16
+ # implementation for the configured server type.
17
+ class Client
18
+ # Shared exception that callers rescue regardless of server type.
19
+ class ServerUnavailable < StandardError; end
20
+
21
+ # Raised when the hosted server rejects the API key (HTTP 401).
22
+ class AuthenticationError < ServerUnavailable; end
23
+
24
+ def self.from(config: Superkick.config, **_kwargs)
25
+ new(socket_path: config.socket_path)
26
+ end
27
+
28
+ TIMEOUT = 1 # seconds
29
+
30
+ def initialize(socket_path: Superkick.config.socket_path)
31
+ @socket_path = socket_path
32
+ end
33
+
34
+ # Send a command to the server and return the wrapped response.
35
+ # @param command [String] control command name
36
+ # @param params [Hash] extra key/value params merged into the request
37
+ # @return [Control::Reply] wrapped response from the server
38
+ def request(command, **params)
39
+ payload = {command: command}.merge(params)
40
+ connection = Superkick::Connection.new(connect)
41
+
42
+ begin
43
+ connection.send_message(payload)
44
+ raw = connection.receive_message
45
+ raise ServerUnavailable, "Server closed connection before responding" unless raw
46
+
47
+ Reply.new(raw)
48
+ ensure
49
+ connection.close
50
+ end
51
+ rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE
52
+ raise ServerUnavailable, "Server socket not found at #{@socket_path}"
53
+ end
54
+
55
+ # Returns true if the server is reachable.
56
+ def alive?
57
+ request("ping").success?
58
+ rescue ServerUnavailable
59
+ false
60
+ end
61
+
62
+ # No-op — each request opens and closes its own connection.
63
+ def close
64
+ end
65
+
66
+ private
67
+
68
+ def connect
69
+ sock = UNIXSocket.new(@socket_path)
70
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO,
71
+ [TIMEOUT, 0].pack("l_2"))
72
+ sock
73
+ rescue Errno::ENOENT
74
+ raise ServerUnavailable, "Server socket not found: #{@socket_path}"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ module Control
5
+ # Wraps a parsed control response hash to provide a typed interface.
6
+ # Returned by Control::Client#request so callers don't have to poke at
7
+ # raw hash keys to check success or read the error message.
8
+ #
9
+ # The envelope fields (:ok, :error) are handled by the convenience
10
+ # methods. All other payload fields are accessible via [].
11
+ class Reply
12
+ def initialize(hash)
13
+ @hash = hash
14
+ end
15
+
16
+ # True when the server replied with ok: true.
17
+ def success?
18
+ @hash[:ok] == true
19
+ end
20
+
21
+ # True when the server replied with ok: false (or ok is absent).
22
+ def error?
23
+ !success?
24
+ end
25
+
26
+ # The error string from an error response; nil on success.
27
+ def error_message
28
+ @hash[:error]
29
+ end
30
+
31
+ # Access a payload field by key, just like a Hash.
32
+ def [](key)
33
+ @hash[key]
34
+ end
35
+
36
+ # Returns the payload without the envelope fields (:ok, :error).
37
+ # Useful when forwarding the response to another layer.
38
+ def payload
39
+ @hash.except(:ok, :error)
40
+ end
41
+ end
42
+ end
43
+ end