openclacky 1.3.2 → 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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -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 +49 -5
  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/default_agents/_panels/git/panel.js +201 -0
  11. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  12. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  13. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  14. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  15. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  16. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  17. data/lib/clacky/media/openai_compat.rb +64 -1
  18. data/lib/clacky/media/output_dir.rb +43 -0
  19. data/lib/clacky/message_history.rb +9 -0
  20. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  21. data/lib/clacky/server/git_panel.rb +115 -0
  22. data/lib/clacky/server/http_server.rb +497 -12
  23. data/lib/clacky/server/server_master.rb +6 -4
  24. data/lib/clacky/version.rb +1 -1
  25. data/lib/clacky/web/app.css +473 -60
  26. data/lib/clacky/web/app.js +30 -7
  27. data/lib/clacky/web/components/code-editor.js +197 -0
  28. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  29. data/lib/clacky/web/core/aside.js +112 -0
  30. data/lib/clacky/web/core/ext.js +387 -0
  31. data/lib/clacky/web/features/backup/store.js +92 -0
  32. data/lib/clacky/web/features/backup/view.js +94 -0
  33. data/lib/clacky/web/features/billing/store.js +163 -0
  34. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  35. data/lib/clacky/web/features/brand/store.js +110 -0
  36. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  37. data/lib/clacky/web/features/channels/store.js +103 -0
  38. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  39. data/lib/clacky/web/features/creator/store.js +81 -0
  40. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  41. data/lib/clacky/web/features/mcp/store.js +158 -0
  42. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  43. data/lib/clacky/web/features/model-tester/store.js +77 -0
  44. data/lib/clacky/web/features/model-tester/view.js +7 -0
  45. data/lib/clacky/web/features/profile/store.js +170 -0
  46. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  47. data/lib/clacky/web/features/share/store.js +145 -0
  48. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  49. data/lib/clacky/web/features/skills/store.js +303 -0
  50. data/lib/clacky/web/features/skills/view.js +550 -0
  51. data/lib/clacky/web/features/tasks/store.js +135 -0
  52. data/lib/clacky/web/features/tasks/view.js +241 -0
  53. data/lib/clacky/web/features/trash/store.js +242 -0
  54. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  55. data/lib/clacky/web/features/version/store.js +165 -0
  56. data/lib/clacky/web/features/version/view.js +323 -0
  57. data/lib/clacky/web/features/workspace/store.js +99 -0
  58. data/lib/clacky/web/features/workspace/view.js +305 -0
  59. data/lib/clacky/web/i18n.js +56 -6
  60. data/lib/clacky/web/index.html +117 -58
  61. data/lib/clacky/web/sessions.js +221 -25
  62. data/lib/clacky/web/settings.js +118 -22
  63. data/lib/clacky/web/skills.js +3 -863
  64. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  65. data/lib/clacky.rb +1 -0
  66. metadata +45 -20
  67. data/lib/clacky/web/backup.js +0 -119
  68. data/lib/clacky/web/model-tester.js +0 -66
  69. data/lib/clacky/web/tasks.js +0 -373
  70. data/lib/clacky/web/version.js +0 -449
  71. data/lib/clacky/web/workspace.js +0 -316
  72. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  73. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  74. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  75. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  76. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -1,373 +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) || isNum(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 content = t.content || "";
112
- const isTruncated = content.trim().length > 0;
113
- const previewText = escapeHtml(content.replace(/\s+/g, " ").trim()) || escapeHtml(I18n.t("tasks.empty"));
114
-
115
- const toggleBtnHtml = t.scheduled ? (isPaused
116
- ? `<button class="task-action-btn task-btn-toggle task-btn-resume">
117
- <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">
118
- <polygon points="6 3 20 12 6 21 6 3"/>
119
- </svg>
120
- <span>${I18n.t("tasks.btn.resume")}</span>
121
- </button>`
122
- : `<button class="task-action-btn task-btn-toggle task-btn-pause">
123
- <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">
124
- <rect x="6" y="4" width="4" height="16" rx="1"/>
125
- <rect x="14" y="4" width="4" height="16" rx="1"/>
126
- </svg>
127
- <span>${I18n.t("tasks.btn.pause")}</span>
128
- </button>`
129
- ) : "";
130
-
131
- row.innerHTML = `
132
- <div class="task-card-main">
133
- <div class="task-card-icon">
134
- <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">
135
- <circle cx="12" cy="12" r="10"/>
136
- <polyline points="12 6 12 12 16 14"/>
137
- </svg>
138
- </div>
139
- <div class="task-card-info">
140
- <div class="task-card-title-row">
141
- <span class="task-card-name">${escapeHtml(t.name)}</span>
142
- ${pausedBadge}
143
- ${schedLabel}
144
- </div>
145
- <div class="task-card-preview${isTruncated ? " task-card-preview-expandable" : ""}">${previewText}</div>
146
- </div>
147
- <div class="task-card-actions">
148
- <button class="task-run-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
149
- <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">
150
- <polygon points="6 3 20 12 6 21 6 3"/>
151
- </svg>
152
- <span>${I18n.t("tasks.btn.run")}</span>
153
- </button>
154
- ${toggleBtnHtml}
155
- <button class="task-action-btn task-btn-edit">
156
- <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">
157
- <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
158
- <path d="m15 5 4 4"/>
159
- </svg>
160
- <span>${I18n.t("tasks.btn.edit")}</span>
161
- </button>
162
- <button class="task-action-btn task-btn-del">
163
- <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">
164
- <path d="M3 6h18"/><path d="M19 6l-1 14H6L5 6"/><path d="M8 6V4h8v2"/>
165
- </svg>
166
- <span>${I18n.t("tasks.btn.delete")}</span>
167
- </button>
168
- </div>
169
- </div>
170
- ${isTruncated ? `<div class="task-card-detail" hidden><pre class="task-card-detail-content">${escapeHtml(content)}</pre></div>` : ""}`;
171
-
172
- row.querySelector(".task-btn-run").addEventListener("click", e => {
173
- e.stopPropagation();
174
- Tasks.run(t.name);
175
- });
176
-
177
- if (isTruncated) {
178
- const previewEl = row.querySelector(".task-card-preview");
179
- const detailEl = row.querySelector(".task-card-detail");
180
- previewEl.addEventListener("click", e => {
181
- e.stopPropagation();
182
- const expanded = !detailEl.hidden;
183
- detailEl.hidden = expanded;
184
- row.classList.toggle("task-card-expanded", !expanded);
185
- });
186
- }
187
- const toggleBtn = row.querySelector(".task-btn-toggle");
188
- if (toggleBtn) {
189
- toggleBtn.addEventListener("click", e => {
190
- e.stopPropagation();
191
- Tasks.toggleEnabled(t.name, isPaused); // isPaused=true means we want to enable
192
- });
193
- }
194
- row.querySelector(".task-btn-edit").addEventListener("click", e => {
195
- e.stopPropagation();
196
- Tasks.editInSession(t.name);
197
- });
198
- row.querySelector(".task-btn-del").addEventListener("click", e => {
199
- e.stopPropagation();
200
- Tasks.delete(t.name);
201
- });
202
-
203
- return row;
204
- }
205
-
206
- // ── Public API ─────────────────────────────────────────────────────────
207
- return {
208
-
209
- // ── Data ─────────────────────────────────────────────────────────────
210
-
211
- /** Fetch cron tasks from server; re-render sidebar + panel if open. */
212
- async load() {
213
- try {
214
- const res = await fetch("/api/cron-tasks");
215
- const data = await res.json();
216
- _tasks = data.cron_tasks || [];
217
- Tasks.renderSection();
218
- if (Router.current === "tasks") Tasks.renderTable();
219
- } catch (e) {
220
- console.error("[Tasks] load failed", e);
221
- }
222
- },
223
-
224
- // ── Router interface ──────────────────────────────────────────────────
225
-
226
- /** Called by Router when the tasks panel becomes active. */
227
- onPanelShow() {
228
- Tasks.load();
229
- const btn = $("btn-create-task");
230
- if (btn) btn.onclick = () => Tasks.createInSession();
231
- },
232
-
233
- // ── Sidebar rendering ─────────────────────────────────────────────────
234
-
235
- renderSection() {
236
- // Sidebar item is static in HTML — just update the label text.
237
- const labelEl = $("tasks-sidebar-label");
238
- if (!labelEl) return;
239
- labelEl.textContent = I18n.t("sidebar.tasks");
240
- },
241
-
242
- // ── Main panel table ──────────────────────────────────────────────────
243
-
244
- /** Render all tasks as rows in the main panel table. */
245
- renderTable() {
246
- const table = $("task-list-table");
247
- table.innerHTML = "";
248
-
249
- if (_tasks.length === 0) {
250
- const empty = document.createElement("div");
251
- empty.className = "task-table-empty";
252
- empty.innerHTML = `
253
- <p>${I18n.t("tasks.noScheduled")}</p>
254
- <button class="task-create-btn" id="btn-create-task-empty">
255
- <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">
256
- <path d="M5 12h14"/>
257
- <path d="M12 5v14"/>
258
- </svg> ${I18n.t("tasks.btn.createTask")}
259
- </button>`;
260
- table.appendChild(empty);
261
- const btn = table.querySelector("#btn-create-task-empty");
262
- if (btn) btn.addEventListener("click", () => Tasks.createInSession());
263
- return;
264
- }
265
-
266
- _tasks.forEach(t => table.appendChild(_renderTaskRow(t)));
267
- },
268
-
269
- // ── CRUD ─────────────────────────────────────────────────────────────
270
-
271
- async run(name) {
272
- const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}/run`, {
273
- method: "POST"
274
- });
275
- const data = await res.json();
276
- if (!res.ok) { alert(I18n.t("tasks.runError") + (data.error || "unknown")); return; }
277
-
278
- if (data.session) {
279
- await Tasks.load();
280
- Sessions.add(data.session);
281
- Sessions.renderList();
282
- Sessions.setPendingRunTask(data.session.id);
283
- Sessions.select(data.session.id);
284
- }
285
- },
286
-
287
- /** Toggle a scheduled task's enabled flag. `wasPaused` is the current
288
- * paused-state before the click; if true, we resume (enabled: true).
289
- * Optimistic: we update local state first, then reload on success. */
290
- async toggleEnabled(name, wasPaused) {
291
- const nextEnabled = wasPaused; // paused → resume(true); running → pause(false)
292
- const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}`, {
293
- method: "PATCH",
294
- headers: { "Content-Type": "application/json" },
295
- body: JSON.stringify({ enabled: nextEnabled })
296
- });
297
- if (!res.ok) {
298
- let msg = "";
299
- try { msg = (await res.json()).error || ""; } catch (_) {}
300
- alert(I18n.t("tasks.toggleError") + (msg ? " " + msg : ""));
301
- return;
302
- }
303
- await Tasks.load();
304
- },
305
-
306
- /** Create a new task by opening a new session and sending /create-task. */
307
- async createInSession() {
308
- const maxN = Sessions.all.reduce((max, s) => {
309
- const m = s.name.match(/^Session (\d+)$/);
310
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
311
- }, 0);
312
- const res = await fetch("/api/sessions", {
313
- method: "POST",
314
- headers: { "Content-Type": "application/json" },
315
- body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
316
- });
317
- const data = await res.json();
318
- if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
319
-
320
- const session = data.session;
321
- if (!session) return;
322
-
323
- // If WS is not yet connected (e.g. called during onboarding), boot the UI
324
- // first so WS connects, then use setPendingMessage so the command is sent
325
- // once the socket is ready. This mirrors Onboard._startSoulSession().
326
- if (!WS.ready) {
327
- WS.connect();
328
- Skills.load();
329
- }
330
-
331
- Sessions.add(session);
332
- Sessions.renderList();
333
- Sessions.setPendingMessage(session.id, "/cron-task-creator");
334
- Sessions.select(session.id);
335
- },
336
-
337
- /** Edit a task by creating a new session and auto-sending the edit command. */
338
- async editInSession(name) {
339
- const maxN = Sessions.all.reduce((max, s) => {
340
- const m = s.name.match(/^Session (\d+)$/);
341
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
342
- }, 0);
343
- const res = await fetch("/api/sessions", {
344
- method: "POST",
345
- headers: { "Content-Type": "application/json" },
346
- body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
347
- });
348
- const data = await res.json();
349
- if (!res.ok) { alert("Error creating session: " + (data.error || "unknown")); return; }
350
-
351
- const session = data.session;
352
- if (!session) return;
353
-
354
- if (!WS.ready) {
355
- WS.connect();
356
- Skills.load();
357
- }
358
-
359
- Sessions.add(session);
360
- Sessions.renderList();
361
- Sessions.setPendingMessage(session.id, `/cron-task-creator I'm editing ${name} task`);
362
- Sessions.select(session.id);
363
- },
364
-
365
- async delete(name) {
366
- if (!confirm(I18n.t("tasks.confirmDelete", { name }))) return;
367
- const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}`, { method: "DELETE" });
368
- if (!res.ok) { alert(I18n.t("tasks.deleteError")); return; }
369
-
370
- await Tasks.load();
371
- },
372
- };
373
- })();