openclacky 0.9.28 → 0.9.30

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/docs/deploy-architecture.md +619 -0
  4. data/lib/clacky/agent/llm_caller.rb +14 -2
  5. data/lib/clacky/agent/message_compressor.rb +24 -6
  6. data/lib/clacky/agent/message_compressor_helper.rb +17 -10
  7. data/lib/clacky/agent/session_serializer.rb +69 -0
  8. data/lib/clacky/agent/skill_manager.rb +2 -2
  9. data/lib/clacky/agent.rb +3 -0
  10. data/lib/clacky/brand_config.rb +29 -3
  11. data/lib/clacky/clacky_auth_client.rb +152 -0
  12. data/lib/clacky/clacky_cloud_config.rb +123 -0
  13. data/lib/clacky/cli.rb +13 -0
  14. data/lib/clacky/client.rb +21 -7
  15. data/lib/clacky/cloud_project_client.rb +169 -0
  16. data/lib/clacky/default_agents/base_prompt.md +1 -0
  17. data/lib/clacky/default_parsers/doc_parser.rb +9 -9
  18. data/lib/clacky/default_skills/browser-setup/SKILL.md +9 -0
  19. data/lib/clacky/default_skills/channel-setup/SKILL.md +21 -4
  20. data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +8 -2
  21. data/lib/clacky/default_skills/deploy/SKILL.md +96 -5
  22. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1268 -274
  23. data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +341 -0
  24. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +72 -147
  25. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +60 -50
  26. data/lib/clacky/default_skills/deploy/tools/list_services.rb +47 -60
  27. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +147 -96
  28. data/lib/clacky/default_skills/new/SKILL.md +117 -5
  29. data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +74 -0
  30. data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +32 -0
  31. data/lib/clacky/deploy_api_client.rb +484 -0
  32. data/lib/clacky/json_ui_controller.rb +16 -10
  33. data/lib/clacky/message_format/bedrock.rb +3 -2
  34. data/lib/clacky/message_history.rb +8 -0
  35. data/lib/clacky/plain_ui_controller.rb +1 -6
  36. data/lib/clacky/providers.rb +23 -4
  37. data/lib/clacky/server/browser_manager.rb +3 -1
  38. data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +2 -1
  39. data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +3 -1
  40. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +5 -5
  41. data/lib/clacky/server/http_server.rb +12 -2
  42. data/lib/clacky/server/server_master.rb +43 -7
  43. data/lib/clacky/server/web_ui_controller.rb +17 -9
  44. data/lib/clacky/skill.rb +6 -2
  45. data/lib/clacky/tools/run_project.rb +4 -1
  46. data/lib/clacky/tools/shell.rb +7 -1
  47. data/lib/clacky/ui2/ui_controller.rb +1 -5
  48. data/lib/clacky/ui_interface.rb +5 -7
  49. data/lib/clacky/utils/arguments_parser.rb +22 -5
  50. data/lib/clacky/version.rb +1 -1
  51. data/lib/clacky/web/app.css +45 -5
  52. data/lib/clacky/web/app.js +126 -19
  53. data/lib/clacky/web/i18n.js +57 -0
  54. data/lib/clacky/web/sessions.js +108 -39
  55. data/lib/clacky/web/skills.js +8 -2
  56. data/lib/clacky.rb +3 -0
  57. metadata +8 -1
data/lib/clacky/client.rb CHANGED
@@ -51,6 +51,7 @@ module Clacky
51
51
  rescue Faraday::Error => e
52
52
  { success: false, error: "Connection error: #{e.message}" }
53
53
  rescue => e
54
+ Clacky::Logger.error("[test_connection] #{e.class}: #{e.message}", error: e)
54
55
  { success: false, error: e.message }
55
56
  end
56
57
 
@@ -113,12 +114,25 @@ module Clacky
113
114
 
114
115
  # ── Prompt-caching support ────────────────────────────────────────────────
115
116
 
116
- # Returns true for Claude 3.5+ models that support prompt caching.
117
+ # Returns true for Claude models that support prompt caching (gen 3.5+ or gen 4+).
118
+ #
119
+ # Handles both direct model names (e.g. "claude-haiku-4-5") and
120
+ # Clacky AI Bedrock proxy names with "abs-" prefix (e.g. "abs-claude-haiku-4-5").
121
+ #
122
+ # Why only Claude models:
123
+ # - MiniMax uses automatic server-side caching (no cache_control needed from client)
124
+ # - Kimi uses a proprietary prompt_cache_key param, not cache_control
125
+ # - MiMo has no documented caching API
126
+ # - Only Claude (direct, OpenRouter, or ClackyAI Bedrock proxy) consumes our
127
+ # cache_control / cachePoint markers
117
128
  def supports_prompt_caching?(model)
