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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +49 -5
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +497 -12
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +473 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +56 -6
- data/lib/clacky/web/index.html +117 -58
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +118 -22
- data/lib/clacky/web/skills.js +3 -863
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -373
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -316
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /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;
|