openclacky 1.2.5 → 1.2.7

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +34 -0
  4. data/README_CN.md +34 -0
  5. data/lib/clacky/agent/cost_tracker.rb +24 -10
  6. data/lib/clacky/agent/llm_caller.rb +25 -3
  7. data/lib/clacky/agent/message_compressor.rb +2 -1
  8. data/lib/clacky/agent/message_compressor_helper.rb +6 -2
  9. data/lib/clacky/agent/session_serializer.rb +23 -4
  10. data/lib/clacky/agent/tool_executor.rb +14 -0
  11. data/lib/clacky/agent/tool_registry.rb +0 -7
  12. data/lib/clacky/agent.rb +43 -10
  13. data/lib/clacky/agent_config.rb +54 -6
  14. data/lib/clacky/billing/billing_store.rb +62 -4
  15. data/lib/clacky/brand_config.rb +5 -0
  16. data/lib/clacky/cli.rb +76 -24
  17. data/lib/clacky/client.rb +59 -4
  18. data/lib/clacky/default_parsers/wps_parser.rb +82 -0
  19. data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
  20. data/lib/clacky/json_ui_controller.rb +5 -2
  21. data/lib/clacky/message_format/anthropic.rb +13 -3
  22. data/lib/clacky/message_format/bedrock.rb +2 -2
  23. data/lib/clacky/plain_ui_controller.rb +1 -1
  24. data/lib/clacky/platform_http_client.rb +28 -1
  25. data/lib/clacky/providers.rb +11 -29
  26. data/lib/clacky/server/channel/channel_manager.rb +148 -12
  27. data/lib/clacky/server/channel/channel_ui_controller.rb +4 -2
  28. data/lib/clacky/server/http_server.rb +133 -13
  29. data/lib/clacky/server/session_registry.rb +30 -4
  30. data/lib/clacky/server/web_ui_controller.rb +6 -3
  31. data/lib/clacky/tools/browser.rb +4 -13
  32. data/lib/clacky/tools/terminal.rb +23 -27
  33. data/lib/clacky/ui2/ui_controller.rb +1 -1
  34. data/lib/clacky/ui_interface.rb +1 -1
  35. data/lib/clacky/utils/file_processor.rb +3 -0
  36. data/lib/clacky/utils/parser_manager.rb +3 -0
  37. data/lib/clacky/version.rb +1 -1
  38. data/lib/clacky/web/app.css +659 -75
  39. data/lib/clacky/web/app.js +0 -1
  40. data/lib/clacky/web/billing.js +371 -99
  41. data/lib/clacky/web/i18n.js +48 -2
  42. data/lib/clacky/web/index.html +34 -1
  43. data/lib/clacky/web/sessions.js +213 -82
  44. data/lib/clacky/web/settings.js +59 -17
  45. data/lib/clacky/web/workspace.js +204 -0
  46. data/lib/clacky/web/ws-dispatcher.js +19 -3
  47. data/lib/clacky.rb +9 -3
  48. metadata +4 -5
  49. data/lib/clacky/tools/list_tasks.rb +0 -54
  50. data/lib/clacky/tools/redo_task.rb +0 -41
  51. data/lib/clacky/tools/undo_task.rb +0 -35