118
- model_str = model.to_s.downcase
129
+ # Strip ClackyAI Bedrock proxy prefix before matching
130
+ model_str = model.to_s.downcase.sub(/^abs-/, "")
119
131
  return false unless model_str.include?("claude")
120
132
 
121
- model_str.match?(/claude(?:-3[-.]?[5-9]|-[4-9]|-sonnet-[34])/)
133
+ # Match Claude gen 3.5+ (3.5/3.6/3.7…) or gen 4+ in any name format:
134
+ # claude-3.5-sonnet-... claude-3-7-sonnet claude-haiku-4-5 claude-sonnet-4-6
135
+ model_str.match?(/claude(?:-3[-.]?[5-9]|.*-[4-9][-.]|.*-[4-9]$|-[4-9][-.]|-[4-9]$|-sonnet-[34])/)
122
136
  end
123
137
 
124
138
 
@@ -245,7 +259,7 @@ module Clacky
245
259
  @bedrock_connection ||= Faraday.new(url: @base_url) do |conn|
246
260
  conn.headers["Content-Type"] = "application/json"
247
261
  conn.headers["Authorization"] = "Bearer #{@api_key}"
248
- conn.options.timeout = 120
262
+ conn.options.timeout = 300
249
263
  conn.options.open_timeout = 10
250
264
  conn.ssl.verify = false
251
265
  conn.adapter Faraday.default_adapter
@@ -256,7 +270,7 @@ module Clacky
256
270
  @openai_connection ||= Faraday.new(url: @base_url) do |conn|
257
271
  conn.headers["Content-Type"] = "application/json"
258
272
  conn.headers["Authorization"] = "Bearer #{@api_key}"
259
- conn.options.timeout = 120
273
+ conn.options.timeout = 300
260
274
  conn.options.open_timeout = 10
261
275
  conn.ssl.verify = false
262
276
  conn.adapter Faraday.default_adapter
@@ -269,7 +283,7 @@ module Clacky
269
283
  conn.headers["x-api-key"] = @api_key
270
284
  conn.headers["anthropic-version"] = "2023-06-01"
271
285
  conn.headers["anthropic-dangerous-direct-browser-access"] = "true"
272
- conn.options.timeout = 120
286
+ conn.options.timeout = 300
273
287
  conn.options.open_timeout = 10
274
288
  conn.ssl.verify = false
275
289
  conn.adapter Faraday.default_adapter
@@ -328,7 +342,7 @@ module Clacky
328
342
  return raw_body unless error_body.is_a?(Hash)
329
343
 
330
344
  error_body["upstreamMessage"]&.then { |m| return m unless m.empty? }
331
- error_body.dig("error", "message")&.then { |m| return m }
345
+ error_body.dig("error", "message")&.then { |m| return m } if error_body["error"].is_a?(Hash)
332
346
  error_body["message"]&.then { |m| return m }
333
347
  error_body["error"].is_a?(String) ? error_body["error"] : (raw_body.to_s[0..200] + (raw_body.to_s.length > 200 ? "..." : ""))
