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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +87 -53
- data/lib/clacky/agent/cost_tracker.rb +19 -2
- data/lib/clacky/agent/llm_caller.rb +218 -0
- data/lib/clacky/agent/message_compressor_helper.rb +32 -2
- data/lib/clacky/agent.rb +54 -22
- data/lib/clacky/client.rb +44 -5
- data/lib/clacky/default_parsers/pdf_parser.rb +58 -17
- data/lib/clacky/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/clacky/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +201 -77
- data/lib/clacky/default_skills/new/SKILL.md +3 -114
- data/lib/clacky/default_skills/onboard/SKILL.md +349 -133
- data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +371 -0
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +175 -0
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +59 -26
- data/lib/clacky/message_format/anthropic.rb +72 -8
- data/lib/clacky/message_format/bedrock.rb +6 -3
- data/lib/clacky/providers.rb +146 -3
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +14 -0
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +10 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +1 -0
- data/lib/clacky/server/channel/channel_manager.rb +12 -4
- data/lib/clacky/server/channel/channel_ui_controller.rb +8 -2
- data/lib/clacky/server/http_server.rb +746 -13
- data/lib/clacky/server/session_registry.rb +55 -24
- data/lib/clacky/skill.rb +10 -9
- data/lib/clacky/skill_loader.rb +23 -11
- data/lib/clacky/tools/file_reader.rb +232 -127
- data/lib/clacky/tools/security.rb +42 -64
- data/lib/clacky/tools/terminal/persistent_session.rb +15 -4
- data/lib/clacky/tools/terminal/safe_rm.sh +106 -0
- data/lib/clacky/tools/terminal/session_manager.rb +8 -3
- data/lib/clacky/tools/terminal.rb +263 -16
- data/lib/clacky/ui2/layout_manager.rb +8 -1
- data/lib/clacky/ui2/output_buffer.rb +83 -23
- data/lib/clacky/ui2/ui_controller.rb +74 -7
- data/lib/clacky/utils/file_processor.rb +14 -40
- data/lib/clacky/utils/model_pricing.rb +215 -0
- data/lib/clacky/utils/parser_manager.rb +70 -6
- data/lib/clacky/utils/string_matcher.rb +23 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +673 -9
- data/lib/clacky/web/app.js +40 -1608
- data/lib/clacky/web/i18n.js +209 -0
- data/lib/clacky/web/index.html +166 -2
- data/lib/clacky/web/onboard.js +77 -1
- data/lib/clacky/web/profile.js +442 -0
- data/lib/clacky/web/sessions.js +1034 -2
- data/lib/clacky/web/settings.js +127 -6
- data/lib/clacky/web/sidebar.js +39 -0
- data/lib/clacky/web/skills.js +460 -0
- data/lib/clacky/web/trash.js +343 -0
- data/lib/clacky/web/ws-dispatcher.js +255 -0
- data/lib/clacky.rb +5 -3
- metadata +16 -17
- data/lib/clacky/clacky_auth_client.rb +0 -152
- data/lib/clacky/clacky_cloud_config.rb +0 -123
- data/lib/clacky/cloud_project_client.rb +0 -169
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +0 -1377
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +0 -116
- data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +0 -341
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +0 -99
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +0 -77
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +0 -67
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +0 -67
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +0 -189
- data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +0 -74
- 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.
|
|
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:
|
|
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:
|
|
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
|