openclacky 1.1.2 → 1.1.4

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +27 -31
  3. data/CHANGELOG.md +30 -0
  4. data/Dockerfile +28 -0
  5. data/README.md +4 -0
  6. data/README_CN.md +198 -0
  7. data/docs/engineering-article.md +343 -0
  8. data/lib/clacky/agent/llm_caller.rb +2 -5
  9. data/lib/clacky/agent/session_serializer.rb +4 -0
  10. data/lib/clacky/agent.rb +22 -1
  11. data/lib/clacky/brand_config.rb +87 -5
  12. data/lib/clacky/cli.rb +1 -1
  13. data/lib/clacky/client.rb +15 -11
  14. data/lib/clacky/message_format/anthropic.rb +30 -2
  15. data/lib/clacky/message_format/bedrock.rb +13 -1
  16. data/lib/clacky/message_format/open_ai.rb +5 -1
  17. data/lib/clacky/providers.rb +34 -0
  18. data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +142 -5
  19. data/lib/clacky/server/channel/adapters/dingtalk/api_client.rb +309 -0
  20. data/lib/clacky/server/http_server.rb +130 -15
  21. data/lib/clacky/server/session_registry.rb +9 -6
  22. data/lib/clacky/ui2/ui_controller.rb +14 -0
  23. data/lib/clacky/ui_interface.rb +14 -0
  24. data/lib/clacky/utils/model_pricing.rb +96 -25
  25. data/lib/clacky/version.rb +1 -1
  26. data/lib/clacky/web/app.css +1286 -1116
  27. data/lib/clacky/web/brand.js +20 -5
  28. data/lib/clacky/web/i18n.js +42 -0
  29. data/lib/clacky/web/index.html +26 -7
  30. data/lib/clacky/web/onboard.js +6 -0
  31. data/lib/clacky/web/sessions.js +194 -11
  32. data/lib/clacky/web/settings.js +51 -10
  33. data/lib/clacky/web/skills.js +53 -31
  34. data/lib/clacky/web/vendor/hljs/highlight.min.js +1244 -0
  35. data/lib/clacky/web/vendor/hljs/hljs-theme.css +95 -0
  36. data/scripts/build/lib/apt.sh +30 -10
  37. data/scripts/build/lib/network.sh +3 -2
  38. data/scripts/install.sh +30 -9
  39. data/scripts/install_browser.sh +2 -1
  40. data/scripts/install_full.sh +2 -1
  41. data/scripts/install_rails_deps.sh +30 -9
  42. data/scripts/install_system_deps.sh +30 -9
  43. metadata +7 -17
  44. data/docs/HOW-TO-USE-CN.md +0 -96
  45. data/docs/HOW-TO-USE.md +0 -94
  46. data/docs/browser-cdp-native-design.md +0 -195
  47. data/docs/c-end-user-positioning.md +0 -64
  48. data/docs/config.example.yml +0 -27
  49. data/docs/deploy-architecture.md +0 -619
  50. data/docs/deploy_subagent_design.md +0 -540
  51. data/docs/install-script-simplification.md +0 -89
  52. data/docs/memory-architecture.md +0 -343
  53. data/docs/openclacky_cloud_api_reference.md +0 -584
  54. data/docs/security-design.md +0 -109
  55. data/docs/session-management-redesign.md +0 -202
  56. data/docs/system-skill-authoring-guide.md +0 -47
  57. data/docs/why-developer.md +0 -371
  58. data/docs/why-openclacky.md +0 -266