334
348
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Clacky
7
+ # CloudProjectClient - Manages cloud project lifecycle via the OpenClacky API
8
+ #
9
+ # Handles creating projects, fetching project details (including subscription
10
+ # status and categorized_config), and listing projects in a workspace.
11
+ #
12
+ # All API calls use the Workspace API Key (clacky_ak_*) from ClackyCloudConfig.
13
+ #
14
+ # Usage:
15
+ # client = CloudProjectClient.new("clacky_ak_xxx", base_url: "https://api.clacky.ai")
16
+ #
17
+ # # Create a new cloud project
18
+ # result = client.create_project(name: "my-app")
19
+ # # => { success: true, project: { "id" => "...", "name" => "...", "workspace_id" => "...",
20
+ # # "categorized_config" => { "auth" => {...}, "email" => {...}, ... } } }
21
+ #
22
+ # # Get project details (subscription + categorized_config)
23
+ # result = client.get_project("019d41be-...")
24
+ # # => { success: true, project: { "id" => "...", "subscription" => { "status" => "PAID" }, ... } }
25
+ #
26
+ # # List all projects in workspace
27
+ # result = client.list_projects
28
+ # # => { success: true, projects: [ { "id" => "...", "name" => "..." }, ... ] }
29
+ #
30
+ # On failure, all methods return: { success: false, error: "..." }
31
+ class CloudProjectClient
32
+ PROJECTS_PATH = "/openclacky/v1/projects"
33
+ REQUEST_TIMEOUT = 15 # seconds
34
+ OPEN_TIMEOUT = 5 # seconds
35
+
36
+ def initialize(workspace_api_key, base_url:)
37
+ @workspace_api_key = workspace_api_key.to_s.strip
38
+ @base_url = base_url.to_s.strip.sub(%r{/+$}, "")
39
+ end
40
+
41
+ # Create a new cloud project with the given name.
42
+ #
43
+ # @param name [String] Project name (typically the local directory name)
44
+ # @return [Hash] { success: true, project: {...} } or { success: false, error: "..." }
45
+ def create_project(name:)
46
+ validate_inputs!
47
+
48
+ response = connection.post(PROJECTS_PATH) do |req|
49
+ req.headers["Content-Type"] = "application/json"
50
+ req.body = JSON.generate({ name: name.to_s.strip })
51
+ end
52
+
53
+ unless response.status == 200
54
+ error_msg = extract_error(response)
55
+ return { success: false, error: "HTTP #{response.status}: #{error_msg}" }
56
+ end
57
+
58
+ body = parse_body(response)
59
+ return body_error(body) unless success_code?(body)
60
+
61
+ { success: true, project: body["data"] }
62
+ rescue Faraday::Error => e
63
+ { success: false, error: "Network error: #{e.message}" }
64
+ rescue => e
65
+ { success: false, error: "Unexpected error: #{e.message}" }
66
+ end
67
+
68
+ # Get project details including subscription status and categorized_config.
69
+ #
70
+ # @param project_id [String] The cloud project UUID
71
+ # @return [Hash] { success: true, project: {...} } or { success: false, error: "..." }
72
+ def get_project(project_id)
73
+ validate_inputs!
74
+
75
+ response = connection.get("#{PROJECTS_PATH}/#{project_id}")
76
+
77
+ unless response.status == 200
78
+ error_msg = extract_error(response)
79
+ return { success: false, error: "HTTP #{response.status}: #{error_msg}" }
80
+ end
81
+
82
+ body = parse_body(response)
83
+ return body_error(body) unless success_code?(body)
84
+
85
+ { success: true, project: body["data"] }
86
+ rescue Faraday::Error => e
87
+ { success: false, error: "Network error: #{e.message}" }
88
+ rescue => e
89
+ { success: false, error: "Unexpected error: #{e.message}" }
90
+ end
91
+
92
+ # List all projects in the current workspace.
93
+ #
94
+ # @return [Hash] { success: true, projects: [...] } or { success: false, error: "..." }
95
+ def list_projects
96
+ validate_inputs!
97
+
98
+ response = connection.get(PROJECTS_PATH)
99
+
100
+ unless response.status == 200
101
+ error_msg = extract_error(response)
102
+ return { success: false, error: "HTTP #{response.status}: #{error_msg}" }
103
+ end
104
+
105
+ body = parse_body(response)
106
+ return body_error(body) unless success_code?(body)
107
+
108
+ projects = body["data"] || []
109
+ projects = projects["list"] if projects.is_a?(Hash) && projects["list"]
110
+
111
+ { success: true, projects: Array(projects) }
112
+ rescue Faraday::Error => e
113
+ { success: false, error: "Network error: #{e.message}" }
114
+ rescue => e
115
+ { success: false, error: "Unexpected error: #{e.message}" }
116
+ end
117
+
118
+ private def validate_inputs!
119
+ raise ArgumentError, "workspace_api_key is required" if @workspace_api_key.empty?
120
+ raise ArgumentError, "base_url is required" if @base_url.empty?
121
+ end
122
+
123
+ private def connection
124
+ @connection ||= Faraday.new(url: @base_url) do |f|
125
+ f.options.timeout = REQUEST_TIMEOUT
126
+ f.options.open_timeout = OPEN_TIMEOUT
127
+ f.headers["Authorization"] = "Bearer #{@workspace_api_key}"
128
+ f.headers["Accept"] = "application/json"
129
+ # Disable SSL verification to avoid OpenSSL certificate path issues
130
+ # on some macOS environments with system Ruby
131
+ f.ssl.verify = false
132
+ f.adapter Faraday.default_adapter
133
+ end
134
+ end
135
+
136
+ # Parse JSON response body.
137
+ # Returns the parsed Hash on success, or nil if the body is not valid JSON.
138
+ private def parse_body(response)
139
+ JSON.parse(response.body)
140
+ rescue JSON::ParserError
141
+ nil
142
+ end
143
+
144
+ # The API returns code 0 or 200 to signal success.
145
+ # Returns false if body is nil (unparseable JSON).
146
+ private def success_code?(body)
147
+ return false if body.nil?
148
+
149
+ code = body["code"].to_i
150
+ code == 0 || code == 200
151
+ end
152
+
153
+ # Build a failure hash from a parsed response body (may be nil for non-JSON)
154
+ private def body_error(body)
155
+ return { success: false, error: "Invalid JSON response from API" } if body.nil?
156
+
157
+ msg = body["message"] || body["msg"] || "Unknown API error (code: #{body["code"]})"
158
+ { success: false, error: msg }
159
+ end
160
+
161
+ # Extract a human-readable error string from a raw Faraday response
162
+ private def extract_error(response)
163
+ parsed = JSON.parse(response.body)
164
+ parsed["message"] || parsed["msg"] || response.body.to_s[0, 200]
165
+ rescue
166
+ response.body.to_s[0, 200]
167
+ end
168
+ end
169
+ end
@@ -10,6 +10,7 @@
10
10
 
