openclacky 0.9.26 → 0.9.27

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +8 -4
  3. data/CHANGELOG.md +25 -0
  4. data/lib/clacky/agent/skill_manager.rb +11 -4
  5. data/lib/clacky/agent.rb +14 -20
  6. data/lib/clacky/agent_config.rb +40 -1
  7. data/lib/clacky/brand_config.rb +36 -82
  8. data/lib/clacky/client.rb +12 -2
  9. data/lib/clacky/default_skills/code-explorer/SKILL.md +1 -0
  10. data/lib/clacky/default_skills/new/SKILL.md +22 -2
  11. data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +30 -33
  12. data/lib/clacky/default_skills/personal-website/publish.rb +59 -38
  13. data/lib/clacky/message_format/anthropic.rb +9 -1
  14. data/lib/clacky/message_format/bedrock.rb +4 -2
  15. data/lib/clacky/platform_http_client.rb +199 -0
  16. data/lib/clacky/providers.rb +19 -0
  17. data/lib/clacky/server/http_server.rb +18 -2
  18. data/lib/clacky/server/web_ui_controller.rb +42 -1
  19. data/lib/clacky/tools/base.rb +3 -0
  20. data/lib/clacky/tools/browser.rb +3 -12
  21. data/lib/clacky/tools/file_reader.rb +9 -13
  22. data/lib/clacky/tools/glob.rb +5 -5
  23. data/lib/clacky/tools/grep.rb +1 -1
  24. data/lib/clacky/tools/safe_shell.rb +2 -2
  25. data/lib/clacky/tools/shell.rb +42 -42
  26. data/lib/clacky/ui2/ui_controller.rb +34 -30
  27. data/lib/clacky/ui_interface.rb +1 -0
  28. data/lib/clacky/utils/file_processor.rb +122 -2
  29. data/lib/clacky/utils/scripts_manager.rb +1 -0
  30. data/lib/clacky/version.rb +1 -1
  31. data/lib/clacky/web/app.css +110 -0
  32. data/lib/clacky/web/app.js +109 -1
  33. data/lib/clacky/web/i18n.js +6 -0
  34. data/lib/clacky/web/index.html +19 -0
  35. data/lib/clacky/web/sessions.js +56 -10
  36. data/lib/clacky/web/settings.js +14 -2
  37. data/lib/clacky/web/skills.js +26 -2
  38. data/lib/clacky.rb +2 -0
  39. data/scripts/build/build.sh +329 -0
  40. data/scripts/build/lib/apt.sh +36 -0
  41. data/scripts/build/lib/brew.sh +89 -0
  42. data/scripts/build/lib/colors.sh +17 -0
  43. data/scripts/build/lib/gem.sh +95 -0
  44. data/scripts/build/lib/mise.sh +125 -0
  45. data/scripts/build/lib/network.sh +156 -0
  46. data/scripts/build/lib/os.sh +57 -0
  47. data/scripts/build/lib/shell.sh +37 -0
  48. data/scripts/build/src/install.sh.cc +164 -0
  49. data/scripts/build/src/install_browser.sh.cc +101 -0
  50. data/scripts/build/src/install_full.sh.cc +290 -0
  51. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  52. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  53. data/scripts/build/src/uninstall.sh.cc +101 -0
  54. data/scripts/install.sh +205 -307
  55. data/scripts/install_browser.sh +313 -114
  56. data/scripts/install_full.sh +528 -589
  57. data/scripts/install_rails_deps.sh +725 -0
  58. data/scripts/install_system_deps.sh +364 -128
  59. data/scripts/uninstall.sh +213 -89
  60. metadata +24 -5
  61. data/lib/clacky/default_skills/new/scripts/rails_env_checker.sh +0 -389
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65dad1ba4790fcffb30157bcc74a05a9f9cbd3e341ead0e007e6c768b0c11fd1
4
- data.tar.gz: f507ade251d206b073eb1b88c236d8905e9c986718d93c62cf3f2b575fa908bb
3
+ metadata.gz: fc55d904ebe65b0a5d464481ef0075dcb3271e8ccd56143b3ea2b04600100a41
4
+ data.tar.gz: e3aab9ed25ac14fc964eff4b2032322679802bb8f627286e60ab7bdc14d80289
5
5
  SHA512:
