openclacky 1.3.1 → 1.3.3

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/lib/clacky/agent/session_serializer.rb +65 -11
  7. data/lib/clacky/agent/time_machine.rb +247 -26
  8. data/lib/clacky/agent.rb +12 -1
  9. data/lib/clacky/agent_config.rb +14 -2
  10. data/lib/clacky/brand_config.rb +1 -1
  11. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  12. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  13. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  14. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  15. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  16. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  17. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  18. data/lib/clacky/media/openai_compat.rb +64 -1
  19. data/lib/clacky/media/output_dir.rb +43 -0
  20. data/lib/clacky/message_history.rb +9 -0
  21. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  22. data/lib/clacky/server/git_panel.rb +115 -0
  23. data/lib/clacky/server/http_server.rb +521 -13
  24. data/lib/clacky/server/server_master.rb +6 -4
  25. data/lib/clacky/utils/environment_detector.rb +16 -0
  26. data/lib/clacky/version.rb +1 -1
  27. data/lib/clacky/web/app.css +512 -60
  28. data/lib/clacky/web/app.js +30 -7
  29. data/lib/clacky/web/components/code-editor.js +197 -0
  30. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  31. data/lib/clacky/web/core/aside.js +112 -0
  32. data/lib/clacky/web/core/ext.js +387 -0
  33. data/lib/clacky/web/features/backup/store.js +92 -0
  34. data/lib/clacky/web/features/backup/view.js +94 -0
  35. data/lib/clacky/web/features/billing/store.js +163 -0
  36. data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
  37. data/lib/clacky/web/features/brand/store.js +110 -0
  38. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  39. data/lib/clacky/web/features/channels/store.js +103 -0
  40. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  41. data/lib/clacky/web/features/creator/store.js +81 -0
  42. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  43. data/lib/clacky/web/features/mcp/store.js +158 -0
  44. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  45. data/lib/clacky/web/features/model-tester/store.js +77 -0
  46. data/lib/clacky/web/features/model-tester/view.js +7 -0
  47. data/lib/clacky/web/features/profile/store.js +170 -0
  48. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  49. data/lib/clacky/web/features/share/store.js +145 -0
  50. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  51. data/lib/clacky/web/features/skills/store.js +303 -0
  52. data/lib/clacky/web/features/skills/view.js +550 -0
  53. data/lib/clacky/web/features/tasks/store.js +135 -0
  54. data/lib/clacky/web/features/tasks/view.js +241 -0
  55. data/lib/clacky/web/features/trash/store.js +242 -0
  56. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  57. data/lib/clacky/web/features/version/store.js +165 -0
  58. data/lib/clacky/web/features/version/view.js +323 -0
  59. data/lib/clacky/web/features/workspace/store.js +99 -0
  60. data/lib/clacky/web/features/workspace/view.js +305 -0
  61. data/lib/clacky/web/i18n.js +60 -6
  62. data/lib/clacky/web/index.html +117 -57
  63. data/lib/clacky/web/sessions.js +221 -25
  64. data/lib/clacky/web/settings.js +121 -25
  65. data/lib/clacky/web/skills.js +3 -821
  66. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  67. data/lib/clacky.rb +1 -0
  68. metadata +45 -20
  69. data/lib/clacky/web/backup.js +0 -119
  70. data/lib/clacky/web/model-tester.js +0 -66
  71. data/lib/clacky/web/tasks.js +0 -365
  72. data/lib/clacky/web/version.js +0 -449
  73. data/lib/clacky/web/workspace.js +0 -212
  74. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  75. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  76. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  77. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  78. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -0,0 +1,303 @@