11
11
  - **ALWAYS use `glob` tool to find files — NEVER use shell `find` command for file discovery**
12
12
  - Test your changes using the shell tool when appropriate
13
+ - **All operations default to the working directory** (shown in session context)
13
14
 
14
15
  ## TODO Manager Rules
15
16
 
@@ -32,15 +32,15 @@ rescue Errno::ENOENT
32
32
  nil # textutil not available (non-macOS)
33
33
  end
34
34
 
35
- # Fallback: strings command extracts printable ASCII sequences
36
- def try_strings(path)
37
- stdout, _stderr, status = Open3.capture3("strings", path)
35
+ # Use antiword to extract text from .doc files (Linux/WSL)
36
+ def try_antiword(path)
37
+ stdout, _stderr, status = Open3.capture3("antiword", path)
38
38
  return nil unless status.success?
39
- lines = stdout.lines.select { |l| l.strip.length >= 4 }
40
- return nil if lines.size < 3
41
- lines.join
39
+ text = stdout.strip
40
+ return nil if text.bytesize < MIN_CONTENT_BYTES
41
+ text
42
42
  rescue Errno::ENOENT
43
- nil # strings not available
43
+ nil # antiword not installed
44
44
  end
45
45
 
46
46
  # --- main ---
@@ -57,13 +57,13 @@ unless File.exist?(path)
57
57
  exit 1
58
58
  end
59
59
 
60
- text = try_textutil(path) || try_strings(path)
60
+ text = try_textutil(path) || try_antiword(path)
61
61
 
62
62
  if text
63
63
  print text
64
64
  exit 0
65
65
  else
66
66
  warn "Could not extract text from .doc file."
67
- warn "Tip: on macOS textutil should work. On Linux try: apt install antiword"
67
+ warn "Tip: on macOS textutil should work. On Linux/WSL try: apt install antiword"
68
68
  exit 1
69
69
  end
@@ -43,6 +43,15 @@ node --version 2>/dev/null
43
43
 
44
44
  Parse the version. If Node.js is missing or version < 20:
45
45
 
46
+ Run the bundled installer to automatically install Node.js:
47
+ ```bash
48
+ bash ~/.clacky/scripts/install_browser.sh
49
+ ```
50
+
51
+ If the script exits 0 → Node.js is now installed. Proceed to Step 2.
52
+
53
+ If the script exits non-zero or doesn't exist:
54
+
46
55
  > ❌ Node.js 20+ is required for browser automation.
47
56
  >
48
57
  > Please install Node.js from: https://nodejs.org
@@ -100,10 +100,27 @@ ruby "SKILL_DIR/feishu_setup.rb"
100
100
  - Tell the user: "✅ Feishu channel configured automatically! The channel is ready."
101
101
  - **Stop here — do not proceed to manual steps.**
