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
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-07 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: faraday
@@ -252,8 +253,8 @@ email:
252
253
  - yafei@dao42.com
253
254
  executables:
254
255
  - clacky
255
- - clarky
256
256
  - openclacky
257
+ - clarky
257
258
  extensions: []
258
259
  extra_rdoc_files: []
259
260
  files:
@@ -318,11 +319,8 @@ files:
318
319
  - lib/clacky/banner.rb
319
320
  - lib/clacky/block_font.rb
320
321
  - lib/clacky/brand_config.rb
321
- - lib/clacky/clacky_auth_client.rb
322
- - lib/clacky/clacky_cloud_config.rb
323
322
  - lib/clacky/cli.rb
324
323
  - lib/clacky/client.rb
325
- - lib/clacky/cloud_project_client.rb
326
324
  - lib/clacky/default_agents/SOUL.md
327
325
  - lib/clacky/default_agents/USER.md
328
326
  - lib/clacky/default_agents/base_prompt.md
@@ -333,6 +331,8 @@ files:
333
331
  - lib/clacky/default_parsers/doc_parser.rb
334
332
  - lib/clacky/default_parsers/docx_parser.rb
335
333
  - lib/clacky/default_parsers/pdf_parser.rb
334
+ - lib/clacky/default_parsers/pdf_parser_ocr.py
335
+ - lib/clacky/default_parsers/pdf_parser_plumber.py
336
336
  - lib/clacky/default_parsers/pptx_parser.rb
337
337
  - lib/clacky/default_parsers/xlsx_parser.rb
338
338
  - lib/clacky/default_skills/browser-setup/SKILL.md
@@ -343,18 +343,11 @@ files:
343
343
  - lib/clacky/default_skills/cron-task-creator/SKILL.md
344
344
  - lib/clacky/default_skills/cron-task-creator/evals/evals.json
345
345
  - lib/clacky/default_skills/deploy/SKILL.md
346
- - lib/clacky/default_skills/deploy/scripts/rails_deploy.rb
347
- - lib/clacky/default_skills/deploy/tools/check_health.rb
348
- - lib/clacky/default_skills/deploy/tools/create_database_service.rb
349
- - lib/clacky/default_skills/deploy/tools/execute_deployment.rb
350
- - lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb
351
- - lib/clacky/default_skills/deploy/tools/list_services.rb
352
- - lib/clacky/default_skills/deploy/tools/report_deploy_status.rb
353
- - lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb
354
346
  - lib/clacky/default_skills/new/SKILL.md
355
- - lib/clacky/default_skills/new/scripts/cloud_project_init.sh
356
347
  - lib/clacky/default_skills/new/scripts/create_rails_project.sh
357
348
  - lib/clacky/default_skills/onboard/SKILL.md
349
+ - lib/clacky/default_skills/onboard/scripts/import_external_skills.rb
350
+ - lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb
358
351
  - lib/clacky/default_skills/personal-website/SKILL.md
359
352
  - lib/clacky/default_skills/personal-website/publish.rb
360
353
  - lib/clacky/default_skills/product-help/SKILL.md
@@ -377,7 +370,6 @@ files:
377
370
  - lib/clacky/default_skills/skill-creator/scripts/run_loop.py
378
371
  - lib/clacky/default_skills/skill-creator/scripts/utils.py
379
372
  - lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb
380
- - lib/clacky/deploy_api_client.rb
381
373
  - lib/clacky/idle_compression_timer.rb
382
374
  - lib/clacky/json_ui_controller.rb
383
375
  - lib/clacky/message_format/anthropic.rb
@@ -426,6 +418,7 @@ files:
426
418
  - lib/clacky/tools/terminal.rb
427
419
  - lib/clacky/tools/terminal/output_cleaner.rb
428
420
  - lib/clacky/tools/terminal/persistent_session.rb
421
+ - lib/clacky/tools/terminal/safe_rm.sh
429
422
  - lib/clacky/tools/terminal/session_manager.rb
430
423
  - lib/clacky/tools/todo_manager.rb
431
424
  - lib/clacky/tools/trash_manager.rb
@@ -491,13 +484,17 @@ files:
491
484
  - lib/clacky/web/index.html
492
485
  - lib/clacky/web/marked.min.js
493
486
  - lib/clacky/web/onboard.js
487
+ - lib/clacky/web/profile.js
494
488
  - lib/clacky/web/sessions.js
495
489
  - lib/clacky/web/settings.js
