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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/docs/deploy-architecture.md +619 -0
- data/lib/clacky/agent/llm_caller.rb +14 -2
- data/lib/clacky/agent/message_compressor.rb +24 -6
- data/lib/clacky/agent/message_compressor_helper.rb +17 -10
- data/lib/clacky/agent/session_serializer.rb +69 -0
- data/lib/clacky/agent/skill_manager.rb +2 -2
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/brand_config.rb +29 -3
- data/lib/clacky/clacky_auth_client.rb +152 -0
- data/lib/clacky/clacky_cloud_config.rb +123 -0
- data/lib/clacky/cli.rb +13 -0
- data/lib/clacky/client.rb +21 -7
- data/lib/clacky/cloud_project_client.rb +169 -0
- data/lib/clacky/default_agents/base_prompt.md +1 -0
- data/lib/clacky/default_parsers/doc_parser.rb +9 -9
- data/lib/clacky/default_skills/browser-setup/SKILL.md +9 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +21 -4
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +8 -2
- data/lib/clacky/default_skills/deploy/SKILL.md +96 -5
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1268 -274
- data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +341 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +72 -147
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +60 -50
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +47 -60
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +147 -96
- data/lib/clacky/default_skills/new/SKILL.md +117 -5
- data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +74 -0
- data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +32 -0
- data/lib/clacky/deploy_api_client.rb +484 -0
- data/lib/clacky/json_ui_controller.rb +16 -10
- data/lib/clacky/message_format/bedrock.rb +3 -2
- data/lib/clacky/message_history.rb +8 -0
- data/lib/clacky/plain_ui_controller.rb +1 -6
- data/lib/clacky/providers.rb +23 -4
- data/lib/clacky/server/browser_manager.rb +3 -1
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +2 -1
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +3 -1
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +5 -5
- data/lib/clacky/server/http_server.rb +12 -2
- data/lib/clacky/server/server_master.rb +43 -7
- data/lib/clacky/server/web_ui_controller.rb +17 -9
- data/lib/clacky/skill.rb +6 -2
- data/lib/clacky/tools/run_project.rb +4 -1
- data/lib/clacky/tools/shell.rb +7 -1
- data/lib/clacky/ui2/ui_controller.rb +1 -5
- data/lib/clacky/ui_interface.rb +5 -7
- data/lib/clacky/utils/arguments_parser.rb +22 -5
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +45 -5
- data/lib/clacky/web/app.js +126 -19
- data/lib/clacky/web/i18n.js +57 -0
- data/lib/clacky/web/sessions.js +108 -39
- data/lib/clacky/web/skills.js +8 -2
- data/lib/clacky.rb +3 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
#
|
|
36
|
-
def
|
|
37
|
-
stdout, _stderr, status = Open3.capture3("
|
|
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
|
-
|
|
40
|
-
return nil if
|
|
41
|
-
|
|
39
|
+
text = stdout.strip
|
|
40
|
+
return nil if text.bytesize < MIN_CONTENT_BYTES
|
|
41
|
+
text
|
|
42
42
|
rescue Errno::ENOENT
|
|
43
|
-
nil #
|
|
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) ||
|
|
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
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
9
|
+
Deploy a Rails application to the Clacky cloud platform (Railway backend).
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
## When to invoke
|
|
13
12
|
|
|
14
|
-
|
|
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`
|