@@ -6,6 +6,11 @@ const Settings = (() => {
6
6
  let _models = [];
7
7
  // Provider presets loaded from server
8
8
  let _providers = [];
9
+ // Provider id selected in the model edit modal (null when "Custom" or unset).
10
+ // Used to opt the request into anthropic_format=true when the user picks
11
+ // the Anthropic provider; other providers leave the flag unset and let the
12
+ // backend's runtime inference decide.
13
+ let _modalSelectedProviderId = null;
9
14
 
10
15
  // ── Public API ──────────────────────────────────────────────────────────────
11
16
 
@@ -52,9 +57,14 @@ const Settings = (() => {
52
57
  }
53
58
 
54
59
  function _getProviderName(model) {
55
- if (!model.base_url) return I18n.t("settings.models.provider.custom");
56
- const url = model.base_url.trim().replace(/\/+$/, "");
57
- const provider = _providers.find(p => {
60
+ const p = _findProviderByBaseUrl(model.base_url);
61
+ return p ? p.name : I18n.t("settings.models.provider.custom");
62
+ }
63
+
64
+ function _findProviderByBaseUrl(baseUrl) {
65
+ if (!baseUrl) return null;
66
+ const url = String(baseUrl).trim().replace(/\/+$/, "");
67
+ return _providers.find(p => {
58
68
  const candidates = [p.base_url].concat(
59
69
  Array.isArray(p.endpoint_variants) ? p.endpoint_variants.map(v => v.base_url) : []
60
70
  ).filter(Boolean);
@@ -62,14 +72,15 @@ const Settings = (() => {
62
72
  const norm = String(c).replace(/\/+$/, "");
63
73
  return url === norm || url.startsWith(norm + "/");
64
74
  });
65
- });
66
- return provider ? provider.name : I18n.t("settings.models.provider.custom");
75
+ }) || null;
67
76
  }
68
77
 
69
78
  function _renderCard(container, model, index) {
70
79
  const isDefault = model.type === "default";
71
80
  const isLite = model.type === "lite";
72
- const providerName = _getProviderName(model);
81
+ const provider = _findProviderByBaseUrl(model.base_url);
82
+ const providerName = provider ? provider.name : I18n.t("settings.models.provider.custom");
83
+ const websiteUrl = provider && provider.website_url;
73
84
  const displayName = model.model || I18n.t("settings.models.unnamed");
74
85
 
75
86
  const card = document.createElement("div");
@@ -84,7 +95,7 @@ const Settings = (() => {
84
95
  ${isLite ? `<span class="badge badge-lite">${I18n.t("settings.models.badge.lite")}</span>` : ""}
85
96
  </div>
86
97
  <div class="model-card-grid-provider">${_esc(providerName)}</div>
87
- ${model.model ? `<div class="model-card-grid-model">${_esc(model.model)}</div>` : ""}
98
+ ${model.api_key_masked ? `<div class="model-card-grid-model">${_esc(model.api_key_masked)}</div>` : ""}
88
99
  <div class="model-card-grid-status">
89
100
  <span class="model-test-result" data-index="${index}"></span>
90
101
  </div>
@@ -107,6 +118,12 @@ const Settings = (() => {
107
118
  <span>${I18n.t("settings.models.btn.delete")}</span>
108
119
  </button>` : ""}
109
120
  </div>
121
+ ${websiteUrl ? `<div class="model-card-grid-footer">
122
+ <a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer">
123
+ ${I18n.t("settings.models.link.topUp")}
124
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M8 7h9v9"/></svg>
125
+ </a>
126
+ </div>` : ""}
110
127
  `;
111
128
 
112
129
  container.appendChild(card);
@@ -150,7 +167,11 @@ const Settings = (() => {
150
167
  document.getElementById("model-modal-default-field").style.display = "none";
151
168
 
152
169
  // Set provider dropdown value
153
- const providerName = _getProviderName(model);
170
+ const matched = _findProviderByBaseUrl(model.base_url);
171
+ // Preserve an explicit anthropic_format=true even if base_url is custom:
172
+ // the user may have configured a self-hosted Anthropic-compatible proxy.
173
+ _modalSelectedProviderId = matched ? matched.id : (model.anthropic_format ? "anthropic" : null);
174
+ const providerName = matched ? matched.name : I18n.t("settings.models.provider.custom");
154
175
  const providerValue = document.getElementById("model-modal-provider-value");
155
176
  providerValue.textContent = providerName;
156
177
  providerValue.classList.remove("placeholder");
@@ -164,6 +185,7 @@ const Settings = (() => {
164
185
  document.getElementById("model-modal-set-default").checked = true;
165
186
 
166
187
  // Reset provider dropdown
188
+ _modalSelectedProviderId = null;
167
189
  const providerValue = document.getElementById("model-modal-provider-value");
168
190
  providerValue.textContent = I18n.t("settings.models.placeholder.provider");
169
191
  providerValue.classList.add("placeholder");
@@ -208,6 +230,10 @@ const Settings = (() => {
208
230
  const value = option.dataset.value;
209
231
  const text = option.dataset.label || option.textContent.trim();
210
232
 
233
+ // Track the picked provider so test/save can flag anthropic_format=true
234
+ // when the user explicitly picks Anthropic. Empty / "custom" → null.
235
+ _modalSelectedProviderId = (value && value !== "custom") ? value : null;
236
+
211
237
  const providerValue = document.getElementById("model-modal-provider-value");
212
238
  providerValue.textContent = text;
213
239
  providerValue.classList.toggle("placeholder", !value);
@@ -256,15 +282,18 @@ const Settings = (() => {
256
282
  _showTestResult(index, null, "");
257
283
 
258
284
  try {
285
+ const body = {
286
+ model: model.model,
287
+ base_url: model.base_url,
288
+ api_key: model.api_key_masked,
289
+ index
290
+ };
291
+ if (model.anthropic_format) body.anthropic_format = true;
292
+
259
293
  const testRes = await fetch("/api/config/test", {
260
294
  method: "POST",
261
295
  headers: { "Content-Type": "application/json" },
262
- body: JSON.stringify({
263
- model: model.model,
264
- base_url: model.base_url,
265
- api_key: model.api_key_masked,
266
- index
267
- })
296
+ body: JSON.stringify(body)
268
297
  });
269
298
  const testData = await testRes.json();
270
299
  _showTestResult(index, testData.ok, testData.message);
@@ -285,15 +314,25 @@ const Settings = (() => {
285
314
 
286
315
  saveBtn.disabled = true;
287
316
 
317
+ // Anthropic protocol is opted in only when the user picks the Anthropic
318
+ // provider in the modal. Other providers leave the flag absent so the
319
+ // backend's runtime inference (provider preset + model api overrides)
320
+ // decides — preserving e.g. OpenRouter's per-model anthropic-messages
321
+ // routing for Claude sub-models.
322
+ const anthropic_format = _modalSelectedProviderId === "anthropic";
323
+
288
324
  // Step 1: Test first
289
325
  saveBtn.textContent = I18n.t("settings.models.btn.testing");
290
326
  _showModalTestResult(null, "");
291
327
 
292
328
  try {
329
+ const testBody = { model, base_url, api_key, index };
330
+ if (anthropic_format) testBody.anthropic_format = true;
331
+
293
332
  const testRes = await fetch("/api/config/test", {
294
333
  method: "POST",
295
334
  headers: { "Content-Type": "application/json" },
296
- body: JSON.stringify({ model, base_url, api_key, index })
335
+ body: JSON.stringify(testBody)
297
336
  });
298
337
  const testData = await testRes.json();
299
338
  _showModalTestResult(testData.ok, testData.message);
@@ -317,7 +356,7 @@ const Settings = (() => {
317
356
  const existing = isNew ? {} : (_models[index] || {});
318
357
  const hasId = !!existing.id;
319
358
 
320
- const payload = { model, base_url, anthropic_format: false };
359
+ const payload = { model, base_url, anthropic_format };
321
360
  if (isNew) {
322
361
  if (document.getElementById("model-modal-set-default").checked) {
323
362
  payload.type = "default";
@@ -886,7 +925,10 @@ const Settings = (() => {
886
925
  model: card.querySelector(`[data-key="model"]`).value.trim(),
887
926
  base_url: card.querySelector(`[data-key="base_url"]`).value.trim(),
888
927
  api_key: card.querySelector(`[data-key="api_key"]`).value.trim(),
889
- anthropic_format: false,
928
+ // The inline card form has no provider picker — preserve whatever the
929
+ // model was saved with. The modal flow is the only place where the
930
+ // user can flip this flag.
931
+ anthropic_format: !!_models[index]?.anthropic_format,
890
932
  type: _models[index]?.type ?? null
891
933
  };
892
934
  }
@@ -0,0 +1,204 @@
1
+ // Workspace panel — lazy file tree for the active session's working directory.
2
+ // Lists one directory level at a time via GET /api/sessions/:id/files,
3
+ // expands/collapses folders in place, and downloads files on click via
4
+ // POST /api/file-action.
5
+ "use strict";
6
+
7
+ const Workspace = (() => {
8
+ const STORAGE_KEY = "clacky.workspace.open";
9
+
10
+ let _sessionId = null;
11
+ let _workingDir = null;
12
+ let _open = false;
13
+
14
+ const $ = (id) => document.getElementById(id);
15
+ const t = (key) => (typeof I18n !== "undefined" ? I18n.t(key) : key);
16
+
17
+ const ICON_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
18
+ const ICON_FILE = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
19
+ const ICON_CARET = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
20
+
21
+ function formatSize(bytes) {
22
+ if (bytes == null) return "";
23
+ if (bytes < 1024) return `${bytes} B`;
24
+ const units = ["KB", "MB", "GB", "TB"];
25
+ let n = bytes / 1024, i = 0;
26
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
27
+ return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
28
+ }
29
+
30
+ async function fetchEntries(relPath) {
31
+ const url = `/api/sessions/${encodeURIComponent(_sessionId)}/files?path=${encodeURIComponent(relPath || "")}`;
32
+ const resp = await fetch(url);
33
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
34
+ const data = await resp.json();
35
+ return data.entries || [];
36
+ }
37
+
38
+ function renderEntries(entries) {
39
+ const frag = document.createDocumentFragment();
40
+ if (!entries.length) {
41
+ const empty = document.createElement("div");
42
+ empty.className = "wt-empty";
43
+ empty.textContent = t("workspace.empty");
44
+ frag.appendChild(empty);
45
+ return frag;
46
+ }
47
+ for (const entry of entries) {
48
+ frag.appendChild(buildNode(entry));
49
+ }
50
+ return frag;
51
+ }
52
+
53
+ function buildNode(entry) {
54
+ const node = document.createElement("div");
55
+ node.className = "wt-node";
56
+
57
+ const row = document.createElement("div");
58
+ row.className = "wt-row";
59
+ row.title = entry.name;
60
+
61
+ const caret = document.createElement("span");
62
+ caret.className = "wt-caret" + (entry.type === "dir" ? "" : " leaf");
63
+ if (entry.type === "dir") caret.innerHTML = ICON_CARET;
64
+
65
+ const icon = document.createElement("span");
66
+ icon.className = "wt-icon";
67
+ icon.innerHTML = entry.type === "dir" ? ICON_FOLDER : ICON_FILE;
68
+
69
+ const name = document.createElement("span");
70
+ name.className = "wt-name";
71
+ name.textContent = entry.name;
72
+
73
+ row.appendChild(caret);
74
+ row.appendChild(icon);
75
+ row.appendChild(name);
76
+
77
+ if (entry.type === "file") {
78
+ const size = document.createElement("span");
79
+ size.className = "wt-size";
80
+ size.textContent = formatSize(entry.size);
81
+ row.appendChild(size);
82
+ }
83
+
84
+ node.appendChild(row);
85
+
86
+ if (entry.type === "dir") {
87
+ const children = document.createElement("div");
88
+ children.className = "wt-children";
89
+ children.style.display = "none";
90
+ node.appendChild(children);
91
+ row.addEventListener("click", () => toggleDir(entry, caret, children));
92
+ } else {
93
+ row.addEventListener("click", () => downloadFile(entry));
94
+ }
95
+
96
+ return node;
97
+ }
98
+
99
+ async function toggleDir(entry, caret, children) {
100
+ const isOpen = caret.classList.contains("open");
101
+ if (isOpen) {
102
+ caret.classList.remove("open");
103
+ children.style.display = "none";
104
+ return;
105
+ }
106
+ caret.classList.add("open");
107
+ children.style.display = "";
108
+ if (children.dataset.loaded === "1") return;
109
+
110
+ children.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
111
+ try {
112
+ const entries = await fetchEntries(entry.path);
113
+ children.innerHTML = "";
114
+ children.appendChild(renderEntries(entries));
115
+ children.dataset.loaded = "1";
116
+ } catch (err) {
117
+ console.error("workspace load failed:", err);
118
+ children.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
119
+ }
120
+ }
121
+
122
+ async function downloadFile(entry) {
123
+ const fullPath = _workingDir.replace(/\/+$/, "") + "/" + entry.path;
124
+ try {
125
+ const resp = await fetch("/api/file-action", {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ path: fullPath, action: "download" })
129
+ });
130
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
131
+ const blob = await resp.blob();
132
+ const url = URL.createObjectURL(blob);
133
+ const a = document.createElement("a");
134
+ a.href = url;
135
+ a.download = entry.name;
136
+ document.body.appendChild(a);
137
+ a.click();
138
+ a.remove();
139
+ URL.revokeObjectURL(url);
140
+ } catch (err) {
141
+ console.error("download failed:", err);
142
+ if (typeof Modal !== "undefined") Modal.toast(t("workspace.downloadFailed"), "error");
143
+ }
144
+ }
145
+
146
+ async function loadRoot() {
147
+ const tree = $("workspace-tree");
148
+ if (!tree || !_sessionId) return;
149
+ tree.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
150
+ try {
151
+ const entries = await fetchEntries("");
152
+ tree.innerHTML = "";
153
+ tree.appendChild(renderEntries(entries));
154
+ } catch (err) {
155
+ console.error("workspace load failed:", err);
156
+ tree.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
157
+ }
158
+ }
159
+
160
+ function applyOpenState() {
161
+ const panel = $("workspace-panel");
162
+ const opener = $("btn-workspace-open");
163
+ if (!panel) return;
164
+ const hasSession = !!_sessionId;
165
+ panel.classList.toggle("collapsed", !(_open && hasSession));
166
+ if (opener) opener.style.display = (!_open && hasSession) ? "" : "none";
167
+ }
168
+
169
+ function setOpen(open) {
170
+ _open = open;
171
+ try { localStorage.setItem(STORAGE_KEY, open ? "1" : "0"); } catch (_) {}
172
+ applyOpenState();
173
+ if (open) loadRoot();
174
+ }
175
+
176
+ return {
177
+ init() {
178
+ try { _open = localStorage.getItem(STORAGE_KEY) === "1"; } catch (_) { _open = false; }
179
+
180
+ const close = $("btn-workspace-close");
181
+ const opener = $("btn-workspace-open");
182
+ const refresh = $("btn-workspace-refresh");
183
+ if (close) close.addEventListener("click", () => setOpen(false));
184
+ if (opener) opener.addEventListener("click", () => setOpen(true));
185
+ if (refresh) refresh.addEventListener("click", () => loadRoot());
186
+
187
+ applyOpenState();
188
+ },
189
+
190
+ // Called from Sessions.updateInfoBar whenever the active session changes.
191
+ onSession(session) {
192
+ const newId = session ? session.id : null;
193
+ const newDir = session ? session.working_dir : null;
194
+ const changed = newId !== _sessionId || newDir !== _workingDir;
195
+ _sessionId = newId;
196
+ _workingDir = newDir;
197
+ applyOpenState();
198
+ if (changed && _open && _sessionId) loadRoot();
199
+ }
200
+ };
201
+ })();
202
+
203
+ document.addEventListener("DOMContentLoaded", () => Workspace.init());
204
+ window.Workspace = Workspace;
@@ -18,7 +18,6 @@
18
18
 
19
19
 
20
20
  WS.onEvent(ev => {
21
- console.log("[DEBUG] WS event received:", ev.type, ev);
22
21
  switch (ev.type) {
23
22
 
24
23
  // ── Internal WS lifecycle ──────────────────────────────────────────
@@ -273,11 +272,28 @@ WS.onEvent(ev => {
273
272
  break;
274
273
 
275
274
  case "error":
276
- if (!ev.session_id || ev.session_id === Sessions.activeId)
277
- Sessions.appendMsg("error", escapeHtml(ev.message));
275
+ if (!ev.session_id || ev.session_id === Sessions.activeId) {
276
+ renderErrorEvent(ev);
277
+ }
278
278
  break;
279
279
  }
280
280
  });
281
281
 
282
+ // ── Error rendering ────────────────────────────────────────────────────────
283
+
284
+ function renderErrorEvent(ev) {
285
+ if (ev.code === "insufficient_credit") {
286
+ const body = escapeHtml(I18n.t("error.insufficient_credit"));
287
+ const action = ev.top_up_url
288
+ ? ` <a href="${escapeHtml(ev.top_up_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(I18n.t("error.insufficient_credit.action"))} →</a>`
289
+ : "";
290
+ Sessions.appendMsg("error", `<span>${body}${action}</span>`);
291
+ return;
292
+ }
293
+ Sessions.appendMsg("error", escapeHtml(ev.message));
294
+ }
295
+
296
+ window.renderErrorEvent = renderErrorEvent;
297
+
282
298
 
283
299
  })();
data/lib/clacky.rb CHANGED
@@ -118,9 +118,6 @@ require_relative "clacky/tools/todo_manager"
118
118
  require_relative "clacky/tools/trash_manager"
119
119
  require_relative "clacky/tools/request_user_feedback"
120
120
  require_relative "clacky/tools/invoke_skill"
121
- require_relative "clacky/tools/undo_task"
122
- require_relative "clacky/tools/redo_task"
123
- require_relative "clacky/tools/list_tasks"
124
121
  require_relative "clacky/tools/browser"
125
122
  require_relative "clacky/tools/terminal"
126
123
  require_relative "clacky/mcp/client"
@@ -139,6 +136,15 @@ module Clacky
139
136
  class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
140
137
  class AgentError < StandardError; end
141
138
  class BadRequestError < AgentError; end # 400 errors — our request was malformed, history should be rolled back
139
+ class InsufficientCreditError < AgentError
140
+ attr_reader :error_code, :provider_id
141
+
142
+ def initialize(message, error_code: nil, provider_id: nil)
143
+ super(message)
144
+ @error_code = error_code
145
+ @provider_id = provider_id
146
+ end
147
+ end
142
148
  class RetryableError < StandardError; end # Transient errors that should be retried (5xx, HTML response, rate limit)
143
149
  # Upstream (model/router like OpenRouter/Bedrock) returned finish_reason="stop" together with
144
150
  # one or more tool_calls whose `arguments` JSON was truncated (empty, "{}" placeholder, or
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.5
4
+ version: 1.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-27 00:00:00.000000000 Z
11
+ date: 2026-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -360,6 +360,7 @@ files:
360
360
  - lib/clacky/default_parsers/pdf_parser_ocr.py
361
361
  - lib/clacky/default_parsers/pdf_parser_plumber.py
362
362
  - lib/clacky/default_parsers/pptx_parser.rb
363
+ - lib/clacky/default_parsers/wps_parser.rb
363
364
  - lib/clacky/default_parsers/xlsx_parser.rb
364
365
  - lib/clacky/default_skills/browser-setup/SKILL.md
365
366
  - lib/clacky/default_skills/channel-manager/SKILL.md
@@ -461,8 +462,6 @@ files:
461
462
  - lib/clacky/tools/glob.rb
462
463
  - lib/clacky/tools/grep.rb
463
464
  - lib/clacky/tools/invoke_skill.rb
464
- - lib/clacky/tools/list_tasks.rb
465
- - lib/clacky/tools/redo_task.rb
466
465
  - lib/clacky/tools/request_user_feedback.rb
467
466
  - lib/clacky/tools/security.rb
468
467
  - lib/clacky/tools/terminal.rb
@@ -472,7 +471,6 @@ files:
472
471
  - lib/clacky/tools/terminal/session_manager.rb
473
472
  - lib/clacky/tools/todo_manager.rb
474
473
  - lib/clacky/tools/trash_manager.rb
475
- - lib/clacky/tools/undo_task.rb
476
474
  - lib/clacky/tools/web_fetch.rb
477
475
  - lib/clacky/tools/web_search.rb
478
476
  - lib/clacky/tools/write.rb
@@ -572,6 +570,7 @@ files:
572
570
  - lib/clacky/web/vendor/katex/katex.min.js
573
571
  - lib/clacky/web/version.js
574
572
  - lib/clacky/web/weixin-qr.html
573
+ - lib/clacky/web/workspace.js
575
574
  - lib/clacky/web/ws-dispatcher.js
576
575
  - lib/clacky/web/ws.js
577
576
  - scripts/build/build.sh
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Clacky
4
- module Tools
5
- # Tool for listing task history (Time Machine feature)
6
- class ListTasks < Base
7
- self.tool_name = "list_tasks"
8
- self.tool_description = "List recent tasks in the task history with summaries. " \
9
- "Shows current task, past tasks, and future tasks (after undo). " \
10
- "Use when user wants to see task history or choose which task to undo/redo to."
11
- self.tool_category = "time_machine"
12
- self.tool_parameters = {
13
- type: "object",
14
- properties: {
15
- limit: {
16
- type: "integer",
17
- description: "Maximum number of recent tasks to show (default: 10)",
18
- default: 10
19
- }
20
- }
21
- }
22
-
23
- def execute(agent:, limit: 10, **_args)
24
- history = agent.get_task_history(limit: limit)
25
-
26
- if history.empty?
27
- return "No task history available."
28
- end
29
-
30
- lines = ["Task History:"]
31
- history.each do |task|
32
- indicator = case task[:status]
33
- when :current then "→"
34
- when :past then " "
35
- when :future then "↯"
36
- end
37
-
38
- branch_indicator = task[:has_branches] ? " ⎇" : ""
39
- lines << "#{indicator}#{branch_indicator} Task #{task[:task_id]}: #{task[:summary]}"
40
- end
41
-
42
- lines.join("\n")
43
- end
44
-
45
- def format_call(limit: 10, **_args)
46
- "Listing task history (limit: #{limit})..."
47
- end
48
-
49
- def format_result(result)
50
- result
51
- end
52
- end
53
- end
54
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Clacky
4
- module Tools
5
- # Tool for redoing a task after undo (Time Machine feature)
6
- class RedoTask < Base
7
- self.tool_name = "redo_task"
8
- self.tool_description = "Redo to a specific task after undo. Restores files to that task's state. " \
9
- "Use when user wants to go forward to a future task or switch to a different branch."
10
- self.tool_category = "time_machine"
11
- self.tool_parameters = {
12
- type: "object",
13
- properties: {
14
- task_id: {
15
- type: "integer",
16
- description: "The task ID to redo to (must be greater than current active task)"
17
- }
18
- },
19
- required: ["task_id"]
20
- }
21
-
22
- def execute(agent:, task_id:, **_args)
23
- result = agent.switch_to_task(task_id)
24
-
25
- if result[:success]
26
- result[:message]
27
- else
28
- "Error: #{result[:message]}"
29
- end
30
- end
31
-
32
- def format_call(task_id:, **_args)
33
- "Redoing to task #{task_id}..."
34
- end
35
-
36
- def format_result(result)
37
- result
38
- end
39
- end
40
- end
41
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Clacky
4
- module Tools
5
- # Tool for undoing the last task (Time Machine feature)
6
- class UndoTask < Base
7
- self.tool_name = "undo_task"
8
- self.tool_description = "Undo the last task and restore files to previous state. " \
9
- "Use when user wants to go back to previous state or undo recent changes."
10
- self.tool_category = "time_machine"
11
- self.tool_parameters = {
12
- type: "object",
13
- properties: {}
14
- }
15
-
16
- def execute(agent:, **_args)
17
- result = agent.undo_last_task
18
-
19
- if result[:success]
20
- result[:message]
21
- else
22
- "Error: #{result[:message]}"
23
- end
24
- end
25
-
26
- def format_call(**_args)
27
- "Undoing last task..."
28
- end
29
-
30
- def format_result(result)
31
- result
32
- end
33
- end
34
- end
35
- end