102
102
 
103
- **If exit code is non-0 (or script not found):**
104
- - Note the failure reason from stdout (the last `❌` line).
105
- - Tell the user: "Automated setup encountered an issue: `<reason>`. Switching to guided setup..."
106
- - Continue to Step 2 (manual flow) below.
103
+ **If exit code is non-0:**
104
+ - Check stdout for the error message.
105
+ - **If the error contains "Browser not configured" or "browser tool":**
106
+ - Tell the user: "The browser tool is not configured yet. Let me help you set it up first..."
107
+ - Invoke the `browser-setup` skill: `invoke_skill("browser-setup", "setup")`.
108
+ - After browser-setup completes, tell the user: "Browser is ready! Let me retry the Feishu setup..."
109
+ - **Retry the script** (same command, same timeout). If it succeeds this time, stop. If it fails again, check the new error and proceed accordingly.
110
+ - **If the error contains "No cookies found" or "Please log in":**
111
+ - Open Feishu login page using browser tool:
112
+ ```
113
+ browser(action="navigate", url="https://open.feishu.cn/app")
114
+ ```
115
+ - Tell the user: "I've opened Feishu in your browser. Please log in, then reply 'done'."
116
+ - Wait for "done".
117
+ - **Retry the script** (same command, same timeout). Repeat this login-wait-retry loop up to **3 times total**.
118
+ - If any attempt succeeds (exit code 0), stop — setup is complete.
119
+ - If an attempt fails with a **different** error (not a login error), break out of the loop and continue to Step 2.
120
+ - If all 3 attempts fail with login errors, tell the user: "Automated setup was unable to detect a Feishu login after 3 attempts. Switching to guided setup..." and continue to Step 2.
121
+ - **Otherwise (non-login, non-browser error):**
122
+ - Tell the user: "Automated setup encountered an issue: `<error message>`. Switching to guided setup..."
123
+ - Continue to Step 2 (manual flow) below.
107
124
 
108
125
  ---
109
126
 
@@ -130,6 +130,12 @@ class BrowserSession
130
130
  snapshot
131
131
  end
132
132
 
133
+ def open(url)
134
+ @client.call("open", url: url)
135
+ sleep 2
136
+ snapshot
137
+ end
138
+
133
139
  def snapshot(interactive: true, compact: true)
134
140
  result = @client.call("snapshot", interactive: interactive, compact: compact)
135
141
  result["output"].to_s
@@ -394,7 +400,7 @@ def run_setup(browser, api)
394
400
 
395
401
  # ── Phase 1: Verify login ────────────────────────────────────────────────
396
402
  step "Phase 1 — Verifying Feishu login..."
397
- snap = browser.navigate("https://open.feishu.cn/app")
403
+ snap = browser.open("https://open.feishu.cn/app")
398
404
  unless snap.include?("创建企业自建") || snap.include?("Create Custom App") || snap.include?("Create Enterprise")
399
405
  fail! "Not logged in to Feishu Open Platform. Please log in to open.feishu.cn in Chrome first, then re-run."
400
406
  end
@@ -554,7 +560,7 @@ browser = BrowserSession.new(tool_client)
554
560
 
555
561
  # Navigate to Feishu to establish page context
556
562
  step "Initializing browser session..."
557
- browser.navigate("https://open.feishu.cn/app")
563
+ browser.open("https://open.feishu.cn/app")
558
564
  sleep 1
559
565
 
560
566
  # Quick sanity check — verify we have cookies
@@ -1,14 +1,105 @@
1
1
  ---
2
2
  name: deploy
3
- description: Deploy Rails applications to Railway PaaS
3
+ description: Deploy Rails applications to Clacky cloud platform(Railway backend)
4
4
  agent: coding
5
- fork_agent: true
6
5
  ---
7
6
 
8
7
  # Railway Deployment for Rails
9
8
 
10
- Deploy a Rails application to Railway platform.
9
+ Deploy a Rails application to the Clacky cloud platform (Railway backend).
11
10
 
12
- Execute the Rails deployment script located in this skill's `scripts/rails_deploy.rb`.
11
+ ## When to invoke
13
12
 