@@ -19,6 +19,8 @@ const Skills = (() => {
19
19
  let _brandSkills = []; // skills from cloud license API
20
20
  let _activeTab = "my-skills"; // "my-skills" | "brand-skills"
21
21
  let _brandActivated = false; // whether a license is currently active
22
+ let _freeMode = false; // brand-skills tab is showing free-mode skills
23
+ let _paidSkillsCount = 0; // count of premium (encrypted) skills locked behind activation
22
24
  let _domWired = false; // whether one-time DOM listeners have been bound
23
25
  let _showSystemSkills = false; // whether system (source=default) skills are shown
24
26
 
@@ -55,38 +57,14 @@ const Skills = (() => {
55
57
  const res = await fetch("/api/brand/skills");
56
58
  const data = await res.json();
57
59
 
58
- if (res.status === 403 || (data.ok === false && (data.error || "").toLowerCase().includes("not activated"))) {
59
- // License not activated — show a friendly prompt instead of an error
60
- const btn = document.createElement("button");
61
- btn.className = "brand-skills-activate-btn";
62
- btn.textContent = I18n.t("skills.brand.activateBtn");
63
- btn.addEventListener("click", () => {
64
- // Reuse the same behaviour as the top banner: navigate to Settings,
65
- // scroll to the license section, flash it, and focus the input.
66
- if (typeof Brand !== "undefined" && Brand.goToLicenseInput) {
67
- Brand.goToLicenseInput();
68
- } else {
69
- Router.navigate("settings");
70
- }
71
- });
72
-
73
- const wrapper = document.createElement("div");
74
- wrapper.className = "brand-skills-unlicensed";
75
- wrapper.innerHTML = `
76
- <div class="brand-skills-unlicensed-icon">🔒</div>
77
- <div class="brand-skills-unlicensed-msg">${I18n.t("skills.brand.needsActivation")}</div>`;
78
- wrapper.appendChild(btn);
79
- container.innerHTML = "";
80
- container.appendChild(wrapper);
81
- return;
82
- }
83
-
84
60
  if (!res.ok || !data.ok) {
85
61
  container.innerHTML = '<div class="brand-skills-error">' + escapeHtml(data.error || I18n.t("skills.brand.loadFailed")) + "</div>";
86
62
  return;
87
63
  }
88
64
 
89
- _brandSkills = data.skills || [];
65
+ _brandSkills = data.skills || [];
66
+ _freeMode = !!data.free_mode;
67
+ _paidSkillsCount = Number(data.paid_skills_count) || 0;
90
68
 
91
69
  // Soft warning: remote API unavailable but local skills returned.
92
70
  // Prefer the server-provided warning_code for proper i18n; fall back to
@@ -126,7 +104,7 @@ const Skills = (() => {
126
104
  if (!container) return;
127
105
  container.innerHTML = "";
128
106
 
129
- if (_brandSkills.length === 0) {
107
+ if (_brandSkills.length === 0 && !(_freeMode && _paidSkillsCount > 0)) {
130
108
  container.innerHTML = `<div class="brand-skills-empty">${I18n.t("skills.brand.empty")}</div>`;
131
109
  return;
132
110
  }
@@ -135,6 +113,34 @@ const Skills = (() => {
135
113
  const card = _renderBrandSkillCard(skill);
136
114
  container.appendChild(card);
137
115
  });
116
+
117
+ // Free mode + premium skills exist → show a footer hint inviting the user to activate.
118
+ if (_freeMode && _paidSkillsCount > 0) {
119
+ const hint = document.createElement("div");
120
+ hint.className = "brand-skills-paid-hint";
121
+
122
+ const msgEl = document.createElement("div");
123
+ msgEl.className = "brand-skills-paid-hint-msg";
124
+ msgEl.textContent = I18n.t("skills.brand.paidHint", { n: _paidSkillsCount });
125
+ msgEl.setAttribute("data-i18n", "skills.brand.paidHint");
126
+ msgEl.setAttribute("data-i18n-vars", `n=${_paidSkillsCount}`);
127
+
128
+ const btn = document.createElement("button");
129
+ btn.className = "brand-skills-activate-btn";
130
+ btn.textContent = I18n.t("skills.brand.activateBtn");
131
+ btn.setAttribute("data-i18n", "skills.brand.activateBtn");
132
+ btn.addEventListener("click", () => {
133
+ if (typeof Brand !== "undefined" && Brand.goToLicenseInput) {
134
+ Brand.goToLicenseInput();
135
+ } else {
136
+ Router.navigate("settings");
137
+ }
138
+ });
139
+
140
+ hint.appendChild(msgEl);
141
+ hint.appendChild(btn);
142
+ container.appendChild(hint);
143
+ }
138
144
  }
139
145
 
140
146
  /** Render a single brand skill card. */
@@ -164,8 +170,10 @@ const Skills = (() => {
164
170
  <button class="btn-brand-use" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.use")}</button>`;
165
171
  }
166
172
 
167
- // All brand skills are private always show the private badge
168
- const privateBadge = `<span class="brand-skill-badge-private" title="${I18n.t("skills.brand.privateTip")}">🔒 ${I18n.t("skills.brand.private")}</span>`;
173
+ // Free skills show a "Free" badge; paid (encrypted) brand skills show "Private".
174
+ const badge = skill.is_free
175
+ ? `<span class="brand-skill-badge-free" title="${I18n.t("skills.brand.freeTip")}">✨ ${I18n.t("skills.brand.free")}</span>`
176
+ : `<span class="brand-skill-badge-private" title="${I18n.t("skills.brand.privateTip")}">🔒 ${I18n.t("skills.brand.private")}</span>`;
169
177
 
170
178
  // Choose description based on current language
171
179
  const currentLang = I18n.lang();
@@ -180,7 +188,7 @@ const Skills = (() => {
180
188
  <div class="brand-skill-info">
181
189
  <div class="brand-skill-title">
182
190
  <span class="brand-skill-name">${escapeHtml((currentLang === "zh" && skill.name_zh) ? skill.name_zh : name)}</span>
183
- ${privateBadge}
191
+ ${badge}
184
192
  </div>
185
193
  <div class="brand-skill-desc">${escapeHtml(description)}</div>
186
194
  </div>
@@ -357,6 +365,20 @@ const Skills = (() => {
357
365
  });
358
366
  }
359
367
 
368
+ // Flip tooltip below when toggle is near top of scroll container
369
+ const toggleLabel = card.querySelector(".skill-toggle");
370
+ if (toggleLabel) {
371
+ toggleLabel.addEventListener("mouseenter", () => {
372
+ const scroller = toggleLabel.closest(".skills-tab-content");
373
+ if (!scroller) return;
374
+ const toggleTop = toggleLabel.getBoundingClientRect().top;
375
+ const scrollerTop = scroller.getBoundingClientRect().top;
376
+ if (toggleTop - scrollerTop < 80) {
377
+ toggleLabel.classList.add("skill-toggle-flip");
378
+ }
379
+ });
380
+ }
381
+
360
382
  // Bind "Use" button event
361
383
  const useBtn = card.querySelector(".btn-skill-use");
362
384
  if (useBtn) {