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.
- checksums.yaml +7 -0
- data/CLA.md +91 -0
- data/CLAUDE.md +2226 -0
- data/CONTRIBUTING.md +104 -0
- data/LICENSE +108 -0
- data/LICENSE-COMMERCIAL.md +39 -0
- data/PLAN.md +161 -0
- data/README.md +1155 -0
- data/exe/superkick +6 -0
- data/lib/superkick/agent/runtime.rb +82 -0
- data/lib/superkick/agent/runtimes/local.rb +74 -0
- data/lib/superkick/agent/runtimes.rb +4 -0
- data/lib/superkick/agent.rb +209 -0
- data/lib/superkick/agent_store.rb +85 -0
- data/lib/superkick/attach/client.rb +245 -0
- data/lib/superkick/attach/protocol.rb +71 -0
- data/lib/superkick/attach/server.rb +371 -0
- data/lib/superkick/budget_checker.rb +120 -0
- data/lib/superkick/buffer/client.rb +91 -0
- data/lib/superkick/buffer/server.rb +127 -0
- data/lib/superkick/cli/agent.rb +524 -0
- data/lib/superkick/cli/completion.rb +591 -0
- data/lib/superkick/cli/goal.rb +71 -0
- data/lib/superkick/cli/mcp.rb +34 -0
- data/lib/superkick/cli/monitor.rb +47 -0
- data/lib/superkick/cli/notifier.rb +39 -0
- data/lib/superkick/cli/repository.rb +46 -0
- data/lib/superkick/cli/server.rb +106 -0
- data/lib/superkick/cli/setup.rb +166 -0
- data/lib/superkick/cli/spawner.rb +85 -0
- data/lib/superkick/cli/team.rb +407 -0
- data/lib/superkick/cli.rb +175 -0
- data/lib/superkick/client_registry.rb +30 -0
- data/lib/superkick/configuration.rb +178 -0
- data/lib/superkick/connection.rb +56 -0
- data/lib/superkick/control/client.rb +78 -0
- data/lib/superkick/control/reply.rb +43 -0
- data/lib/superkick/control/server.rb +1271 -0
- data/lib/superkick/cost_accumulator.rb +53 -0
- data/lib/superkick/cost_extractor.rb +65 -0
- data/lib/superkick/cost_poller.rb +70 -0
- data/lib/superkick/driver/profile_source.rb +134 -0
- data/lib/superkick/driver.rb +179 -0
- data/lib/superkick/drivers/claude_code.rb +110 -0
- data/lib/superkick/drivers/codex.rb +57 -0
- data/lib/superkick/drivers/copilot.rb +75 -0
- data/lib/superkick/drivers/gemini.rb +86 -0
- data/lib/superkick/drivers/goose.rb +74 -0
- data/lib/superkick/drivers.rb +16 -0
- data/lib/superkick/drop.rb +80 -0
- data/lib/superkick/drops.rb +76 -0
- data/lib/superkick/environment_executor.rb +90 -0
- data/lib/superkick/goal.rb +95 -0
- data/lib/superkick/goals/agent_exit.rb +41 -0
- data/lib/superkick/goals/agent_signal.rb +42 -0
- data/lib/superkick/goals/command.rb +103 -0
- data/lib/superkick/history_buffer.rb +38 -0
- data/lib/superkick/hosted/attach/bridge.rb +52 -0
- data/lib/superkick/hosted/attach/client.rb +208 -0
- data/lib/superkick/hosted/attach/relay.rb +313 -0
- data/lib/superkick/hosted/attach/relay_store.rb +48 -0
- data/lib/superkick/hosted/bridge.rb +263 -0
- data/lib/superkick/hosted/buffer/bridge.rb +42 -0
- data/lib/superkick/hosted/buffer/client.rb +63 -0
- data/lib/superkick/hosted/buffer/relay.rb +126 -0
- data/lib/superkick/hosted/buffer/relay_store.rb +42 -0
- data/lib/superkick/hosted/control/client.rb +84 -0
- data/lib/superkick/hosted/mcp_proxy.rb +144 -0
- data/lib/superkick/inject_handler.rb +24 -0
- data/lib/superkick/injection_guard.rb +26 -0
- data/lib/superkick/injection_queue.rb +177 -0
- data/lib/superkick/injector.rb +65 -0
- data/lib/superkick/input_buffer.rb +171 -0
- data/lib/superkick/integrations/bugsnag/README.md +98 -0
- data/lib/superkick/integrations/bugsnag/spawner.rb +307 -0
- data/lib/superkick/integrations/bugsnag/templates/error_opened.liquid +17 -0
- data/lib/superkick/integrations/bugsnag.rb +7 -0
- data/lib/superkick/integrations/circleci/README.md +75 -0
- data/lib/superkick/integrations/circleci/monitor.rb +185 -0
- data/lib/superkick/integrations/circleci/probe.rb +36 -0
- data/lib/superkick/integrations/circleci/templates/ci_failure.liquid +8 -0
- data/lib/superkick/integrations/circleci/templates/ci_success.liquid +1 -0
- data/lib/superkick/integrations/circleci.rb +8 -0
- data/lib/superkick/integrations/datadog/README.md +253 -0
- data/lib/superkick/integrations/datadog/alert_goal.rb +94 -0
- data/lib/superkick/integrations/datadog/alert_monitor.rb +163 -0
- data/lib/superkick/integrations/datadog/alert_spawner.rb +201 -0
- data/lib/superkick/integrations/datadog/notification_templates/default.liquid +10 -0
- data/lib/superkick/integrations/datadog/notifier.rb +294 -0
- data/lib/superkick/integrations/datadog/spawner.rb +201 -0
- data/lib/superkick/integrations/datadog/templates/alert_changed.liquid +8 -0
- data/lib/superkick/integrations/datadog/templates/alert_escalated.liquid +8 -0
- data/lib/superkick/integrations/datadog/templates/alert_recovered.liquid +14 -0
- data/lib/superkick/integrations/datadog/templates/alert_triggered.liquid +29 -0
- data/lib/superkick/integrations/datadog/templates/error_opened.liquid +15 -0
- data/lib/superkick/integrations/datadog.rb +14 -0
- data/lib/superkick/integrations/docker/README.md +256 -0
- data/lib/superkick/integrations/docker/client.rb +295 -0
- data/lib/superkick/integrations/docker/runtime.rb +218 -0
- data/lib/superkick/integrations/docker.rb +4 -0
- data/lib/superkick/integrations/git/repository_source.rb +66 -0
- data/lib/superkick/integrations/git/version_control.rb +119 -0
- data/lib/superkick/integrations/git.rb +8 -0
- data/lib/superkick/integrations/github/README.md +300 -0
- data/lib/superkick/integrations/github/check_failed_spawner.rb +199 -0
- data/lib/superkick/integrations/github/drops.rb +114 -0
- data/lib/superkick/integrations/github/goal.rb +135 -0
- data/lib/superkick/integrations/github/issue_goal.rb +104 -0
- data/lib/superkick/integrations/github/issue_spawner.rb +160 -0
- data/lib/superkick/integrations/github/monitor.rb +251 -0
- data/lib/superkick/integrations/github/probe.rb +30 -0
- data/lib/superkick/integrations/github/repository_source.rb +228 -0
- data/lib/superkick/integrations/github/templates/check_failed.liquid +10 -0
- data/lib/superkick/integrations/github/templates/ci_failure.liquid +5 -0
- data/lib/superkick/integrations/github/templates/ci_success.liquid +1 -0
- data/lib/superkick/integrations/github/templates/issue_opened.liquid +20 -0
- data/lib/superkick/integrations/github/templates/pr_comment.liquid +2 -0
- data/lib/superkick/integrations/github/templates/pr_review.liquid +4 -0
- data/lib/superkick/integrations/github.rb +16 -0
- data/lib/superkick/integrations/honeybadger/README.md +97 -0
- data/lib/superkick/integrations/honeybadger/notification_templates/default.liquid +8 -0
- data/lib/superkick/integrations/honeybadger/notifier.rb +250 -0
- data/lib/superkick/integrations/honeybadger/spawner.rb +214 -0
- data/lib/superkick/integrations/honeybadger/templates/error_opened.liquid +17 -0
- data/lib/superkick/integrations/honeybadger.rb +9 -0
- data/lib/superkick/integrations/shell/README.md +83 -0
- data/lib/superkick/integrations/shell/monitor.rb +87 -0
- data/lib/superkick/integrations/shell/templates/shell_alert.liquid +6 -0
- data/lib/superkick/integrations/shell/templates/shell_success.liquid +6 -0
- data/lib/superkick/integrations/shell.rb +7 -0
- data/lib/superkick/integrations/shortcut/README.md +193 -0
- data/lib/superkick/integrations/shortcut/drops.rb +91 -0
- data/lib/superkick/integrations/shortcut/monitor.rb +582 -0
- data/lib/superkick/integrations/shortcut/probe.rb +34 -0
- data/lib/superkick/integrations/shortcut/spawner.rb +264 -0
- data/lib/superkick/integrations/shortcut/templates/related_story_changed.liquid +6 -0
- data/lib/superkick/integrations/shortcut/templates/story_blocker.liquid +8 -0
- data/lib/superkick/integrations/shortcut/templates/story_comment.liquid +5 -0
- data/lib/superkick/integrations/shortcut/templates/story_description_changed.liquid +19 -0
- data/lib/superkick/integrations/shortcut/templates/story_owner_changed.liquid +10 -0
- data/lib/superkick/integrations/shortcut/templates/story_ready.liquid +41 -0
- data/lib/superkick/integrations/shortcut/templates/story_state_changed.liquid +9 -0
- data/lib/superkick/integrations/shortcut/templates/story_unblocked.liquid +5 -0
- data/lib/superkick/integrations/shortcut.rb +11 -0
- data/lib/superkick/integrations/slack/README.md +297 -0
- data/lib/superkick/integrations/slack/drops.rb +70 -0
- data/lib/superkick/integrations/slack/notifier.rb +426 -0
- data/lib/superkick/integrations/slack/spawner.rb +251 -0
- data/lib/superkick/integrations/slack/templates/default.liquid +17 -0
- data/lib/superkick/integrations/slack/templates/slack_reply.liquid +3 -0
- data/lib/superkick/integrations/slack/templates/spawn/slack_message.liquid +10 -0
- data/lib/superkick/integrations/slack/thread_monitor.rb +161 -0
- data/lib/superkick/integrations/slack.rb +12 -0
- data/lib/superkick/liquid.rb +129 -0
- data/lib/superkick/local/repository_source.rb +148 -0
- data/lib/superkick/mcp_server.rb +596 -0
- data/lib/superkick/monitor.rb +215 -0
- data/lib/superkick/notification_dispatcher.rb +280 -0
- data/lib/superkick/notifier.rb +173 -0
- data/lib/superkick/notifier_state_store.rb +55 -0
- data/lib/superkick/notifier_template.rb +121 -0
- data/lib/superkick/notifiers/command.rb +124 -0
- data/lib/superkick/notifiers/terminal_bell.rb +41 -0
- data/lib/superkick/output_logger.rb +54 -0
- data/lib/superkick/poller.rb +126 -0
- data/lib/superkick/process_runner.rb +87 -0
- data/lib/superkick/pty_proxy.rb +403 -0
- data/lib/superkick/registry.rb +75 -0
- data/lib/superkick/repository_source.rb +195 -0
- data/lib/superkick/server.rb +211 -0
- data/lib/superkick/session_recorder.rb +154 -0
- data/lib/superkick/setup.rb +160 -0
- data/lib/superkick/spawn/agent_spawner.rb +311 -0
- data/lib/superkick/spawn/approval_store.rb +113 -0
- data/lib/superkick/spawn/handler.rb +144 -0
- data/lib/superkick/spawn/injector.rb +119 -0
- data/lib/superkick/spawn/workflow_executor.rb +196 -0
- data/lib/superkick/spawn/workflow_validator.rb +77 -0
- data/lib/superkick/spawner.rb +67 -0
- data/lib/superkick/supervisor.rb +516 -0
- data/lib/superkick/team/artifact_store.rb +92 -0
- data/lib/superkick/team/log.rb +140 -0
- data/lib/superkick/team/log_entry_drop.rb +34 -0
- data/lib/superkick/team/log_monitor.rb +84 -0
- data/lib/superkick/team/log_notifier.rb +96 -0
- data/lib/superkick/team/log_store.rb +40 -0
- data/lib/superkick/template_filters.rb +24 -0
- data/lib/superkick/template_renderer.rb +223 -0
- data/lib/superkick/templates/team_log/planning_agent.liquid +38 -0
- data/lib/superkick/templates/team_log/team_digest.liquid +45 -0
- data/lib/superkick/templates/team_log/teammate_message.liquid +7 -0
- data/lib/superkick/templates/team_log/worker_kickoff.liquid +37 -0
- data/lib/superkick/templates/workflow/workflow_triggered.liquid +22 -0
- data/lib/superkick/version.rb +5 -0
- data/lib/superkick/version_control.rb +135 -0
- data/lib/superkick/yaml_config.rb +302 -0
- data/lib/superkick.rb +198 -0
- data/plan.md +267 -0
- 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
|