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
@@ -1,365 +0,0 @@
1
- // ── Tasks — task/schedule state, rendering, CRUD ──────────────────────────
2
- //
3
- // Responsibilities:
4
- // - Single source of truth for tasks + schedules data
5
- // - Render the "Scheduled Tasks" entry in the sidebar
6
- // - Show/render the task list table in the main panel
7
- // - CRUD: load, run, editInSession (creates new session), delete
8
- //
9
- // Panel switching is delegated to Router — Tasks only manages data + rendering.
10
- //
11
- // Depends on: WS (ws.js), Sessions (sessions.js), Router (app.js),
12
- // global $ / escapeHtml helpers
13
- // ─────────────────────────────────────────────────────────────────────────
14
-
15
- const Tasks = (() => {
16
- // ── Private state ──────────────────────────────────────────────────────
17
- let _tasks = []; // [{ name, content, cron, enabled, scheduled }]
18
-
19
- // ── Private helpers ────────────────────────────────────────────────────
20
-
21
- function _humanCron(cron) {
22
- if (!cron) return cron;
23
- const parts = cron.trim().split(/\s+/);
24
- if (parts.length !== 5) return cron;
25
- const [min, hour, dom, month, dow] = parts;
26
-
27
- const isAny = v => v === "*";
28
- const isNum = v => /^\d+$/.test(v);
29
- const pad = n => String(n).padStart(2, "0");
30
-
31
- const lang = (typeof I18n !== "undefined" && I18n.lang()) || "zh";
32
- const isZh = lang === "zh";
33
-
34
- const dowNames = isZh
35
- ? ["周日","周一","周二","周三","周四","周五","周六"]
36
- : ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
37
-
38
- const monthNames = isZh
39
- ? ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]
40
- : ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
41
-
42
- const timeStr = (isNum(hour) && isNum(min))
43
- ? `${pad(hour)}:${pad(min)}`
44
- : null;
45
-
46
- // Every N minutes
47
- if (min.startsWith("*/") && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
48
- const n = min.slice(2);
49
- return isZh ? `每 ${n} 分钟` : `Every ${n} min`;
50
- }
51
- // Every N hours
52
- if (isAny(min) && hour.startsWith("*/") && isAny(dom) && isAny(month) && isAny(dow)) {
53
- const n = hour.slice(2);
54
- return isZh ? `每 ${n} 小时` : `Every ${n} hr`;
55
- }
56
- // Every minute
57
- if (isAny(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
58
- return isZh ? "每分钟" : "Every minute";
59
- }
60
- // Every hour at :MM
61
- if (isNum(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
62
- return isZh ? `每小时 :${pad(min)}` : `Hourly at :${pad(min)}`;
63
- }
64
- // Specific day-of-week
65
- if (timeStr && isAny(dom) && isAny(month) && isNum(dow)) {
66
- const d = dowNames[parseInt(dow, 10)] || dow;
67
- return isZh ? `每${d} ${timeStr}` : `${d} ${timeStr}`;
68
- }
69
- // Weekdays (1-5)
70
- if (timeStr && isAny(dom) && isAny(month) && dow === "1-5") {
71
- return isZh ? `工作日 ${timeStr}` : `Weekdays ${timeStr}`;
72
- }
73
- // Weekends (0,6 or 6,0)
74
- if (timeStr && isAny(dom) && isAny(month) && (dow === "0,6" || dow === "6,0")) {
75
- return isZh ? `周末 ${timeStr}` : `Weekends ${timeStr}`;
76
- }
77
- // Every day at HH:MM
78
- if (timeStr && isAny(dom) && isAny(month) && isAny(dow)) {
79
- return isZh ? `每天 ${timeStr}` : `Daily ${timeStr}`;
80
- }
81
- // Specific day of month
82
- if (timeStr && isNum(dom) && isAny(month) && isAny(dow)) {
83
- return isZh ? `每月 ${dom} 日 ${timeStr}` : `Monthly day ${dom} ${timeStr}`;
84
- }
85
- // Specific month + day
86
- if (timeStr && isNum(dom) && isNum(month) && isAny(dow)) {
87
- const m = monthNames[parseInt(month, 10) - 1] || month;
88
- return isZh ? `${m}${dom}日 ${timeStr}` : `${m} ${dom} ${timeStr}`;
89
- }
90
-
91
- return cron;
92
- }
93
-
94
- /** Render a single task row in the main panel table. */
95
- function _renderTaskRow(t) {
96
- const row = document.createElement("div");
97
- row.className = "task-card";
98
- row.dataset.name = t.name;
99
-
100
- const isPaused = t.scheduled && t.enabled === false;
101
- row.classList.toggle("task-card-paused", isPaused);
102
-
103
- const schedLabel = t.scheduled
104
- ? `<span class="task-card-cron" title="${escapeHtml(t.cron)}">${escapeHtml(_humanCron(t.cron))}</span>`
105
- : `<span class="task-card-cron task-card-cron-manual">${I18n.t("tasks.manual")}</span>`;
106
-
107
- const pausedBadge = isPaused
108
- ? `<span class="task-card-badge task-card-badge-paused">${I18n.t("tasks.paused")}</span>`
109
- : "";
110
-
111
- const preview = (t.content || "")
112
- .split("\n")
113
- .map(l => l.trim())
114
- .find(l => l.length > 0) || I18n.t("tasks.empty");
115
- const previewText = preview.length > 120
116
- ? escapeHtml(preview.slice(0, 120)) + "…"
117
- : escapeHtml(preview);
118
-
119
- const toggleBtnHtml = t.scheduled ? (isPaused
120
- ? `<button class="task-action-btn task-btn-toggle task-btn-resume">
121
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
122
- <polygon points="6 3 20 12 6 21 6 3"/>
123
- </svg>
124
- <span>${I18n.t("tasks.btn.resume")}</span>
125
- </button>`
126
- : `<button class="task-action-btn task-btn-toggle task-btn-pause">
127
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
128
- <rect x="6" y="4" width="4" height="16" rx="1"/>
129
- <rect x="14" y="4" width="4" height="16" rx="1"/>
130
- </svg>
131
- <span>${I18n.t("tasks.btn.pause")}</span>
132
- </button>`
133
- ) : "";
134
-
135
- row.innerHTML = `
136
- <div class="task-card-main">
137
- <div class="task-card-icon">
138
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
139
- <circle cx="12" cy="12" r="10"/>
140
- <polyline points="12 6 12 12 16 14"/>
141
- </svg>
142
- </div>
143
- <div class="task-card-info">
144
- <div class="task-card-title-row">
145
- <span class="task-card-name">${escapeHtml(t.name)}</span>
146
- ${pausedBadge}
147
- ${schedLabel}
148
- </div>
149
- <div class="task-card-preview">${previewText}</div>
150
- </div>
151
- <div class="task-card-actions">
152
- <button class="task-run-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
153
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
154
- <polygon points="6 3 20 12 6 21 6 3"/>
155
- </svg>
156
- <span>${I18n.t("tasks.btn.run")}</span>
157
- </button>
158
- ${toggleBtnHtml}
159
- <button class="task-action-btn task-btn-edit">
160
- <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
161
- <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
162
- <path d="m15 5 4 4"/>
163
- </svg>
164
- <span>${I18n.t("tasks.btn.edit")}</span>
165
- </button>
166
- <button class="task-action-btn task-btn-del">
167
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
168
- <path d="M3 6h18"/><path d="M19 6l-1 14H6L5 6"/><path d="M8 6V4h8v2"/>
169
- </svg>
170
- <span>${I18n.t("tasks.btn.delete")}</span>
171
- </button>
172
- </div>
173
- </div>`;
174
-
175
- row.querySelector(".task-btn-run").addEventListener("click", e => {
176
- e.stopPropagation();
177
- Tasks.run(t.name);
178
- });
179
- const toggleBtn = row.querySelector(".task-btn-toggle");
180
- if (toggleBtn) {
181
- toggleBtn.addEventListener("click", e => {
182
- e.stopPropagation();
183
- Tasks.toggleEnabled(t.name, isPaused); // isPaused=true means we want to enable
184
- });
185
- }
186
- row.querySelector(".task-btn-edit").addEventListener("click", e => {
187
- e.stopPropagation();
188
- Tasks.editInSession(t.name);
189
- });
190
- row.querySelector(".task-btn-del").addEventListener("click", e => {
191
- e.stopPropagation();
192
- Tasks.delete(t.name);
193
- });
194
-
195
- return row;
196
- }
197
-
198
- // ── Public API ─────────────────────────────────────────────────────────
199
- return {
200
-
201
- // ── Data ─────────────────────────────────────────────────────────────
202
-
203
- /** Fetch cron tasks from server; re-render sidebar + panel if open. */
204
- async load() {
205
- try {
206
- const res = await fetch("/api/cron-tasks");
207
- const data = await res.json();
208
- _tasks = data.cron_tasks || [];
209
- Tasks.renderSection();
210
- if (Router.current === "tasks") Tasks.renderTable();
211
- } catch (e) {
212
- console.error("[Tasks] load failed", e);
213
- }
214
- },
215
-
216
- // ── Router interface ──────────────────────────────────────────────────
217
-
218
- /** Called by Router when the tasks panel becomes active. */
219
- onPanelShow() {
220
- Tasks.load();
221
- const btn = $("btn-create-task");
222
- if (btn) btn.onclick = () => Tasks.createInSession();
223
- },
224
-
225
- // ── Sidebar rendering ─────────────────────────────────────────────────
226
-
227
- renderSection() {
228
- // Sidebar item is static in HTML — just update the label text.
229
- const labelEl = $("tasks-sidebar-label");
230
- if (!labelEl) return;
231
- labelEl.textContent = I18n.t("sidebar.tasks");
232
- },
233
-
234
- // ── Main panel table ──────────────────────────────────────────────────
235
-
236
- /** Render all tasks as rows in the main panel table. */
237
- renderTable() {
238
- const table = $("task-list-table");
239
- table.innerHTML = "";
240
-
241
- if (_tasks.length === 0) {
242
- const empty = document.createElement("div");
243
- empty.className = "task-table-empty";
244
- empty.innerHTML = `
245
- <p>${I18n.t("tasks.noScheduled")}</p>
246
- <button class="task-create-btn" id="btn-create-task-empty">
247
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
248
- <path d="M5 12h14"/>
249
- <path d="M12 5v14"/>
250
- </svg> ${I18n.t("tasks.btn.createTask")}
251
- </button>`;
252
- table.appendChild(empty);
253
- const btn = table.querySelector("#btn-create-task-empty");
254
- if (btn) btn.addEventListener("click", () => Tasks.createInSession());
255
- return;
256
- }
257
-
258
- _tasks.forEach(t => table.appendChild(_renderTaskRow(t)));
259
- },
260
-
261
- // ── CRUD ─────────────────────────────────────────────────────────────
262
-
263
- async run(name) {
264
- const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}/run`, {
265
- method: "POST"
266
- });
267
- const data = await res.json();
268
- if (!res.ok) { alert(I18n.t("tasks.runError") + (data.error || "unknown")); return; }
269
-
270
- if (data.session) {
271
- await Tasks.load();
272
- Sessions.add(data.session);
273
- Sessions.renderList();
274
- Sessions.setPendingRunTask(data.session.id);
275
- Sessions.select(data.session.id);
276
- }
277
- },
278
-
279
- /** Toggle a scheduled task's enabled flag. `wasPaused` is the current
280
- * paused-state before the click; if true, we resume (enabled: true).
281
- * Optimistic: we update local state first, then reload on success. */
282
- async toggleEnabled(name, wasPaused) {
283
- const nextEnabled = wasPaused; // paused → resume(true); running → pause(false)
284
- const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}`, {
285
- method: "PATCH",
286
- headers: { "Content-Type": "application/json" },
287
- body: JSON.stringify({ enabled: nextEnabled })
288
- });
289
- if (!res.ok) {
290
- let msg = "";
291
- try { msg = (await res.json()).error || ""; } catch (_) {}
292
- alert(I18n.t("tasks.toggleError") + (msg ? " " + msg : ""));
293
- return;
294
- }
295
- await Tasks.load();
296
- },
297
-
298
- /** Create a new task by opening a new session and sending /create-task. */
299
- async createInSession() {
300
- const maxN = Sessions.all.reduce((max, s) => {
301
- const m = s.name.match(/^Session (\d+)$/);
302
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
303
- }, 0);
304
- const res = await fetch("/api/sessions", {
305
- method: "POST",
306
- headers: { "Content-Type": "application/json" },
307
- body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
308
- });
309
- const data = await res.json();
310
- if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
311
-
312
- const session = data.session;
313
- if (!session) return;
314
-
315
- // If WS is not yet connected (e.g. called during onboarding), boot the UI
316
- // first so WS connects, then use setPendingMessage so the command is sent
317
- // once the socket is ready. This mirrors Onboard._startSoulSession().
318
- if (!WS.ready) {
319
- WS.connect();
320
- Skills.load();
321
- }
322
-
323
- Sessions.add(session);
324
- Sessions.renderList();
325
- Sessions.setPendingMessage(session.id, "/cron-task-creator");
326
- Sessions.select(session.id);
327
- },
328
-
329
- /** Edit a task by creating a new session and auto-sending the edit command. */
330
- async editInSession(name) {
331
- const maxN = Sessions.all.reduce((max, s) => {
332
- const m = s.name.match(/^Session (\d+)$/);
333
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
334
- }, 0);
335
- const res = await fetch("/api/sessions", {
336
- method: "POST",
337
- headers: { "Content-Type": "application/json" },
338
- body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
339
- });
340
- const data = await res.json();
341
- if (!res.ok) { alert("Error creating session: " + (data.error || "unknown")); return; }
342
-
343
- const session = data.session;
344
- if (!session) return;
345
-
346
- if (!WS.ready) {
347
- WS.connect();
348
- Skills.load();
349
- }
350
-
351
- Sessions.add(session);
352
- Sessions.renderList();
353
- Sessions.setPendingMessage(session.id, `/cron-task-creator I'm editing ${name} task`);
354
- Sessions.select(session.id);
355
- },
356
-
357
- async delete(name) {
358
- if (!confirm(I18n.t("tasks.confirmDelete", { name }))) return;
359
- const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}`, { method: "DELETE" });
360
- if (!res.ok) { alert(I18n.t("tasks.deleteError")); return; }
361
-
362
- await Tasks.load();
363
- },
364
- };
365
- })();