490
+ - lib/clacky/web/sidebar.js
496
491
  - lib/clacky/web/skills.js
497
492
  - lib/clacky/web/tasks.js
498
493
  - lib/clacky/web/theme.js
494
+ - lib/clacky/web/trash.js
499
495
  - lib/clacky/web/version.js
500
496
  - lib/clacky/web/weixin-qr.html
497
+ - lib/clacky/web/ws-dispatcher.js
501
498
  - lib/clacky/web/ws.js
502
499
  - scripts/build/build.sh
503
500
  - scripts/build/lib/apt.sh
@@ -529,6 +526,7 @@ metadata:
529
526
  homepage_uri: https://github.com/yafeilee/clacky
530
527
  source_code_uri: https://github.com/yafeilee/clacky
531
528
  changelog_uri: https://github.com/yafeilee/clacky/blob/main/CHANGELOG.md
529
+ post_install_message:
532
530
  rdoc_options: []
533
531
  require_paths:
534
532
  - lib
@@ -543,7 +541,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
543
541
  - !ruby/object:Gem::Version
544
542
  version: '0'
545
543
  requirements: []
546
- rubygems_version: 4.0.4
544
+ rubygems_version: 3.5.22
545
+ signing_key:
547
546
  specification_version: 4
548
547
  summary: A command-line interface for AI models (Claude, OpenAI, etc.)
549
548
  test_files: []