14
- The script validates the environment and runs an 8-step deployment workflow.
13
+ Trigger this skill when the user says:
14
+ - "deploy", "/deploy", "deploy my app", "push to production"
15
+ - "部署", "上线", "发布"
16
+
17
+ ---
18
+
19
+ ## How to run
20
+
21
+ ### Step 1 — Run the deploy script
22
+
23
+ ```bash
24
+ bundle exec ruby <absolute-path-to-this-skill>/scripts/rails_deploy.rb
25
+ ```
26
+
27
+ **Timeout**: set to at least 300 seconds (5 minutes).
28
+
29
+ The script prints each step as it runs. When it finishes it prints one of:
30
+
31
+ ```
32
+ [DEPLOY] RESULT: SUCCESS (2m 34s)
33
+ [DEPLOY] RESULT: FAILED (45s) — <error message>
34
+ ```
35
+
36
+ ### Step 2 — Show the full output to the user
37
+
38
+ After the script exits, **always show the complete stdout output** to the user
39
+ in a code block or verbatim. The output contains step-by-step logs they need
40
+ to see. Do NOT summarise silently — show everything, then add your summary.
41
+
42
+ ### On success
43
+ After showing the output, report the deployed URL and any useful links.
44
+
45
+ ### On failure
46
+ After showing the output, show the error message and summarise the most
47
+ likely cause in one sentence. Suggest next steps (e.g. fix the error shown,
48
+ then re-run `/deploy`).
49
+
50
+ ---
51
+
52
+ ## What the script does internally
53
+
54
+ The script runs three phases automatically. Do **not** add any AI reasoning
55
+ steps between phases — the script handles all logic internally.
56
+
57
+ **Phase 0 — Cloud project binding**
58
+ 1. Reads `.clacky/openclacky.yml` for `project_id`
59
+ - If file is missing → runs inline cloud project creation flow
60
+ (reuses `new/scripts/cloud_project_init.sh`), writes the file, continues
61
+ - If `project_id` is blank → hard-fail (corrupted file)
62
+ 2. Reads `~/.clacky/clacky_cloud.yml` for `workspace_key`
63
+ - If missing/empty → hard-fail with guidance to obtain key offline
64
+ 3. Calls `GET /openclacky/v1/projects/:id` to verify the project exists
65
+ - 404 → runs inline cloud project creation flow, continues
66
+ - Other error → hard-fail
67
+
68
+ **Phase 1 — Subscription check**
69
+
70
+ | `subscription.status` | Action |
71
+ |------------------------|--------|
72
+ | `PAID` | ✅ Continue |
73
+ | `FREEZE` | ⚠️ Warn, continue |
74
+ | `SUSPENDED` | ❌ Hard-fail |
75
+ | `null` / `OFF` / `CANCELLED` | Open payment page, poll for activation |
76
+
77
+ Payment polling: open `https://app.clacky.ai/dashboard/openclacky-project/<id>`
78
+ in browser, poll `GET /openclacky/v1/deploy/payment` every 10 s for up to 180 s.
79
+
80
+ **Phase 2 — Deployment (8 steps)**
81
+
82
+ | Step | Action |
83
+ |------|--------|
84
+ | 1 | `POST /deploy/create-task` → get `platform_token`, `platform_project_id`, `deploy_task_id` |
85
+ | 2 | `railway link --project <id> --environment production` |
86
+ | 3 | Inject env vars: Rails defaults + Figaro `config/application.yml` production block + `categorized_config` |
87
+ | 4 | Poll `GET /deploy/services` until DB middleware is `SUCCESS` → inject `DATABASE_URL` reference; call `POST /deploy/bind-domain` |
88
+ | 5 | `railway up --service <name> --detach` → notify backend `"deploying"` |
89
+ | 6 | Poll `GET /deploy/status` every 5 s (max 300 s) until `SUCCESS` or failure |
90
+ | 7 | `railway run bundle exec rails db:migrate`; seed if first deployment |
91
+ | 8 | HTTP health check on deployed URL; notify backend `"success"` |
92
+
93
+ All `railway` commands receive `RAILWAY_TOKEN` via Ruby `ENV` hash — no
94
+ `clackycli` wrapper is needed.
95
+
96
+ ---
97
+
98
+ ## Important constraints
99
+
100
+ - **Never** modify source files before deploying.
101
+ - **Never** commit or push changes as part of this skill.
102
+ - **Never** prompt the user for Railway credentials — those come from the
103
+ Clacky platform (`platform_token` is returned by `create-task`).
104
+ - If `railway` CLI is not installed, hard-fail with install instructions:
105
+ `npm install -g @railway/cli`