openclacky 1.0.0 → 1.0.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +87 -53
  4. data/lib/clacky/agent/cost_tracker.rb +19 -2
  5. data/lib/clacky/agent/llm_caller.rb +218 -0
  6. data/lib/clacky/agent/message_compressor_helper.rb +32 -2
  7. data/lib/clacky/agent.rb +54 -22
  8. data/lib/clacky/client.rb +44 -5
  9. data/lib/clacky/default_parsers/pdf_parser.rb +58 -17
  10. data/lib/clacky/default_parsers/pdf_parser_ocr.py +103 -0
  11. data/lib/clacky/default_parsers/pdf_parser_plumber.py +62 -0
  12. data/lib/clacky/default_skills/deploy/SKILL.md +201 -77
  13. data/lib/clacky/default_skills/new/SKILL.md +3 -114
  14. data/lib/clacky/default_skills/onboard/SKILL.md +349 -133
  15. data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +371 -0
  16. data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +175 -0
  17. data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +59 -26
  18. data/lib/clacky/message_format/anthropic.rb +72 -8
  19. data/lib/clacky/message_format/bedrock.rb +6 -3
  20. data/lib/clacky/providers.rb +146 -3
  21. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +14 -0
  22. data/lib/clacky/server/channel/adapters/feishu/bot.rb +10 -0
  23. data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +1 -0
  24. data/lib/clacky/server/channel/channel_manager.rb +12 -4
  25. data/lib/clacky/server/channel/channel_ui_controller.rb +8 -2
  26. data/lib/clacky/server/http_server.rb +746 -13
  27. data/lib/clacky/server/session_registry.rb +55 -24
  28. data/lib/clacky/skill.rb +10 -9
  29. data/lib/clacky/skill_loader.rb +23 -11
  30. data/lib/clacky/tools/file_reader.rb +232 -127
  31. data/lib/clacky/tools/security.rb +42 -64
  32. data/lib/clacky/tools/terminal/persistent_session.rb +15 -4
  33. data/lib/clacky/tools/terminal/safe_rm.sh +106 -0
  34. data/lib/clacky/tools/terminal/session_manager.rb +8 -3
  35. data/lib/clacky/tools/terminal.rb +263 -16
  36. data/lib/clacky/ui2/layout_manager.rb +8 -1
  37. data/lib/clacky/ui2/output_buffer.rb +83 -23
  38. data/lib/clacky/ui2/ui_controller.rb +74 -7
  39. data/lib/clacky/utils/file_processor.rb +14 -40
  40. data/lib/clacky/utils/model_pricing.rb +215 -0
  41. data/lib/clacky/utils/parser_manager.rb +70 -6
  42. data/lib/clacky/utils/string_matcher.rb +23 -1
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky/web/app.css +673 -9
  45. data/lib/clacky/web/app.js +40 -1608
  46. data/lib/clacky/web/i18n.js +209 -0
  47. data/lib/clacky/web/index.html +166 -2
  48. data/lib/clacky/web/onboard.js +77 -1
  49. data/lib/clacky/web/profile.js +442 -0
  50. data/lib/clacky/web/sessions.js +1034 -2
  51. data/lib/clacky/web/settings.js +127 -6
  52. data/lib/clacky/web/sidebar.js +39 -0
  53. data/lib/clacky/web/skills.js +460 -0
  54. data/lib/clacky/web/trash.js +343 -0
  55. data/lib/clacky/web/ws-dispatcher.js +255 -0
  56. data/lib/clacky.rb +5 -3
  57. metadata +16 -17
  58. data/lib/clacky/clacky_auth_client.rb +0 -152
  59. data/lib/clacky/clacky_cloud_config.rb +0 -123
  60. data/lib/clacky/cloud_project_client.rb +0 -169
  61. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +0 -1377
  62. data/lib/clacky/default_skills/deploy/tools/check_health.rb +0 -116
  63. data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +0 -341
  64. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +0 -99
  65. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +0 -77
  66. data/lib/clacky/default_skills/deploy/tools/list_services.rb +0 -67
  67. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +0 -67
  68. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +0 -189
  69. data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +0 -74
  70. data/lib/clacky/deploy_api_client.rb +0 -484
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/http'
4
- require 'uri'
5
- require 'json'
6
-
7
- module Clacky
8
- module DeployTools
9
- # Perform HTTP health check on deployed application
10
- class CheckHealth
11
- DEFAULT_PATH = '/'
12
- DEFAULT_TIMEOUT = 30
13
- MAX_TIMEOUT = 120
14
-
15
- # Perform health check
16
- #
17
- # @param url [String] Optional URL (defaults to RAILWAY_PUBLIC_DOMAIN env var)
18
- # @param path [String] Health check path (default: "/")
19
- # @param timeout [Integer] Request timeout in seconds (default: 30)
20
- # @return [Hash] Result of health check
21
- def self.execute(url: nil, path: DEFAULT_PATH, timeout: DEFAULT_TIMEOUT)
22
- # Get URL from parameter or environment
23
- target_url = url || ENV['RAILWAY_PUBLIC_DOMAIN']
24
-
25
- if target_url.nil? || target_url.empty?
26
- return {
27
- error: "No URL provided",
28
- details: "Please provide a URL or set RAILWAY_PUBLIC_DOMAIN environment variable"
29
- }
30
- end
31
-
32
- # Ensure URL has protocol
33
- target_url = "https://#{target_url}" unless target_url.start_with?('http://', 'https://')
34
-
35
- # Build full URL with path
36
- full_url = "#{target_url.chomp('/')}#{path}"
37
-
38
- # Validate timeout
39
- timeout = timeout.to_i
40
- if timeout <= 0 || timeout > MAX_TIMEOUT
41
- timeout = DEFAULT_TIMEOUT
42
- end
43
-
44
- puts "🏥 Checking health: #{full_url} (timeout: #{timeout}s)"
45
-
46
- begin
47
- uri = URI.parse(full_url)
48
- result = perform_request(uri, timeout)
49
-
50
- if result[:success]
51
- puts "✅ Health check passed: #{result[:status_code]}"
52
- else
53
- puts "❌ Health check failed: #{result[:error]}"
54
- end
55
-
56
- result.merge(url: full_url, path: path)
57
- rescue URI::InvalidURIError => e
58
- {
59
- success: false,
60
- error: "Invalid URL",
61
- details: e.message,
62
- url: full_url
63
- }
64
- end
65
- end
66
-
67
- # Perform HTTP request
68
- #
69
- # @param uri [URI] Target URI
70
- # @param timeout [Integer] Timeout in seconds
71
- # @return [Hash] Request result
72
- def self.perform_request(uri, timeout)
73
- start_time = Time.now
74
-
75
- http = Net::HTTP.new(uri.host, uri.port)
76
- http.use_ssl = (uri.scheme == 'https')
77
- http.open_timeout = timeout
78
- http.read_timeout = timeout
79
-
80
- request = Net::HTTP::Get.new(uri.request_uri)
81
- request['User-Agent'] = 'Clacky-Deploy-Health-Check/1.0'
82
-
83
- response = http.request(request)
84
- elapsed = Time.now - start_time
85
-
86
- {
87
- success: response.is_a?(Net::HTTPSuccess),
88
- status_code: response.code.to_i,
89
- status_message: response.message,
90
- elapsed: elapsed.round(2),
91
- headers: response.to_hash,
92
- body_preview: response.body&.slice(0, 500) # First 500 chars
93
- }
94
- rescue Net::OpenTimeout, Net::ReadTimeout => e
95
- {
96
- success: false,
97
- error: "Request timeout",
98
- details: e.message,
99
- elapsed: timeout
100
- }
101
- rescue SocketError => e
102
- {
103
- success: false,
104
- error: "Network error",
105
- details: e.message
106
- }
107
- rescue StandardError => e
108
- {
109
- success: false,
110
- error: "Health check failed",
111
- details: "#{e.class}: #{e.message}"
112
- }
113
- end
114
- end
115
- end
116
- end
@@ -1,341 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "tempfile"
5
-
6
- module Clacky
7
- module DeployTools
8
- # Create a PostgreSQL database service on Railway and wait for it to be ready.
9
- #
10
- # This tool handles the known Railway CLI bug where `railway add` returns an
11
- # error exit code when using project tokens, but actually succeeds in creating
12
- # the service.
13
- #
14
- # Strategy:
15
- # 1. Capture existing service IDs before creation
16
- # 2. Execute `railway add --database postgres` (ignore exit code)
17
- # 3. Poll for new service ID (max 15 seconds)
18
- # 4. Wait for DATABASE_URL to be available on the new service (max 2 minutes)
19
- #
20
- # @example
21
- # result = CreateDatabaseService.execute(
22
- # platform_token: "railway-token-here"
23
- # )
24
- # if result[:success]
25
- # puts "Database created: #{result[:service_name]}"
26
- # puts "DATABASE_URL: #{result[:database_url]}"
27
- # end
28
- class CreateDatabaseService
29
-
30
- # Maximum time to wait for new service to appear after creation command
31
- DETECTION_TIMEOUT = 15 # seconds
32
- DETECTION_INTERVAL = 3 # seconds
33
-
34
- # Maximum time to wait for DATABASE_URL to be available
35
- PROVISION_TIMEOUT = 120 # seconds (2 minutes)
36
- PROVISION_INTERVAL = 5 # seconds
37
-
38
- # Execute database creation
39
- #
40
- # @param platform_token [String] RAILWAY_TOKEN for authentication
41
- # @return [Hash] {
42
- # success: Boolean,
43
- # service_id: String (if success),
44
- # service_name: String (if success),
45
- # database_url: String (if success),
46
- # error: String (if failed)
47
- # }
48
- def self.execute(platform_token:)
49
- new(platform_token).execute
50
- end
51
-
52
- def initialize(platform_token)
53
- @platform_token = platform_token
54
- @env = { "RAILWAY_TOKEN" => platform_token }
55
- end
56
-
57
- def execute
58
- # Step 0: Check if database already exists
59
- existing_db = find_existing_database
60
-
61
- if existing_db
62
- puts " ✅ Database already exists: #{existing_db[:name]}"
63
-
64
- # Return without DATABASE_URL - Railway automatically shares it across services
65
- return {
66
- success: true,
67
- service_id: existing_db[:id],
68
- service_name: existing_db[:name],
69
- database_url: nil, # Don't fetch - Railway auto-injects
70
- status: "existing"
71
- }
72
- end
73
-
74
- # No database exists - create new one
75
- puts " 📦 Creating new PostgreSQL database..."
76
-
77
- # Step 1: Get existing service IDs
78
- existing_service_ids = fetch_service_ids
79
-
80
- unless existing_service_ids
81
- return { success: false, error: "Failed to fetch existing services" }
82
- end
83
-
84
- # Step 2: Execute create command (ignore exit code due to known bug)
85
- execute_create_command
86
-
87
- # Step 3: Detect new service
88
- new_service = detect_new_service(existing_service_ids)
89
-
90
- unless new_service
91
- return { success: false, error: "Database service not detected after #{DETECTION_TIMEOUT}s" }
92
- end
93
-
94
- # Step 4: Wait for DATABASE_URL
95
- database_url = wait_for_database_url(new_service[:name])
96
-
97
- unless database_url
98
- return {
99
- success: false,
100
- error: "DATABASE_URL not available after #{PROVISION_TIMEOUT}s for service #{new_service[:name]}"
101
- }
102
- end
103
-
104
- {
105
- success: true,
106
- service_id: new_service[:id],
107
- service_name: new_service[:name],
108
- database_url: database_url,
109
- status: "created"
110
- }
111
-
112
- rescue => e
113
- {
114
- success: false,
115
- error: "Unexpected error: #{e.message}"
116
- }
117
- end
118
-
119
- # Find existing Postgres database service
120
- # @return [Hash, nil] { id:, name: } or nil if not found
121
- private def find_existing_database
122
- Tempfile.create('railway_status') do |tmpfile|
123
- success = system(
124
- @env,
125
- "railway", "status", "--json",
126
- in: :close,
127
- out: tmpfile,
128
- err: File::NULL
129
- )
130
-
131
- return nil unless success
132
-
133
- tmpfile.rewind
134
- output = tmpfile.read.strip
135
- return nil if output.empty?
136
-
137
- begin
138
- data = JSON.parse(output)
139
-
140
- # Navigate to production environment's service instances
141
- edges = data.dig("environments", "edges")
142
- return nil unless edges&.any?
143
-
144
- prod_env = edges.find { |e| e.dig("node", "name") == "production" }
145
- return nil unless prod_env
146
-
147
- service_edges = prod_env.dig("node", "serviceInstances", "edges")
148
- return nil unless service_edges
149
-
150
- # Find Postgres service (by name pattern or image source)
151
- postgres_edge = service_edges.find do |edge|
152
- node = edge["node"]
153
- next false unless node
154
-
155
- service_name = node["serviceName"].to_s
156
- source_image = node.dig("source", "image").to_s
157
-
158
- # Match: name contains "Postgres" or "postgres", or source is postgres image
159
- service_name.match?(/postgres/i) || source_image.include?("postgres")
160
- end
161
-
162
- return nil unless postgres_edge
163
-
164
- node = postgres_edge["node"]
165
- { id: node["serviceId"], name: node["serviceName"] }
166
- rescue JSON::ParserError
167
- nil
168
- end
169
- end
170
- end
171
-
172
- # Fetch all existing service IDs from railway status
173
- # @return [Array<String>, nil] Array of service IDs, or nil on failure
174
- private def fetch_service_ids
175
- Tempfile.create('railway_status') do |tmpfile|
176
- success = system(
177
- @env,
178
- "railway", "status", "--json",
179
- in: :close,
180
- out: tmpfile,
181
- err: File::NULL
182
- )
183
-
184
- return nil unless success
185
-
186
- tmpfile.rewind
187
- output = tmpfile.read.strip
188
- return [] if output.empty?
189
-
190
- begin
191
- data = JSON.parse(output)
192
-
193
- # Extract service IDs from environments.edges[0].node.serviceInstances.edges
194
- edges = data.dig("environments", "edges")
195
- return [] unless edges&.any?
196
-
197
- service_instances = edges[0].dig("node", "serviceInstances", "edges")
198
- return [] unless service_instances
199
-
200
- service_instances.map { |edge| edge.dig("node", "serviceId") }.compact
201
- rescue JSON::ParserError
202
- nil
203
- end
204
- end
205
- end
206
-
207
- # Execute the railway add command
208
- # NOTE: We ignore the exit code because Railway CLI has a known bug
209
- # where it returns error with project tokens but still succeeds
210
- private def execute_create_command
211
- system(
212
- @env,
213
- "railway", "add", "--database", "postgres",
214
- in: :close,
215
- out: File::NULL,
216
- err: File::NULL
217
- )
218
- # Exit code intentionally ignored
219
- end
220
-
221
- # Detect newly created service by comparing service IDs
222
- # @param existing_ids [Array<String>] Service IDs before creation
223
- # @return [Hash, nil] { id: String, name: String } or nil if not found
224
- private def detect_new_service(existing_ids)
225
- max_attempts = DETECTION_TIMEOUT / DETECTION_INTERVAL
226
-
227
- max_attempts.times do
228
- sleep DETECTION_INTERVAL
229
-
230
- current_ids = fetch_service_ids
231
- next unless current_ids
232
-
233
- new_ids = current_ids - existing_ids
234
-
235
- if new_ids.any?
236
- # Fetch full service info to get the name
237
- return fetch_service_info(new_ids.first)
238
- end
239
- end
240
-
241
- nil
242
- end
243
-
244
- # Fetch service name for a given service ID
245
- # @param service_id [String]
246
- # @return [Hash, nil] { id: String, name: String }
247
- private def fetch_service_info(service_id)
248
- Tempfile.create('railway_status') do |tmpfile|
249
- success = system(
250
- @env,
251
- "railway", "status", "--json",
252
- in: :close,
253
- out: tmpfile,
254
- err: File::NULL
255
- )
256
-
257
- return nil unless success
258
-
259
- tmpfile.rewind
260
- output = tmpfile.read.strip
261
- return nil if output.empty?
262
-
263
- begin
264
- data = JSON.parse(output)
265
- edges = data.dig("environments", "edges")
266
- return nil unless edges&.any?
267
-
268
- service_instances = edges[0].dig("node", "serviceInstances", "edges")
269
- return nil unless service_instances
270
-
271
- # Find the service with matching ID
272
- service_node = service_instances.find do |edge|
273
- edge.dig("node", "serviceId") == service_id
274
- end
275
-
276
- return nil unless service_node
277
-
278
- {
279
- id: service_id,
280
- name: service_node.dig("node", "serviceName")
281
- }
282
- rescue JSON::ParserError
283
- nil
284
- end
285
- end
286
- end
287
-
288
- # Wait for DATABASE_URL to be available on the service
289
- # @param service_name [String]
290
- # @return [String, nil] DATABASE_URL or nil if timeout
291
- private def wait_for_database_url(service_name)
292
- max_attempts = PROVISION_TIMEOUT / PROVISION_INTERVAL
293
-
294
- max_attempts.times do
295
- sleep PROVISION_INTERVAL
296
-
297
- database_url = fetch_database_url(service_name)
298
- return database_url if database_url
299
- end
300
-
301
- nil
302
- end
303
-
304
- # Fetch DATABASE_URL from service variables
305
- # @param service_name [String]
306
- # @return [String, nil] DATABASE_URL value or nil
307
- private def fetch_database_url(service_name)
308
- tmpfile = Tempfile.new(['railway_vars', '.json'])
309
-
310
- begin
311
- success = system(
312
- @env,
313
- "railway", "variables", "--service", service_name, "--json",
314
- in: :close,
315
- out: tmpfile,
316
- err: File::NULL
317
- )
318
-
319
- return nil unless success
320
-
321
- tmpfile.rewind
322
- output = tmpfile.read.strip
323
- return nil if output.empty?
324
-
325
- begin
326
- # Output should be a JSON hash of variables
327
- vars = JSON.parse(output)
328
-
329
- # Prefer DATABASE_URL, fall back to DATABASE_PUBLIC_URL
330
- vars["DATABASE_URL"] || vars["DATABASE_PUBLIC_URL"]
331
- rescue JSON::ParserError
332
- nil
333
- end
334
- ensure
335
- tmpfile.close
336
- tmpfile.unlink
337
- end
338
- end
339
- end
340
- end
341
- end
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Clacky
4
- module DeployTools
5
- # Trigger a Railway deployment via `railway up` (blocking, with live logs).
6
- # Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
7
- class ExecuteDeployment
8
-
9
- # Trigger deployment for a service (blocking - waits for completion).
10
- #
11
- # @param service_name [String] Railway service name (from railway.toml or service list)
12
- # @param platform_token [String] RAILWAY_TOKEN for this deploy task
13
- # @return [Hash] { success: true, url: String } or { success: false, error: String }
14
- def self.execute(service_name:, platform_token:)
15
- if service_name.nil? || service_name.strip.empty?
16
- return { success: false, error: "service_name is required" }
17
- end
18
-
19
- if platform_token.nil? || platform_token.strip.empty?
20
- return { success: false, error: "platform_token is required" }
21
- end
22
-
23
- puts "🚀 Deploying service: #{service_name}"
24
- puts " (This may take several minutes - you'll see live build logs below)"
25
- puts ""
26
-
27
- env = { "RAILWAY_TOKEN" => platform_token }
28
-
29
- # Use railway up without --detach to block and show live logs
30
- success = system(
31
- env,
32
- "railway", "up", "--service", service_name,
33
- in: :close # Don't redirect stdout/stderr - let user see live logs
34
- )
35
-
36
- if success
37
- puts ""
38
- puts "✅ Deployment completed successfully"
39
-
40
- # Extract URL from railway status after successful deployment
41
- url = extract_url(env, service_name)
42
-
43
- return { success: true, url: url }
44
- else
45
- puts ""
46
- puts "❌ Deployment failed"
47
- return { success: false, error: "railway up exited with error code" }
48
- end
49
-
50
- rescue => e
51
- { success: false, error: "Unexpected error: #{e.message}" }
52
- end
53
-
54
- private_class_method def self.extract_url(env, service_name)
55
- require "json"
56
- require "tempfile"
57
-
58
- Tempfile.create("railway_status") do |tmpfile|
59
- system(
60
- env,
61
- "railway", "status", "--json",
62
- in: :close,
63
- out: tmpfile,
64
- err: File::NULL
65
- )
66
-
67
- tmpfile.rewind
68
- output = tmpfile.read.strip
69
- return nil if output.empty?
70
-
71
- status_data = JSON.parse(output)
72
-
73
- # Navigate: environments.edges[].node (name="production").serviceInstances.edges[].node
74
- env_edges = status_data.dig("environments", "edges")
75
- return nil unless env_edges.is_a?(Array)
76
-
77
- prod_env = env_edges.find { |e| e.dig("node", "name") == "production" }
78
- return nil unless prod_env
79
-
80
- service_edges = prod_env.dig("node", "serviceInstances", "edges")
81
- return nil unless service_edges.is_a?(Array)
82
-
83
- service_edge = service_edges.find do |edge|
84
- edge.dig("node", "serviceName") == service_name
85
- end
86
- return nil unless service_edge
87
-
88
- domains = service_edge.dig("node", "domains", "customDomains")
89
- return nil unless domains.is_a?(Array) && !domains.empty?
90
-
91
- domain = domains.first["domain"]
92
- domain ? "https://#{domain}" : nil
93
- end
94
- rescue
95
- nil
96
- end
97
- end
98
- end
99
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "uri"
5
- require "json"
6
-
7
- module Clacky
8
- module DeployTools
9
- # Fetch runtime (environment) logs from a deployed Railway service via
10
- # the Clacky Deploy API SSE endpoint.
11
- #
12
- # Uses DeployApiClient#stream_build_logs for build-phase logs (on failure),
13
- # and this class for runtime log fetching (e.g. post-deploy diagnostics).
14
- class FetchRuntimeLogs
15
- DEFAULT_LINES = 50
16
- MAX_LINES = 200
17
- STREAM_TIMEOUT = 60 # seconds
18
-
19
- # Fetch recent runtime logs for a project.
20
- #
21
- # @param project_id [String] Clacky project ID
22
- # @param workspace_key [String] clacky_ak_* key
23
- # @param base_url [String] API base URL
24
- # @param lines [Integer] max log lines to collect
25
- # @param keyword [String] optional filter keyword
26
- # @return [Hash] { success: true, logs: Array<String> }
27
- # or { success: false, error: String }
28
- def self.execute(project_id:, workspace_key:, base_url:,
29
- lines: DEFAULT_LINES, keyword: nil)
30
- lines = [[lines.to_i, 1].max, MAX_LINES].min
31
-
32
- params = "project_id=#{URI.encode_www_form_component(project_id)}"
33
- params += "&keyword=#{URI.encode_www_form_component(keyword)}" if keyword
34
-
35
- uri = URI.parse(
36
- "#{base_url.to_s.sub(%r{/+$}, "")}" \
37
- "/openclacky/v1/deploy/logs/environment/stream?#{params}"
38
- )
39
-
40
- collected = []
41
-
42
- http = Net::HTTP.new(uri.host, uri.port)
43
- http.use_ssl = (uri.scheme == "https")
44
- http.open_timeout = 10
45
- http.read_timeout = STREAM_TIMEOUT
46
-
47
- req = Net::HTTP::Get.new(uri.request_uri)
48
- req["Authorization"] = "Bearer #{workspace_key}"
49
- req["Accept"] = "text/event-stream"
50
-
51
- http.request(req) do |response|
52
- response.read_body do |chunk|
53
- chunk.split("\n").each do |raw|
54
- next unless raw.start_with?("data:")
55
-
56
- json_str = raw.sub(/\Adata:\s*/, "")
57
- next if json_str.strip.empty?
58
-
59
- begin
60
- event = JSON.parse(json_str)
61
- msg = event["message"].to_s
62
- collected << msg unless msg.empty?
63
- return { success: true, logs: collected } if collected.size >= lines
64
- rescue JSON::ParserError
65
- # skip malformed events
66
- end
67
- end
68
- end
69
- end
70
-
71
- { success: true, logs: collected }
72
- rescue => e
73
- { success: false, error: "Failed to fetch runtime logs: #{e.message}" }
74
- end
75
- end
76
- end
77
- end
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open3"
4
- require "json"
5
-
6
- module Clacky
7
- module DeployTools
8
- # List Railway services for the linked project.
9
- # Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
10
- #
11
- # NOTE: In the new deploy flow, service discovery is primarily done via the
12
- # Clacky Deploy API (deploy/services endpoint). This tool is kept as a
13
- # fallback for detecting the main service name via `railway service list`.
14
- class ListServices
15
-
16
- # List services for the current Railway project.
17
- #
18
- # @param platform_token [String] RAILWAY_TOKEN for this deploy task
19
- # @return [Hash] {
20
- # success: Boolean,
21
- # services: Array<Hash>,
22
- # main_service: Hash | nil, # first non-middleware service
23
- # db_service: Hash | nil # first postgres/mysql service
24
- # }
25
- def self.execute(platform_token:)
26
- if platform_token.nil? || platform_token.strip.empty?
27
- return { success: false, error: "platform_token is required" }
28
- end
29
-
30
- env = ENV.to_h.merge("RAILWAY_TOKEN" => platform_token)
31
- out, err, status = Open3.capture3(env, "railway status --json")
32
-
33
- unless status.success?
34
- return {
35
- success: false,
36
- error: "railway service list failed (exit #{status.exitstatus})",
37
- details: err
38
- }
39
- end
40
-
41
- info = JSON.parse(out)
42
- services = info["services"] || []
43
-
44
- main_svc = services.find do |s|
45
- name = s["name"].to_s.downcase
46
- !%w[postgres postgresql mysql redis].any? { |db| name.include?(db) }
47
- end
48
-
49
- db_svc = services.find do |s|
50
- name = s["name"].to_s.downcase
51
- %w[postgres postgresql mysql].any? { |db| name.include?(db) }
52
- end
53
-
54
- {
55
- success: true,
56
- services: services,
57
- main_service: main_svc,
58
- db_service: db_svc
59
- }
60
- rescue JSON::ParserError => e
61
- { success: false, error: "Failed to parse service list: #{e.message}", raw: out.to_s[0, 200] }
62
- rescue => e
63
- { success: false, error: "Unexpected error: #{e.message}" }
64
- end
65
- end
66
- end
67
- end