6
- metadata.gz: f70df4500b95cd35c2a3fb245e384f94bbf4051da6528d6b9bd76a9a38e81838844cc68276ab0b7fc3833982252b9d61c1c5c3b414e22422049e2a91f2c24a4b
7
- data.tar.gz: 6cd678e1288d06f6997c01c056b898c900ba9b7fcde2bacdd3a35b113d0ab4d33d3e5e2c2fde9a31d1c9c351b615b93f28bcfa305eb7489b4deda1c1c801ce45
6
+ metadata.gz: 361f37d7bcb4546d0ff92783bdc3018068cb47d411b80595fdcd003c8d62f007e9483ccce46292f7b1fe5a9828ef4e9779f7553ad747e21c388bf39de0b12487
7
+ data.tar.gz: 04db6b2d4d939d58e49265dc3c000b82455b857e1634987c4c5218ae716e7ed9d4e7f7950964d56aa484c29ee11290b32721e3000c1bc51022edee14eb417d2b
@@ -132,10 +132,13 @@ To use this skill, simply say:
132
132
 
133
133
  6. **Sync scripts/ to OSS**
134
134
 
135
- After updating latest.txt, sync all files in `scripts/` to OSS so users always get the latest install scripts:
135
+ After updating latest.txt, first rebuild all shell scripts from templates, then sync to OSS:
136
136
 
137
137
  ```bash
138
- # Upload each script file to OSS
138
+ # Step 1: Rebuild .sh files from .sh.cc templates
139
+ bash scripts/build/build.sh
140
+
141
+ # Step 2: Upload each script file to OSS
139
142
  for script in scripts/*; do
140
143
  coscli cp "$script" cos://clackyai-1258723534/clacky-ai/openclacky/main/scripts/$(basename "$script")
141
144
  done
@@ -144,7 +147,7 @@ To use this skill, simply say:
144
147
  curl -fsSL https://oss.1024code.com/clacky-ai/openclacky/main/scripts/install.sh | head -5
145
148
  ```
146
149
 
147
- This ensures `scripts/install.sh`, `scripts/install_simple.sh`, `scripts/install.ps1`, `scripts/uninstall.sh` and any future scripts are all mirrored on OSS.
150
+ This ensures `scripts/install.sh`, `scripts/install_simple.sh`, `scripts/install.ps1`, `scripts/uninstall.sh` and any future scripts are compiled from latest templates and mirrored on OSS.
148
151
 
149
152
  > **Prerequisite**: Same `coscli` setup as above
150
153
 
@@ -338,7 +341,8 @@ echo "X.Y.Z" > /tmp/latest.txt
338
341
  coscli cp /tmp/latest.txt cos://clackyai-1258723534/openclacky/latest.txt
339
342
  curl -fsSL https://oss.1024code.com/openclacky/latest.txt # verify
340
343
 