1
+ // ── Skills · store — data, state, network, business actions ────────────────
2
+ //
3
+ // The store is the single source of truth for skills data. It owns state,
4
+ // talks to the server, and runs business actions (toggle/delete/install/open
5
+ // a session). It NEVER touches rendering DOM directly — when data changes it
6
+ // emits an event and lets the view re-render.
7
+ //
8
+ // Two event channels, on purpose:
9
+ // 1. Internal bus (Store.on / _emit) — ALWAYS live. The core view layer
10
+ // subscribes here. This must keep working in pure mode, otherwise the
11
+ // official panel would stop rendering.
12
+ // 2. Clacky.ext.emit(...) — the extension bus. Fired alongside the internal
13
+ // bus so user/AI extensions can observe core data changes. It is a no-op
14
+ // under ?pure=true by design (extensions are silenced, core is not).
15
+ //
16
+ // `Skills` stays the single public facade so existing callers (app.js,
17
+ // settings.js, tasks.js, ws-dispatcher.js, brand.js, creator.js, onboard.js)
18
+ // keep working unchanged. View functions are reached through SkillsView, which
19
+ // the store calls only via events — never by importing view internals.
20
+ //
21
+ // Depends on: WS (ws.js), Sessions (sessions.js), Router (app.js),
22
+ // Modal/I18n, global $ / escapeHtml helpers, Clacky.ext (core/ext.js)
23
+ // ───────────────────────────────────────────────────────────────────────────
24
+
25
+ const SkillsStore = (() => {
26
+ // ── State (single source of truth) ─────────────────────────────────────
27
+ let _skills = []; // [{ name, description, source, enabled }]
28
+ let _brandSkills = []; // skills from cloud license API
29
+ let _activeTab = "my-skills"; // "my-skills" | "brand-skills"
30
+ let _brandActivated = false; // whether a license is currently active
31
+ let _freeMode = false; // brand-skills tab is showing free-mode skills
32
+ let _paidSkillsCount = 0; // premium (encrypted) skills locked behind activation
33
+ let _showSystemSkills = false; // whether system (source=default) skills are shown
34
+
35
+ // ── Internal event bus ──────────────────────────────────────────────────
36
+ // Always live (unlike Clacky.ext, which is silenced under ?pure=true). The
37
+ // core view layer subscribes here so the official panel keeps rendering even
38
+ // in pure mode.
39
+ const _listeners = {}; // event => [handler]
40
+
41
+ function _on(event, handler) {
42
+ (_listeners[event] ||= []).push(handler);
43
+ return () => {
44
+ const list = _listeners[event];
45
+ const i = list ? list.indexOf(handler) : -1;
46
+ if (i >= 0) list.splice(i, 1);
47
+ };
48
+ }
49
+
50
+ // Notify the core view (internal bus) and mirror to the extension bus so
51
+ // extensions can observe core data changes. Extension delivery is silenced
52
+ // in pure mode by Clacky.ext itself.
53
+ function _emit(event, payload) {
54
+ (_listeners[event] || []).forEach((h) => h(payload));
55
+ if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
56
+ }
57
+
58
+ // ── Read-only accessors used by the view ────────────────────────────────
59
+ const state = {
60
+ get skills() { return _skills; },
61
+ get brandSkills() { return _brandSkills; },
62
+ get activeTab() { return _activeTab; },
63
+ get brandActivated() { return _brandActivated; },
64
+ get freeMode() { return _freeMode; },
65
+ get paidSkillsCount() { return _paidSkillsCount; },
66
+ get showSystemSkills() { return _showSystemSkills; },
67
+ };
68
+
69
+ // ── Helpers shared by business actions ───────────────────────────────────
70
+
71
+ // Resolve the next "Session N" name and create a session, then hand off to
72
+ // Sessions. Used by every "open a session and run a command" action.
73
+ async function _openSessionWith(message) {
74
+ const maxN = Sessions.all.reduce((max, s) => {
75
+ const m = s.name.match(/^Session (\d+)$/);
76
+ return m ? Math.max(max, parseInt(m[1], 10)) : max;
77
+ }, 0);
78
+ const res = await fetch("/api/sessions", {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
82
+ });
83
+ const data = await res.json();
84
+ if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return null; }
85
+
86
+ const session = data.session;
87
+ if (!session) return null;
88
+
89
+ if (!WS.ready) { WS.connect(); Skills.load(); }
90
+
91
+ Sessions.add(session);
92
+ Sessions.renderList();
93
+ Sessions.setPendingMessage(session.id, message);
94
+ Sessions.select(session.id);
95
+ return session;
96
+ }
97
+
98
+ /** Return a user-friendly message for install/update errors. */
99
+ function _friendlyInstallError(rawError) {
100
+ if (!rawError) return I18n.t("skills.brand.unknownError");
101
+ const lower = rawError.toLowerCase();
102
+ if (lower.includes("timeout") || lower.includes("network error") ||
103
+ lower.includes("execution expired") || lower.includes("failed to open")) {
104
+ return I18n.t("skills.brand.networkRetry");
105
+ }
106
+ return I18n.t("skills.brand.installFailed") + rawError;
107
+ }
108
+
109
+ // ── Public facade (kept identical for existing callers) ──────────────────
110
+ const Skills = {
111
+
112
+ // ── Store wiring (used by the view layer only) ─────────────────────────
113
+ on: _on,
114
+ state,
115
+
116
+ // ── Data ───────────────────────────────────────────────────────────────
117
+
118
+ /** Return current skills list (read-only snapshot). */
119
+ get all() { return _skills.slice(); },
120
+
121
+ /** Fetch skills from server; emit so the view re-renders. */
122
+ async load() {
123
+ try {
124
+ const res = await fetch("/api/skills");
125
+ const data = await res.json();
126
+ _skills = data.skills || [];
127
+ _emit("skills:changed", { skills: _skills });
128
+ } catch (e) {
129
+ console.error("[Skills] load failed", e);
130
+ }
131
+ },
132
+
133
+ /** Fetch brand skills from server; emit so the view re-renders. */
134
+ async loadBrandSkills() {
135
+ _emit("brandSkills:loading");
136
+ try {
137
+ const res = await fetch("/api/brand/skills");
138
+ const data = await res.json();
139
+
140
+ if (!res.ok || !data.ok) {
141
+ _emit("brandSkills:error", { error: data.error || I18n.t("skills.brand.loadFailed") });
142
+ return;
143
+ }
144
+
145
+ _brandSkills = data.skills || [];
146
+ _freeMode = !!data.free_mode;
147
+ _paidSkillsCount = Number(data.paid_skills_count) || 0;
148
+
149
+ _emit("brandSkills:changed", {
150
+ brandSkills: _brandSkills,
151
+ freeMode: _freeMode,
152
+ paidSkillsCount: _paidSkillsCount,
153
+ warning: data.warning,
154
+ warningCode: data.warning_code,
155
+ });
156
+ } catch (e) {
157
+ console.error("[Skills] brand skills load failed", e);
158
+ _emit("brandSkills:error", { network: true });
159
+ }
160
+ },
161
+
162
+ /** Refresh brand license status; emit so the view can toggle the tab. */
163
+ async refreshBrandStatus() {
164
+ try {
165
+ const res = await fetch("/api/brand/status");
166
+ const data = await res.json();
167
+ const prevActivated = _brandActivated;
168
+ _brandActivated = data.branded && !data.needs_activation;
169
+ _emit("brandStatus:changed", {
170
+ branded: data.branded,
171
+ activated: _brandActivated,
172
+ activatedChanged: prevActivated !== _brandActivated,
173
+ });
174
+ } catch (_e) {
175
+ // On network error, keep whatever is currently shown.
176
+ }
177
+ },
178
+
179
+ // ── State setters driven by the view ─────────────────────────────────────
180
+
181
+ /** Switch the active tab; emit so the view updates tab UI. */
182
+ setActiveTab(tab) {
183
+ _activeTab = tab;
184
+ _emit("tab:changed", { tab });
185
+ if (tab === "brand-skills") Skills.loadBrandSkills();
186
+ },
187
+
188
+ /** Toggle visibility of system skills; emit so the view re-renders. */
189
+ setShowSystemSkills(show) {
190
+ _showSystemSkills = !!show;
191
+ _emit("skills:changed", { skills: _skills });
192
+ },
193
+
194
+ // ── Actions ──────────────────────────────────────────────────────────────
195
+
196
+ /** Toggle enable/disable for a skill, then reload. */
197
+ async toggle(name, enabled) {
198
+ try {
199
+ const res = await fetch(`/api/skills/${encodeURIComponent(name)}/toggle`, {
200
+ method: "PATCH",
201
+ headers: { "Content-Type": "application/json" },
202
+ body: JSON.stringify({ enabled })
203
+ });
204
+ const data = await res.json();
205
+ if (!res.ok) { alert(I18n.t("skills.toggleError") + (data.error || "unknown")); return; }
206
+ await Skills.load();
207
+ } catch (e) {
208
+ console.error("[Skills] toggle failed", e);
209
+ }
210
+ },
211
+
212
+ /** Delete a custom skill by name. Confirms, then reloads. */
213
+ async delete(name) {
214
+ if (!confirm(I18n.t("skills.deleteConfirm", { name }))) return;
215
+ try {
216
+ const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, { method: "DELETE" });
217
+ const data = await res.json();
218
+ if (!res.ok) { alert(data.error || I18n.t("skills.deleteError")); return; }
219
+ Modal.toast(I18n.t("skills.deleted", { name }), "success");
220
+ await Skills.load();
221
+ } catch (e) {
222
+ console.error("[Skills] delete failed", e);
223
+ }
224
+ },
225
+
226
+ /** Install or update a brand skill. Resolves to a result the view renders. */
227
+ async installBrandSkill(name) {
228
+ try {
229
+ const res = await fetch(`/api/brand/skills/${encodeURIComponent(name)}/install`, { method: "POST" });
230
+ const data = await res.json();
231
+ if (!res.ok || !data.ok) {
232
+ return { ok: false, message: _friendlyInstallError(data.error) };
233
+ }
234
+ const skill = _brandSkills.find(s => s.name === name);
235
+ if (skill) { skill.installed_version = data.version; skill.needs_update = false; }
236
+ _emit("brandSkills:changed", { brandSkills: _brandSkills, freeMode: _freeMode, paidSkillsCount: _paidSkillsCount });
237
+ await Skills.load();
238
+ return { ok: true };
239
+ } catch (e) {
240
+ return { ok: false, message: I18n.t("skills.brand.networkRetry") };
241
+ }
242
+ },
243
+
244
+ /** Delete an installed brand skill. */
245
+ async deleteBrandSkill(name) {
246
+ if (!confirm(I18n.t("skills.deleteConfirm", { name }))) return;
247
+ try {
248
+ const res = await fetch(`/api/brand/skills/${encodeURIComponent(name)}`, { method: "DELETE" });
249
+ const data = await res.json();
250
+ if (!res.ok || !data.ok) { alert(data.error || I18n.t("skills.deleteError")); return; }
251
+ const skill = _brandSkills.find(s => s.name === name);
252
+ if (skill) skill.installed_version = null;
253
+ _emit("brandSkills:changed", { brandSkills: _brandSkills, freeMode: _freeMode, paidSkillsCount: _paidSkillsCount });
254
+ await Skills.load();
255
+ Modal.toast(I18n.t("skills.deleted", { name }), "success");
256
+ } catch (e) {
257
+ console.error("[Skills] brand skill delete failed", e);
258
+ }
259
+ },
260
+
261
+ /** Open a session and run a brand skill by sending "/{name}". */
262
+ useInstalledSkill(name) {
263
+ return _openSessionWith("/" + name);
264
+ },
265
+
266
+ /** Create a new custom skill via a session running /skill-creator. */
267
+ createInSession(message) {
268
+ return _openSessionWith(message || "/skill-creator");
269
+ },
270
+
271
+ /** Import a skill: validate url/path, open a session, run /skill-add. */
272
+ async importSkill(url) {
273
+ const trimmed = (url || "").trim();
274
+ if (!trimmed) return { ok: false, reason: "empty" };
275
+ const isUrl = /^https?:\/\//i.test(trimmed);
276
+ const isLocalPath = trimmed.startsWith("/") || trimmed.startsWith("~");
277
+ if (!isUrl && !isLocalPath) return { ok: false, reason: "invalid" };
278
+ try {
279
+ await _openSessionWith(`/skill-add ${trimmed}`);
280
+ return { ok: true };
281
+ } catch (e) {
282
+ console.error("[Skills] import failed", e);
283
+ return { ok: false, reason: "network" };
284
+ }
285
+ },
286
+
287
+ // ── Cross-feature state resets (called by settings.js) ───────────────────
288
+
289
+ /** Reset to My Skills and clear brand state after license unbind. */
290
+ resetAfterUnbind() {
291
+ _brandSkills = [];
292
+ _brandActivated = false;
293
+ _activeTab = "my-skills";
294
+ _emit("brandStatus:changed", { branded: false, activated: false, activatedChanged: true });
295
+ _emit("tab:changed", { tab: "my-skills", reason: "unbind" });
296
+ },
297
+ };
298
+
299
+ return Skills;
300
+ })();
301
+
302
+ // Expose the facade under its historical global name.
303
+ const Skills = SkillsStore;