@@ -1,152 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "faraday"
4
- require "json"
5
-
6
- module Clacky
7
- # ClackyAuthClient - Fetches LLM keys from a Clacky workspace via API
8
- #
9
- # Usage:
10
- # client = ClackyAuthClient.new("clacky_ak_xxx", base_url: "https://api.example.com")
11
- # result = client.fetch_workspace_keys
12
- # # => { success: true, llm_key: "ABSK...", model_name: "jp.anthropic.claude-sonnet-4-6",
13
- # # base_url: "https://...", anthropic_format: false }
14
- class ClackyAuthClient
15
- WORKSPACE_KEYS_PATH = "/openclacky/v1/workspace/keys"
16
- REQUEST_TIMEOUT = 15 # seconds
17
- OPEN_TIMEOUT = 5 # seconds
18
-
19
- # Default model to use when the workspace/keys response does not specify one
20
- DEFAULT_MODEL = "jp.anthropic.claude-sonnet-4-6"
21
-
22
- def initialize(workspace_api_key, base_url:)
23
- @workspace_api_key = workspace_api_key.to_s.strip
24
- @base_url = base_url.to_s.strip.sub(%r{/+$}, "")
25
- end
26
-
27
- # Fetch workspace keys from the Clacky backend.
28
- #
29
- # @return [Hash]
30
- # On success:
31
- # { success: true,
32
- # llm_key: "...", # raw LLM key string returned by the API (ABSK prefix = Bedrock)
33
- # model_name: "...", # model to configure (provider default or our default)
34
- # base_url: "...", # LLM proxy base URL (clean host, no path suffix)
35
- # anthropic_format: false # ABSK keys use Bedrock Converse format, not Anthropic wire format
36
- # }
37
- # On failure:
38
- # { success: false, error: "..." }
39
- def fetch_workspace_keys
40
- validate_inputs!
41
-
42
- response = connection.get(WORKSPACE_KEYS_PATH)
43
-
44
- unless response.status == 200
45
- error_msg = extract_error(response)
46
- return { success: false, error: "HTTP #{response.status}: #{error_msg}" }
47
- end
48
-
49
- body = JSON.parse(response.body)
50
-
51
- unless body["code"].to_i == 200
52
- return { success: false, error: "API error: #{body["msg"] || body["message"]}" }
53
- end
54
-
55
- llm_key_data = body.dig("data", "llm_key")
56
- if llm_key_data.nil?
57
- return { success: false, error: "No LLM key available for this workspace" }
58
- end
59
-
60
- # Extract key value – the API returns a hash with fields:
61
- # raw_key – plaintext secret (primary field since 2026-03-30)
62
- # key – alias used by some gateway endpoints
63
- # key_id – legacy identifier (kept for forward-compat)
64
- # Priority: raw_key > key > key_id > value
65
- # We also accept a plain string form for forward-compat.
66
- llm_key = case llm_key_data
67
- when String then llm_key_data
68
- when Hash
69
- llm_key_data["raw_key"] || llm_key_data["key"] ||
70
- llm_key_data["key_id"] || llm_key_data["value"]
71
- end
72
-
73
- if llm_key.nil? || llm_key.to_s.strip.empty?
74
- return { success: false, error: "LLM key value is empty or missing in response" }
75
- end
76
-
77
- # base_url comes from the `host` field in the API response (set per environment by backend config).
78
- # Fallback to @base_url (the backend URL the user entered).
79
- # No path suffix is appended — the LLM key has ABSK prefix (Bedrock), so client.rb will
80
- # automatically build the correct endpoint: /model/{model}/converse
81
- host = llm_key_data.is_a?(Hash) ? llm_key_data["host"].to_s.strip : ""
82
- llm_base_url = if host.start_with?("http://", "https://")
83
- host
84
- else
85
- @base_url
86
- end
87
-
88
- {
89
- success: true,
90
- llm_key: llm_key.to_s.strip,
91
- model_name: DEFAULT_MODEL,
92
- base_url: llm_base_url,
93
- anthropic_format: false
94
- }
95
- rescue Faraday::ConnectionFailed => e
96
- { success: false, error: "Connection failed: #{e.message}" }
97
- rescue Faraday::TimeoutError
98
- { success: false, error: "Request timed out (#{REQUEST_TIMEOUT}s)" }
99
- rescue Faraday::Error => e
100
- { success: false, error: "Network error: #{e.message}" }
101
- rescue JSON::ParserError => e
102
- { success: false, error: "Invalid JSON response: #{e.message}" }
103
- rescue ArgumentError => e
104
- { success: false, error: e.message }
105
- rescue => e
106
- { success: false, error: "Unexpected error: #{e.message}" }
107
- end
108
-
109
- # Validate that inputs look reasonable before making a network request.
110
- private def validate_inputs!
111
- if @workspace_api_key.empty?
112
- raise ArgumentError, "Workspace API key is required"
113
- end
114
-
115
- unless @workspace_api_key.start_with?("clacky_ak_")
116
- raise ArgumentError, "Invalid key format (expected prefix: clacky_ak_)"
117
- end
118
-
119
- if @base_url.empty?
120
- raise ArgumentError, "Base URL is required"
121
- end
122
-
123
- unless @base_url.start_with?("http://", "https://")
124
- raise ArgumentError, "Base URL must start with http:// or https://"
125
- end
126
- end
127
-
128
- # Build a Faraday connection pointing at the Clacky backend.
129
- private def connection
130
- @connection ||= Faraday.new(url: @base_url) do |conn|
131
- conn.headers["Content-Type"] = "application/json"
132
- conn.headers["Authorization"] = "Bearer #{@workspace_api_key}"
133
- conn.options.timeout = REQUEST_TIMEOUT
134
- conn.options.open_timeout = OPEN_TIMEOUT
135
- conn.ssl.verify = false
136
- conn.adapter Faraday.default_adapter
137
- end
138
- end
139
-
140
- # Extract a human-readable error from a failed response.
141
- private def extract_error(response)
142
- body = JSON.parse(response.body) rescue nil
143
- return response.body.to_s[0..200] unless body.is_a?(Hash)
144
-
145
- body["msg"] ||
146
- body["message"] ||
147
- body.dig("error", "message") ||
148
- body["error"].to_s[0..200] ||
149
- response.body.to_s[0..200]
150
- end
151
- end
152
- end
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
- require "fileutils"
5
-
6
- module Clacky
7
- # ClackyCloudConfig — stores the Clacky Cloud credentials used for workspace-key
8
- # import (workspace_api_key + backend base_url) in a dedicated file so the user
9
- # never has to re-enter them.
10
- #
11
- # File location: ~/.clacky/clacky_cloud.yml
12
- # File format (YAML):
13
- # workspace_key: clacky_ak_xxxx
14
- # base_url: https://api.clacky.ai
15
- # dashboard_url: https://app.clacky.ai # optional, inferred from base_url if absent
16
- #
17
- # Usage:
18
- # cfg = ClackyCloudConfig.load
19
- # cfg.workspace_key # => "clacky_ak_xxxx" or nil
20
- # cfg.base_url # => "https://api.clacky.ai"
21
- # cfg.dashboard_url # => "https://app.clacky.ai" (explicit or inferred)
22
- # cfg.configured? # => true / false
23
- #
24
- # cfg.workspace_key = "clacky_ak_newkey"
25
- # cfg.save
26
- class ClackyCloudConfig
27
- CONFIG_DIR = File.join(Dir.home, ".clacky")
28
- CONFIG_FILE = File.join(CONFIG_DIR, "clacky_cloud.yml")
29
-
30
- DEFAULT_BASE_URL = "https://api.clacky.ai"
31
- DEFAULT_DASHBOARD_URL = "https://app.clacky.ai"
32
-
33
- attr_accessor :workspace_key, :base_url, :dashboard_url
34
-
35
- def initialize(workspace_key: nil, base_url: DEFAULT_BASE_URL, dashboard_url: nil)
36
- @workspace_key = workspace_key.to_s.strip
37
- @workspace_key = nil if @workspace_key.empty?
38
- @base_url = (base_url.to_s.strip.empty? ? DEFAULT_BASE_URL : base_url.to_s.strip)
39
- .sub(%r{/+$}, "") # strip trailing slash
40
-
41
- # dashboard_url: use explicit value if provided, otherwise infer from base_url
42
- explicit = dashboard_url.to_s.strip.sub(%r{/+$}, "")
43
- @dashboard_url = explicit.empty? ? infer_dashboard_url(@base_url) : explicit
44
- end
45
-
46
- # Load from ~/.clacky/clacky_cloud.yml (returns an empty config if the file is absent)
47
- def self.load(config_file = CONFIG_FILE)
48
- if File.exist?(config_file)
49
- data = YAML.safe_load(File.read(config_file)) || {}
50
- new(
51
- workspace_key: data["workspace_key"],
52
- base_url: data["base_url"] || DEFAULT_BASE_URL,
53
- dashboard_url: data["dashboard_url"]
54
- )
55
- else
56
- new
57
- end
58
- rescue => e
59
- # Corrupt file — return empty config rather than crash
60
- warn "[clacky_cloud_config] Failed to load #{config_file}: #{e.message}"
61
- new
62
- end
63
-
64
- # Persist to ~/.clacky/clacky_cloud.yml
65
- def save(config_file = CONFIG_FILE)
66
- FileUtils.mkdir_p(File.dirname(config_file))
67
- File.write(config_file, to_yaml)
68
- FileUtils.chmod(0o600, config_file)
69
- self
70
- end
71
-
72
- # Serialize to YAML string
73
- def to_yaml
74
- data = { "base_url" => @base_url }
75
- data["workspace_key"] = @workspace_key if @workspace_key
76
- # Only persist dashboard_url when it differs from the inferred default,
77
- # so the file stays minimal for users who don't need to override it.
78
- inferred = infer_dashboard_url(@base_url)
79
- data["dashboard_url"] = @dashboard_url if @dashboard_url != inferred
80
- YAML.dump(data)
81
- end
82
-
83
- # True when a non-empty workspace_key is stored
84
- def configured?
85
- !@workspace_key.nil? && !@workspace_key.empty?
86
- end
87
-
88
- # Remove the saved file (used for reset / tests)
89
- def self.clear!(config_file = CONFIG_FILE)
90
- FileUtils.rm_f(config_file)
91
- end
92
-
93
- # Derive the dashboard web-app URL from the API base_url.
94
- #
95
- # Mapping rules:
96
- # https://api.clacky.ai -> https://app.clacky.ai
97
- # https://<env>.api.clackyai.com -> https://<env>.app.clackyai.com
98
- # http://localhost:<port> -> http://localhost:3001
99
- # (anything else) -> https://app.clacky.ai (safe default)
100
- private def infer_dashboard_url(api_url)
101
- return DEFAULT_DASHBOARD_URL if api_url.nil? || api_url.strip.empty?
102
-
103
- # Production: api.clacky.ai -> app.clacky.ai
104
- return "https://app.clacky.ai" if api_url == "https://api.clacky.ai"
105
-
106
- # Staging/dev on clackyai.com: <env>.api.clackyai.com -> <env>.app.clackyai.com
107
- if api_url =~ %r{\Ahttps?://(.+)\.api\.clackyai\.com\z}
108
- env_prefix = Regexp.last_match(1)
109
- scheme = api_url.start_with?("https") ? "https" : "http"
110
- return "#{scheme}://#{env_prefix}.app.clackyai.com"
111
- end
112
-
113
- # Local development: localhost:<port> -> localhost:3001
114
- if api_url =~ %r{\Ahttps?://localhost(:\d+)?\z}
115
- scheme = api_url.start_with?("https") ? "https" : "http"
116
- return "#{scheme}://localhost:3001"
117
- end
118
-
119
- # Fallback: return production dashboard
120
- DEFAULT_DASHBOARD_URL
121
- end
122
- end
123
- end
@@ -1,169 +0,0 @@
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