341
- # Sync scripts/ to OSS
344
+ # Sync scripts/ to OSS (build from templates first)
345
+ bash scripts/build/build.sh
342
346
  for script in scripts/*; do
343
347
  coscli cp "$script" cos://clackyai-1258723534/clacky-ai/openclacky/main/scripts/$(basename "$script")
344
348
  done
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.27] - 2026-04-07
11
+
12
+ ### Added
13
+ - **Image understanding in file_reader**: the agent can now read and analyse images (PNG, JPG, GIF, WebP) by sending them to the vision API — just attach or reference an image file
14
+ - **Image auto-resize before upload**: large images are automatically resized to fit within model limits (max 5 MB base64), so vision requests no longer fail on high-resolution files
15
+ - **Rails project installer script**: new `install_rails_deps.sh` script sets up a complete Ruby on Rails development environment (Ruby, Bundler, Node, Yarn, PostgreSQL) in one command
16
+ - **Uninstall script**: new `scripts/uninstall.sh` to cleanly remove the openclacky gem and its associated files
17
+ - **Shell script build system**: `scripts/build/` now contains a template compiler (`.sh.cc` → `.sh`) with dependency checking — install scripts are generated from composable library modules
18
+ - **stdout streaming in Web UI**: agent tool output and shell results are now streamed live to the browser as they arrive, rather than waiting for a full response
19
+ - **Ctrl+O shortcut in CLI**: pressing Ctrl+O in the terminal UI opens a file/output viewer for the current session
20
+
21
+ ### Improved
22
+ - **Smart error recovery on 400 responses**: the agent now rolls back its message history when an API request is rejected as malformed (BadRequestError), preventing the same bad message from being replayed on every subsequent turn
23
+ - **Brand skill reliability**: brand skills now auto-retry on transient failures and fall back gracefully if the remote skill is temporarily unavailable
24
+ - **Shell tool RC file loading**: shell commands now correctly source `.bashrc` / `.zshrc` so user-defined aliases and environment variables are available inside tool executions
25
+ - **Shell UTF-8 encoding**: fixed a warning about character encoding when shell output contains non-ASCII characters
26
+
27
+ ### Fixed
28
+ - **Shell UTF-8 warning suppression**: eliminated noisy encoding warnings that appeared in shell tool output on some macOS setups
29
+
30
+ ### More
31
+ - Lite mode configuration groundwork for clackyai platform
32
+ - Rails new-project skill updated with improved environment checker
33
+ - `new` skill improvements: http_server integration and tool category support
34
+
10
35
  ## [0.9.26] - 2026-04-03
11
36
 
12
37
  ### Added
@@ -379,9 +379,6 @@ module Clacky
379
379
  # @param arguments [String] Arguments for the skill
380
380
  # @return [String] Summary of subagent execution
381
381
  def execute_skill_with_subagent(skill, arguments)
382
- # Log subagent fork
383
- @ui&.show_info("Subagent start: #{skill.identifier}")
384
-
385
382
  # For encrypted brand skills with supporting scripts: decrypt to a tmpdir.
386
383
  # Subagent path has a clear boundary (subagent.run returns), so we shred inline
387
384
  # rather than registering on the parent agent.
@@ -406,6 +403,10 @@ module Clacky
406
403
  system_prompt_suffix: skill_instructions
407
404
  )
408
405
 
406
+ # Log which model the subagent is actually using (may differ from requested
407
+ # when "lite" falls back to default due to no lite model configured)
408
+ @ui&.show_info("Subagent start: #{skill.identifier} [#{subagent.current_model_info[:model]}]")
409
+
409
410
  # Run subagent with the actual task as the sole user turn.
410
411
  # If the user typed the skill command with no arguments (e.g. "/jade-appraisal"),
411
412
  # use a generic trigger phrase so the user message is never empty.
@@ -446,8 +447,14 @@ module Clacky
446
447
  m[:skill_name] = skill.identifier
447
448
  end
448
449
 
450
+ # Merge subagent cost into parent agent's total so the sessionbar reflects
451
+ # the real cumulative spend across all subagents
452
+ subagent_cost = result[:total_cost_usd] || 0.0
453
+ @total_cost += subagent_cost
454
+ @ui&.update_sessionbar(cost: @total_cost)
455
+
449
456
  # Log completion
450
- @ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{result[:total_cost_usd].round(4)}")
457
+ @ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{subagent_cost.round(4)} (total: $#{@total_cost.round(4)})")
451
458
 
452
459
  # Return summary as the skill execution result
453
460
  summary
data/lib/clacky/agent.rb CHANGED
@@ -432,6 +432,11 @@ module Clacky
432
432
  }
433
433
  Clacky::Logger.error("agent_run_error", error: e)
434
434
 
435
+ # 400 errors mean our request was malformed — roll back history so the bad
436
+ # message is not replayed on the next user turn.
437
+ # Other errors (auth, network, etc.) leave history intact for retry.
438
+ @pending_error_rollback = true if e.is_a?(Clacky::BadRequestError)
439
+
435
440
  # Build error result for session data, but let CLI handle error display
436
441
  result = build_result(:error, error: e.message) # rubocop:disable Lint/UselessAssignment
437
442
  raise
@@ -647,35 +652,24 @@ module Clacky
647
652
  # Automatic progress display after 2 seconds for any tool execution
648
653
  progress_shown = false
649
654
  progress_timer = nil
650
- output_buffer = nil
651
655
 
652
656
  if @ui
653
657
  progress_message = build_tool_progress_message(call[:name], args)
654
658
 
655
- # For shell commands, create shared output buffer
656
- if call[:name] == "shell" || call[:name] == "safe_shell"
657
- output_buffer = { content: "", timestamp: Time.now }
658
- args[:output_buffer] = output_buffer
659
+ # For shell/safe_shell: inject on_output callback for real-time stdout streaming.
660
+ # The callback fires immediately on each read_nonblock chunk — no polling delay.
661
+ if (call[:name] == "shell" || call[:name] == "safe_shell") &&
662
+ @ui.respond_to?(:show_tool_stdout)
663
+ args[:on_output] = ->(stream, data) {
664
+ @ui.show_tool_stdout([data]) if stream == :stdout
665
+ }
659
666
  end
660
667
 
661
668
  progress_timer = Thread.new do
662
669
  sleep 2
663
- @ui.show_progress(progress_message, prefix_newline: false, output_buffer: output_buffer)
670
+ @ui.show_progress(progress_message, prefix_newline: false)
664
671
  progress_shown = true
665
-
666
- # For shell commands: stream new stdout lines to WebUI as they arrive
667
- if output_buffer && @ui.respond_to?(:show_tool_stdout)
668
- last_sent_count = 0
669
- loop do
670
- sleep 1
671
- stdout_lines = output_buffer[:stdout_lines]&.to_a || []
672
- new_lines = stdout_lines[last_sent_count..]
673
- if new_lines && !new_lines.empty?
674
- @ui.show_tool_stdout(new_lines)
675
- last_sent_count = stdout_lines.size
676
- end
677
- end
678
- end
672
+ # Streaming is handled by on_output callback — no polling loop needed here
679
673
  end
680
674
  end
681
675
 
@@ -222,9 +222,45 @@ module Clacky
222
222
  end
223
223
  end
224
224
 
225
+ # Auto-inject lite model from provider preset when:
226
+ # 1. A default model exists
227
+ # 2. No lite model is configured yet (neither in file nor env)
228
+ # 3. The default model's provider has a known lite_model
229
+ # The injected lite model is runtime-only (not persisted to config.yml)
230
+ inject_provider_lite_model(models)
231
+
225
232
  new(models: models)
226
233
  end
227
234
 
235
+ # Auto-inject a lite model entry if the default model's provider supports one
236
+ # and no lite model is already present. The injected entry reuses the same
237
+ # api_key and base_url as the default model — only the model name differs.
238
+ # @param models [Array<Hash>] mutable models array (modified in-place)
239
+ private_class_method def self.inject_provider_lite_model(models)
240
+ return if models.any? { |m| m["type"] == "lite" }
241
+
242
+ default_model = models.find { |m| m["type"] == "default" } || models.first
243
+ return unless default_model
244
+
245
+ provider_id = Clacky::Providers.find_by_base_url(default_model["base_url"])
246
+ return unless provider_id
247
+
248
+ lite_model_name = Clacky::Providers.lite_model(provider_id)
249
+ return unless lite_model_name
250
+
251
+ # Don't inject if the default model IS the lite model
252
+ return if default_model["model"] == lite_model_name
253
+
254
+ models << {
255
+ "api_key" => default_model["api_key"],
256
+ "base_url" => default_model["base_url"],
257
+ "model" => lite_model_name,
258
+ "anthropic_format" => default_model["anthropic_format"] || false,
259
+ "type" => "lite",
260
+ "auto_injected" => true # Mark as auto-injected (not saved to file)
261
+ }
262
+ end
263
+
228
264
  # Save configuration to file
229
265
  # Deep copy — models array contains mutable Hashes, so a shallow dup would
230
266
  # let the copy share the same Hash objects with the original, causing
@@ -244,8 +280,11 @@ module Clacky
244
280
  end
245
281
 
246
282
  # Convert to YAML format (top-level array)
283
+ # Auto-injected lite models (auto_injected: true) are excluded from persistence —
284
+ # they are regenerated at load time from the provider preset.
247
285
  def to_yaml
248
- YAML.dump(@models)
286
+ persistable = @models.reject { |m| m["auto_injected"] }
287
+ YAML.dump(persistable)
249
288
  end
250
289
 
251
290
  # Check if any model is configured
@@ -33,11 +33,6 @@ module Clacky
33
33
  CONFIG_DIR = File.join(Dir.home, ".clacky")
34
34
  BRAND_FILE = File.join(CONFIG_DIR, "brand.yml")
35
35
 
36
- # OpenClacky Cloud API base URL.
37
- # Override with CLACKY_LICENSE_SERVER env var for local development:
38
- # CLACKY_LICENSE_SERVER=http://localhost:3000 bundle exec ruby bin/clacky server
39
- API_BASE_URL = ENV.fetch("CLACKY_LICENSE_SERVER", "https://www.openclacky.com")
40
-
41
36
  # How often to send a heartbeat (seconds) — once per day
42
37
  HEARTBEAT_INTERVAL = 86_400
43
38
 
@@ -285,9 +280,6 @@ module Clacky
285
280
  return { success: false, error: "License not activated" } unless activated?
286
281
  return { success: false, error: "User license required to upload skills" } unless user_licensed?
287
282
 
288
- require "net/http"
289
- require "uri"
290
-
291
283
  # The client skills API uses @license_user_id (the platform owner user id),
292
284
  # NOT the user_id embedded in the license key structure.
293
285
  user_id = @license_user_id.to_s
@@ -299,23 +291,17 @@ module Clacky
299
291
 
300
292
  # POST /api/v1/client/skills → create (first upload)
301
293
  # PATCH /api/v1/client/skills/:name → update (force overwrite)
302
- if force
303
- uri = URI.parse("#{API_BASE_URL}/api/v1/client/skills/#{URI.encode_www_form_component(skill_name)}")
304
- else
305
- uri = URI.parse("#{API_BASE_URL}/api/v1/client/skills")
306
- end
307
-
308
- http = Net::HTTP.new(uri.host, uri.port)
309
- http.use_ssl = uri.scheme == "https"
310
- http.open_timeout = 15
311
- http.read_timeout = 60
294
+ path = if force
295
+ "/api/v1/client/skills/#{URI.encode_www_form_component(skill_name)}"
296
+ else
297
+ "/api/v1/client/skills"
298
+ end
312
299
 
313
300
  boundary = "----ClackySkillUpload#{SecureRandom.hex(8)}"
314
301
  crlf = "\r\n"
315
302
 
316
- # Build multipart body as a binary string using Array#pack so that null bytes
317
- # in the ZIP data are preserved. Net::HTTP's body= raises on null bytes in
318
- # the body string — avoid by writing all parts as binary and using body_stream.
303
+ # Build multipart body as a binary string so that null bytes in the ZIP
304
+ # data are preserved. All parts are joined as binary before sending.
319
305
  parts = []
320
306
  fields = {
321
307
  "key_hash" => key_hash,
@@ -339,34 +325,34 @@ module Clacky
339
325
  parts << "--#{boundary}#{crlf}"
340
326
  parts << "Content-Disposition: form-data; name=\"skill_zip\"; filename=\"#{skill_name}.zip\"#{crlf}"
341
327
  parts << "Content-Type: application/zip#{crlf}#{crlf}"
342
- # zip_data is binary — keep as-is
343
328
  parts << zip_data.b
344
329
  parts << "#{crlf}--#{boundary}--#{crlf}"
345
330
 
346
- # Concatenate all parts as a single binary string
347
- body_bytes = parts.map { |p| p.b }.join
331
+ body_bytes = parts.map(&:b).join
348
332
 
349
- request = force ? Net::HTTP::Patch.new(uri.path) : Net::HTTP::Post.new(uri.path)
350
- request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
351
- request["Content-Length"] = body_bytes.bytesize.to_s
352
- request.body_stream = StringIO.new(body_bytes)
333
+ # Delegate sending (with retry + failover) to PlatformHttpClient.
334
+ # Uploads can be slow so we allow a generous 60-second read timeout.
335
+ result = if force
336
+ platform_client.multipart_patch(path, body_bytes, boundary, read_timeout: 60)
337
+ else
338
+ platform_client.multipart_post(path, body_bytes, boundary, read_timeout: 60)
339
+ end
353
340
 
354
- response = http.request(request)
355
- parsed = JSON.parse(response.body) rescue {}
356
-
357
- code_i = response.code.to_i
358
- if code_i == 200 || code_i == 201
341
+ if result[:success]
342
+ parsed = result[:data]
359
343
  { success: true, skill: parsed["skill"] }
360
344
  else
361
- # Server returns { status: "error", code: "...", errors: [...] }
362
- code = parsed["code"] || parsed["error"]
363
- errors = parsed["errors"]&.join(", ")
364
- msg = [code, errors].compact.join(": ")
365
- msg = "Upload failed (HTTP #{response.code})" if msg.empty?
366
-
367
- # Detect "already exists" conflicts (HTTP 409 or name_taken error code)
368
- # so the caller can offer the user an overwrite option.
369
- already_exists = code_i == 409 || code.to_s.include?("name_taken") || code.to_s.include?("already")
345
+ # Propagate structured error from PlatformHttpClient
346
+ body = result[:data] || {}
347
+ code = body["code"] || body["error"]
348
+ errors = body["errors"]&.join(", ")
349
+ msg = result[:error] || [code, errors].compact.join(": ")
350
+ msg = "Upload failed" if msg.to_s.strip.empty?
351
+
352
+ # Detect "already exists" conflicts so the caller can offer an overwrite option.
353
+ already_exists = body["code"].to_s.include?("name_taken") ||
354
+ body["code"].to_s.include?("already") ||
355
+ result[:error].to_s.include?("HTTP 409")
370
356
  { success: false, error: msg, already_exists: already_exists }
371
357
  end
372
358
  rescue StandardError => e
@@ -1158,49 +1144,17 @@ module Clacky
1158
1144
  nil
1159
1145
  end
1160
1146
 
1161
- # POST JSON to the API and return { success:, data:, error: }.
1147
+ # POST JSON to the platform API with automatic retry and domain failover.
1148
+ # Returns { success:, data:, error: }.
1162
1149
  private def api_post(path, payload)
1163
- require "net/http"
1164
- require "uri"
1165
-
1166
- uri = URI.parse("#{API_BASE_URL}#{path}")
1167
- http = Net::HTTP.new(uri.host, uri.port)
1168
- http.use_ssl = uri.scheme == "https"
1169
- http.open_timeout = 10
1170
- http.read_timeout = 15
1171
-
1172
- request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
1173
- request.body = JSON.generate(payload)
1174
-
1175
- response = http.request(request)
1176
- body = JSON.parse(response.body) rescue {}
1177
-
1178
- if response.code.to_i == 200
1179
- { success: true, data: body["data"] || body }
1180
- else
1181
- error_msg = map_api_error(body["code"])
1182
- { success: false, error: error_msg, data: body }
1183
- end
1184
- rescue StandardError => e
1185
- { success: false, error: "Network error: #{e.message}", data: {} }
1150
+ platform_client.post(path, payload)
1186
1151
  end
1187
1152
 
1188
- # Map API error codes to human-readable messages.
1189
- API_ERROR_MESSAGES = {
1190
- "invalid_proof" => "Invalid license key — please check and try again.",
1191
- "invalid_signature" => "Invalid request signature.",
1192
- "nonce_replayed" => "Duplicate request detected. Please try again.",
1193
- "timestamp_expired" => "System clock is out of sync. Please adjust your time settings.",
1194
- "license_revoked" => "This license has been revoked. Please contact support.",
1195
- "license_expired" => "This license has expired. Please renew to continue.",
1196
- "device_limit_reached" => "Device limit reached for this license.",
1197
- "device_revoked" => "This device has been revoked from the license.",
1198
- "invalid_license" => "License key not found. Please verify the key.",
1199
- "device_not_found" => "Device not registered. Please re-activate."
1200
- }.freeze
1201
-
1202
- private def map_api_error(code)
1203
- API_ERROR_MESSAGES[code] || "Activation failed (#{code || 'unknown error'}). Please contact support."
1153
+ # Lazy-initialised PlatformHttpClient, respecting CLACKY_LICENSE_SERVER override.
1154
+ private def platform_client
1155
+ @platform_client ||= Clacky::PlatformHttpClient.new(
1156
+ base_url: ENV["CLACKY_LICENSE_SERVER"]
1157
+ )
1204
1158
  end
1205
1159
  end
1206
1160
  end
data/lib/clacky/client.rb CHANGED
@@ -291,9 +291,19 @@ module Clacky
291
291
 
292
292
  case response.status
293
293
  when 400
294
- hint = error_message.downcase.match?(/unavailable|quota/) ? " (possibly out of credits)" : ""
295
- raise AgentError, "API request failed (400): #{error_message}#{hint}"
294
+ # Well-behaved APIs (Anthropic, OpenAI) never put quota/availability issues in 400.
295
+ # However, some proxy/relay providers do — so we inspect the message first.
296
+ # Also, Bedrock returns ThrottlingException as 400 instead of 429.
297
+ if error_message.match?(/ThrottlingException|unavailable|quota/i)
298
+ hint = error_message.match?(/quota/i) ? " (possibly out of credits)" : ""
299
+ raise RetryableError, "Rate limit or service issue (400): #{error_message}#{hint}"
300
+ end
301
+
302
+ # True bad request — our message was malformed. Roll back history so the
303
+ # broken message is not replayed on the next user turn.
304
+ raise BadRequestError, "API request failed (400): #{error_message}"
296
305
  when 401 then raise AgentError, "Invalid API key"
306
+ when 402 then raise AgentError, "Billing or payment issue (possibly out of credits): #{error_message}"
297
307
  when 403 then raise AgentError, "Access denied: #{error_message}"
298
308
  when 404 then raise AgentError, "API endpoint not found: #{error_message}"
299
309
  when 429 then raise RetryableError, "Rate limit exceeded, please wait a moment"
@@ -3,6 +3,7 @@ name: code-explorer
3
3
  description: Use this skill when exploring, analyzing, or understanding project/code structure. Required for tasks like "analyze project", "explore codebase", "understand how X works".
4
4
  agent: coding
5
5
  fork_agent: true
6
+ model: lite
6
7
  forbidden_tools:
7
8
  - write
8
9
  - edit
@@ -16,6 +16,26 @@ When user wants to create a new Rails project:
16
16
 
17
17
  ## Process Steps
18
18
 
19
+ ### 0. Ask Project Type and Requirement
20
+ Before doing anything, use `request_user_feedback` to ask the user two things:
21
+
22
+ ```
23
+ project_type: "demo" or "production"
24
+ requirement: one-sentence description of what they want to build
25
+ ```
26
+
27
+ Card content:
28
+ - Title: "🚀 New Project"
29
+ - Two options for project type:
30
+ - **⚡ Demo** — no database, AI builds freely, quick prototype
31
+ - **🏗️ Production** — real app, ready to deploy, full Rails setup
32
+ - One text input: "Describe your project in one sentence"
33
+ - Confirm button: "Let's go!"
34
+
35
+ **Based on user's choice:**
36
+ - If **Demo**: do NOT follow the Rails setup steps below. Instead, freely build a simple HTML/CSS/JS (or React) prototype directly in the working directory based on their requirement. Use your creativity.
37
+ - If **Production**: continue with steps 1–3 below (full Rails flow).
38
+
19
39
  ### 1. Check Directory Before Starting
20
40
  Before running the setup script, check if current directory is empty:
21
41
  - Use glob tool to check if directory has files: `glob("*", base_path: ".")`
@@ -38,7 +58,7 @@ The script will automatically:
38
58
 
39
59
  **Step 2: Check Environment**
40
60
  - Run rails_env_checker.sh to verify dependencies:
41
- - Ruby >= 3.0.0 (must be pre-installed)
61
+ - Ruby >= 3.3.0 (auto-installed via mise if missing or too old — supports CN mirrors)
42
62
  - Node.js >= 22.0.0 (will install automatically if missing on macOS/Ubuntu)
43
63
  - PostgreSQL (will install automatically if missing on macOS/Ubuntu)
44
64
  - Script automatically installs missing dependencies without prompting
@@ -76,7 +96,7 @@ What would you like to develop next?
76
96
  ## Error Handling
77
97
  - Directory not empty → Ask user confirmation, abort if declined
78
98
  - Git clone fails → Check network connection, verify repository URL
79
- - Ruby not installed Error message, user must install Ruby 3.x manually
99
+ - Ruby < 3.3 or missing **Automatically installs Ruby 3.3 via mise** (with CN mirror support); exits with instructions if mise install fails
80
100
  - Node.js < 22 → Script installs automatically (macOS/Ubuntu)
81
101
  - PostgreSQL missing → Script installs automatically (macOS/Ubuntu)
82
102
  - bin/setup fails → Show error, suggest running `./bin/setup` manually
@@ -33,16 +33,13 @@ print_step() {
33
33
  echo -e "\n${BLUE}==>${NC} $1"
34
34
  }
35
35
 
36
- # Get script directory
37
- SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
38
-
39
36
  # Check if current directory is empty
40
37
  check_current_directory() {
41
38
  print_step "Checking current directory..."
42
-
39
+
43
40
  local current_dir=$(pwd)
44
41
  print_info "Working in: $current_dir"
45
-
42
+
46
43
  # Check if directory is empty (silently continue if not)
47
44
  if [ "$(ls -A .)" ]; then
48
45
  print_warning "Current directory is not empty - continuing anyway"
@@ -54,11 +51,11 @@ check_current_directory() {
54
51
  # Clone template to current directory
55
52
  clone_template() {
56
53
  print_step "Cloning Rails template..."
57
-
54
+
58
55
  # Create temporary directory
59
56
  local temp_dir=$(mktemp -d)
60
57
  print_info "Using temporary directory: $temp_dir"
61
-
58
+
62
59
  # Clone template to temp directory
63
60
  print_info "Downloading template from GitHub..."
64
61
  if git clone https://github.com/clacky-ai/rails-template-7x-starter.git "$temp_dir" >/dev/null 2>&1; then
@@ -68,18 +65,18 @@ clone_template() {
68
65
  rm -rf "$temp_dir"
69
66
  exit 1
70
67
  fi
71
-
68
+
72
69
  # Move all files to current directory
73
70
  print_info "Moving files to current directory..."
74
71
  mv "$temp_dir"/* "$temp_dir"/.* . 2>/dev/null || true
75
-
72
+
76
73
  # Delete .git directory
77
74
  rm -rf .git
78
-
75
+
79
76
  # Clean up temp directory
80
77
  rm -rf "$temp_dir"
81
78
  print_success "Template files copied to current directory"
82
-
79
+
83
80
  # Initialize new git repository
84
81
  print_info "Initializing git repository..."
85
82
  git init > /dev/null 2>&1
@@ -88,37 +85,37 @@ clone_template() {
88
85
  print_success "Git repository initialized"
89
86
  }
90
87
 
91
- # Check environment dependencies
88
+ # Check and install environment dependencies
92
89
  check_environment() {
93
90
  print_step "Checking environment dependencies..."
94
-
95
- # Run rails_env_checker.sh
96
- if [ -f "$SCRIPT_DIR/rails_env_checker.sh" ]; then
97
- if bash "$SCRIPT_DIR/rails_env_checker.sh"; then
98
- print_success "Environment check passed"
99
- return 0
100
- else
101
- print_error "Environment check failed"
102
- return 1
103
- fi
104
- else
105
- print_warning "rails_env_checker.sh not found, skipping environment check"
106
- print_info "Please ensure you have Ruby 3.x, Node.js 22+, and PostgreSQL installed"
91
+
92
+ local installer="$HOME/.clacky/scripts/install_rails_deps.sh"
93
+ if [ ! -f "$installer" ]; then
94
+ print_warning "install_rails_deps.sh not found at $installer"
95
+ print_info "Please ensure Ruby 3.3+, Node.js 22+, and PostgreSQL are installed"
96
+ return 1
97
+ fi
98
+
99
+ if bash "$installer"; then
100
+ print_success "Environment ready"
107
101
  return 0
102
+ else
103
+ print_error "Environment setup failed"
104
+ return 1
108
105
  fi
109
106
  }
110
107
 
111
108
  # Run project setup
112
109
  run_project_setup() {
113
110
  print_step "Running project setup..."
114
-
111
+
115
112
  if [ ! -f "./bin/setup" ]; then
116
113
  print_error "bin/setup not found"
117
114
  return 1
118
115
  fi
119
-
116
+
120
117
  chmod +x ./bin/setup
121
-
118
+
122
119
  if ./bin/setup; then
123
120
  print_success "Project setup completed"
124
121
  return 0
@@ -139,27 +136,27 @@ main() {
139
136
  echo "║ ║"
140
137
  echo "╚═══════════════════════════════════════════════════════════╝"
141
138
  echo ""
142
-
139
+
143
140
  # Check current directory
144
141
  check_current_directory
145
-
142
+
146
143
  # Clone template
147
144
  if ! clone_template; then
148
145
  exit 1
149
146
  fi
150
-
147
+
151
148
  # Check environment
152
149
  if ! check_environment; then
153
150
  print_error "Please fix environment issues and run ./bin/setup manually"
154
151
  exit 1
155
152
  fi
156
-
153
+
157
154
  # Run project setup
158
155
  if ! run_project_setup; then
159
156
  print_error "Setup failed. You can try running './bin/setup' manually"
160
157
  exit 1
161
158
  fi
162
-
159
+
163
160
  # Project is ready
164
161
  echo ""
165
162
  echo "╔═══════════════════════════════════════════════════════════╗"