openclacky 1.5.19 → 1.5.21
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 +95 -0
- data/README.md +19 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +17 -3
- data/lib/clacky/agent/message_compressor_helper.rb +36 -2
- data/lib/clacky/agent/session_serializer.rb +45 -4
- data/lib/clacky/agent.rb +135 -55
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +10 -5
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +12 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +45 -4
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +38 -138
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +285 -53
- data/lib/clacky/server/model_prices.rb +10 -5
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +37 -7
- data/lib/clacky/server/web_ui_controller.rb +72 -0
- data/lib/clacky/session_manager.rb +28 -6
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +242 -39
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1158 -249
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +92 -97
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/features/archive/store.js +196 -0
- data/lib/clacky/web/features/archive/view.js +409 -0
- data/lib/clacky/web/features/backup/view.js +17 -7
- data/lib/clacky/web/features/billing/view.js +84 -10
- data/lib/clacky/web/features/channels/view.js +75 -29
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/trash/store.js +14 -11
- data/lib/clacky/web/features/trash/view.js +66 -13
- data/lib/clacky/web/features/workspace/view.js +421 -16
- data/lib/clacky/web/i18n.js +242 -70
- data/lib/clacky/web/index.html +270 -117
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +609 -267
- data/lib/clacky/web/settings.js +186 -32
- data/lib/clacky/web/ws-dispatcher.js +33 -21
- data/lib/clacky.rb +4 -2
- metadata +6 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "../agent_config"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
module Server
|
|
8
|
+
# Shared directory policy for master lifecycle and CLI discovery.
|
|
9
|
+
module PidFiles
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def directory
|
|
13
|
+
override = ENV["CLACKY_PID_DIR"]
|
|
14
|
+
configured = if override && !override.strip.empty?
|
|
15
|
+
override
|
|
16
|
+
else
|
|
17
|
+
AgentConfig.load.pid_dir
|
|
18
|
+
end
|
|
19
|
+
return Dir.tmpdir if configured.nil? || configured.strip.empty?
|
|
20
|
+
|
|
21
|
+
File.expand_path(configured)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
|
-
|
|
4
|
+
require_relative "pid_files"
|
|
5
5
|
require_relative "../banner"
|
|
6
6
|
require_relative "../version"
|
|
7
7
|
require_relative "../agent_config"
|
|
@@ -248,13 +248,19 @@ module Clacky
|
|
|
248
248
|
end
|
|
249
249
|
|
|
250
250
|
def pid_file_path
|
|
251
|
-
File.join(
|
|
251
|
+
File.join(pid_directory, "clacky-master-#{@port}.pid")
|
|
252
252
|
end
|
|
253
253
|
|
|
254
254
|
def write_pid_file
|
|
255
|
+
FileUtils.mkdir_p(pid_directory)
|
|
255
256
|
File.write(pid_file_path, Process.pid.to_s)
|
|
256
257
|
end
|
|
257
258
|
|
|
259
|
+
# Keep cleanup tied to the directory used at startup, even if config changes.
|
|
260
|
+
private def pid_directory
|
|
261
|
+
@pid_directory ||= PidFiles.directory
|
|
262
|
+
end
|
|
263
|
+
|
|
258
264
|
def remove_pid_file
|
|
259
265
|
# A replacement may already own the port after this master was stopped.
|
|
260
266
|
if File.exist?(pid_file_path) && File.read(pid_file_path).strip == Process.pid.to_s
|
|
@@ -318,7 +324,7 @@ module Clacky
|
|
|
318
324
|
end
|
|
319
325
|
|
|
320
326
|
private def kill_master_on_port(port)
|
|
321
|
-
path = File.join(
|
|
327
|
+
path = File.join(pid_directory, "clacky-master-#{port}.pid")
|
|
322
328
|
return unless File.exist?(path)
|
|
323
329
|
|
|
324
330
|
pid = File.read(path).strip.to_i
|
|
@@ -194,7 +194,8 @@ module Clacky
|
|
|
194
194
|
# profile: "general"|"coding"|nil
|
|
195
195
|
# nil = no agent_profile filter
|
|
196
196
|
# limit: max sessions to return (applies to NON-PINNED only; see below)
|
|
197
|
-
# before:
|
|
197
|
+
# before, before_id: cursor — only sessions sorting after (before, before_id)
|
|
198
|
+
# in the newest-first order; take it from a row's `cursor`
|
|
198
199
|
# (also applies to NON-PINNED only; pinned items are a separate
|
|
199
200
|
# logical section, they should never be paginated away)
|
|
200
201
|
# include_pinned: when true (default), all matching pinned sessions are
|
|
@@ -204,11 +205,16 @@ module Clacky
|
|
|
204
205
|
# an old session always see it at the top of the sidebar,
|
|
205
206
|
# even if many newer sessions exist.
|
|
206
207
|
#
|
|
208
|
+
# archived: false (default) → archived sessions are excluded from the
|
|
209
|
+
# result; true → ONLY archived sessions are returned. The two
|
|
210
|
+
# sets are disjoint, so every other caller keeps seeing exactly
|
|
211
|
+
# the sessions it saw before archiving existed. nil → no filter.
|
|
212
|
+
#
|
|
207
213
|
# Ordering of the returned array:
|
|
208
214
|
# [ ...all_pinned_matching (newest-first), ...non_pinned (newest-first, limited) ]
|
|
209
215
|
#
|
|
210
216
|
# source and profile are orthogonal — either can be nil independently.
|
|
211
|
-
def list(limit: nil, before: nil, q: nil, q_scope: "name", date: nil, type: nil, exclude_type: nil, include_pinned: true, project_id: nil, exclude_project: false)
|
|
217
|
+
def list(limit: nil, before: nil, before_id: nil, q: nil, q_scope: "name", date: nil, type: nil, exclude_type: nil, include_pinned: true, project_id: nil, exclude_project: false, archived: false)
|
|
212
218
|
return [] unless @session_manager
|
|
213
219
|
|
|
214
220
|
live = @mutex.synchronize do
|
|
@@ -222,6 +228,7 @@ module Clacky
|
|
|
222
228
|
model: model_info&.dig(:model), model_id: model_info&.dig(:id), name: live_name,
|
|
223
229
|
total_tasks: s[:agent]&.total_tasks, total_cost: s[:agent]&.total_cost,
|
|
224
230
|
cost_source: live_cost_source,
|
|
231
|
+
permission_mode: s[:agent]&.permission_mode,
|
|
225
232
|
reasoning_effort: s[:agent]&.reasoning_effort,
|
|
226
233
|
latest_latency: s[:agent]&.latest_latency,
|
|
227
234
|
card_model: model_info&.dig(:card_model),
|
|
@@ -233,6 +240,8 @@ module Clacky
|
|
|
233
240
|
|
|
234
241
|
all = @session_manager.all_sessions # already sorted newest-first
|
|
235
242
|
|
|
243
|
+
all = all.select { |s| archived ? s[:archived_at].to_s != "" : s[:archived_at].to_s == "" } unless archived.nil?
|
|
244
|
+
|
|
236
245
|
# ── type filter (replaces old source/profile split) ──────────────────
|
|
237
246
|
# type=coding → agent_profile == "coding"
|
|
238
247
|
# type=manual/cron/channel/setup → source match (profile=general implied)
|
|
@@ -290,9 +299,11 @@ module Clacky
|
|
|
290
299
|
pinned, non_pinned = all.partition { |s| s[:pinned] }
|
|
291
300
|
|
|
292
301
|
# `before` cursor ONLY applies to non-pinned (paginated) sessions.
|
|
293
|
-
# Cursor
|
|
294
|
-
|
|
295
|
-
|
|
302
|
+
# Cursor must match the sort key in all_sessions; the id breaks same-second ties.
|
|
303
|
+
if before
|
|
304
|
+
boundary = [before, before_id.to_s]
|
|
305
|
+
non_pinned = non_pinned.select { |s| (sort_key(s) <=> boundary) < 0 }
|
|
306
|
+
end
|
|
296
307
|
non_pinned = non_pinned.first(limit) if limit
|
|
297
308
|
|
|
298
309
|
# Pinned section: only included on the first page (before == nil) so
|
|
@@ -336,6 +347,7 @@ module Clacky
|
|
|
336
347
|
model: model_info&.dig(:model), model_id: model_info&.dig(:id),
|
|
337
348
|
name: live_name, total_tasks: s[:agent]&.total_tasks,
|
|
338
349
|
total_cost: s[:agent]&.total_cost, cost_source: s[:agent]&.cost_source,
|
|
350
|
+
permission_mode: s[:agent]&.permission_mode,
|
|
339
351
|
reasoning_effort: s[:agent]&.reasoning_effort,
|
|
340
352
|
latest_latency: s[:agent]&.latest_latency,
|
|
341
353
|
card_model: model_info&.dig(:card_model),
|
|
@@ -354,6 +366,10 @@ module Clacky
|
|
|
354
366
|
# Merge a single disk-side session hash with the corresponding live
|
|
355
367
|
# in-memory agent fields (may be nil) into the row shape the frontend
|
|
356
368
|
# consumes.
|
|
369
|
+
private def sort_key(s)
|
|
370
|
+
[s[:updated_at] || s[:created_at] || "", s[:session_id].to_s]
|
|
371
|
+
end
|
|
372
|
+
|
|
357
373
|
private def build_enriched_row(s, ls)
|
|
358
374
|
id = s[:session_id]
|
|
359
375
|
{
|
|
@@ -366,6 +382,7 @@ module Clacky
|
|
|
366
382
|
raw_message: ls&.dig(:raw_message),
|
|
367
383
|
model: ls&.dig(:model),
|
|
368
384
|
model_id: ls&.dig(:model_id),
|
|
385
|
+
permission_mode: ls&.dig(:permission_mode) || s.dig(:config, :permission_mode_override) || "confirm_all",
|
|
369
386
|
card_model: ls&.dig(:card_model),
|
|
370
387
|
sub_model: ls&.dig(:sub_model),
|
|
371
388
|
sub_model_options: ls&.dig(:sub_model_options) || [],
|
|
@@ -384,9 +401,12 @@ module Clacky
|
|
|
384
401
|
latest_latency: ls&.dig(:latest_latency),
|
|
385
402
|
reasoning_effort: ls&.dig(:reasoning_effort) || s.dig(:config, :reasoning_effort),
|
|
386
403
|
pinned: s[:pinned] || false,
|
|
404
|
+
archived_at: s[:archived_at],
|
|
387
405
|
channel_info: s[:channel_info],
|
|
388
406
|
project_id: s[:project_id],
|
|
389
407
|
todos: ls&.dig(:todos) || s[:todos] || [],
|
|
408
|
+
# Rows report live activity in updated_at; paging needs the on-disk sort key.
|
|
409
|
+
cursor: sort_key(s).then { |at, sid| { before: at, before_id: sid } },
|
|
390
410
|
}
|
|
391
411
|
end
|
|
392
412
|
|
|
@@ -414,10 +434,13 @@ module Clacky
|
|
|
414
434
|
public
|
|
415
435
|
|
|
416
436
|
# Count + latest updated_at for sessions of a given source, excluding
|
|
417
|
-
# those assigned to a project (they show in the project section instead)
|
|
437
|
+
# those assigned to a project (they show in the project section instead)
|
|
438
|
+
# and archived ones (they show in the archive view instead).
|
|
418
439
|
def group_stats(source)
|
|
419
440
|
return { count: 0, latest_updated_at: nil } unless @session_manager
|
|
420
|
-
group_all = @session_manager.all_sessions.select {
|
|
441
|
+
group_all = @session_manager.all_sessions.select {
|
|
442
|
+
|s| s_source(s) == source && s[:project_id].to_s.strip == "" && s[:archived_at].to_s == ""
|
|
443
|
+
}
|
|
421
444
|
latest = group_all.map { |s| s[:updated_at] || s[:created_at] }.compact.max
|
|
422
445
|
{ count: group_all.size, latest_updated_at: latest }
|
|
423
446
|
end
|
|
@@ -427,6 +450,13 @@ module Clacky
|
|
|
427
450
|
SessionManager::GROUPED_SOURCES.to_h { |source| [source, group_stats(source)] }
|
|
428
451
|
end
|
|
429
452
|
|
|
453
|
+
# How many sessions the archive holds. The archive page keeps only one
|
|
454
|
+
# page of rows in the browser, so its "delete all" confirmation takes the
|
|
455
|
+
# size from here — a header-only scan, unlike `list`.
|
|
456
|
+
def archived_count
|
|
457
|
+
@session_manager ? @session_manager.archived_count : 0
|
|
458
|
+
end
|
|
459
|
+
|
|
430
460
|
# Delete a session from registry (and interrupt its thread).
|
|
431
461
|
def delete(session_id)
|
|
432
462
|
@mutex.synchronize do
|
|
@@ -68,6 +68,71 @@ module Clacky
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Use the same contract as the CLI. The server owns the deadline;
|
|
72
|
+
# typing cancels automatic continuation but keeps the answer wait alive.
|
|
73
|
+
def request_feedback_with_countdown(seconds: 20)
|
|
74
|
+
pending = { id: @feedback_request_id || SecureRandom.uuid, cond: ConditionVariable.new,
|
|
75
|
+
deadline_ms: (Time.now.to_f * 1000).to_i + seconds * 1000 }
|
|
76
|
+
pending[:seconds] = seconds
|
|
77
|
+
pending[:deadline] = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
pending[:manual] = @feedback_permission_mode == :confirm_all
|
|
80
|
+
@pending_feedback = pending
|
|
81
|
+
end
|
|
82
|
+
emit("feedback_countdown", request_id: pending[:id], deadline_ms: pending[:manual] ? nil : pending[:deadline_ms])
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
until pending[:result]
|
|
85
|
+
remaining = pending[:deadline] - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
86
|
+
if !pending[:manual] && remaining <= 0
|
|
87
|
+
pending[:result] = :timeout
|
|
88
|
+
else
|
|
89
|
+
pending[:cond].wait(@mutex, pending[:manual] ? nil : remaining)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
pending[:result]
|
|
93
|
+
end
|
|
94
|
+
ensure
|
|
95
|
+
cleared = @mutex.synchronize do
|
|
96
|
+
if pending && @pending_feedback.equal?(pending)
|
|
97
|
+
@pending_feedback = nil
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
if cleared
|
|
102
|
+
emit("feedback_countdown", request_id: pending[:id], resolved: true,
|
|
103
|
+
resolution: pending[:result] || :cancelled)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def update_permission_mode(mode)
|
|
108
|
+
countdown = @mutex.synchronize do
|
|
109
|
+
@feedback_permission_mode = mode.to_sym
|
|
110
|
+
pending = @pending_feedback
|
|
111
|
+
if pending && !pending[:result]
|
|
112
|
+
if mode.to_sym == :auto_approve && pending[:manual]
|
|
113
|
+
pending[:deadline] = Process.clock_gettime(Process::CLOCK_MONOTONIC) + pending[:seconds]
|
|
114
|
+
pending[:deadline_ms] = (Time.now.to_f * 1000).to_i + pending[:seconds] * 1000
|
|
115
|
+
end
|
|
116
|
+
pending[:manual] = mode.to_sym != :auto_approve
|
|
117
|
+
pending[:cond].signal
|
|
118
|
+
{ request_id: pending[:id],
|
|
119
|
+
deadline_ms: pending[:manual] ? nil : pending[:deadline_ms] }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
emit("feedback_countdown", **countdown) if countdown
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def intervene_feedback(request_id)
|
|
126
|
+
@mutex.synchronize do
|
|
127
|
+
pending = @pending_feedback
|
|
128
|
+
return false unless pending && pending[:id] == request_id && !pending[:result]
|
|
129
|
+
pending[:result] = :intervened
|
|
130
|
+
pending[:cond].signal
|
|
131
|
+
emit("feedback_countdown", request_id: pending[:id], deadline_ms: nil)
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
71
136
|
# === Output display ===
|
|
72
137
|
|
|
73
138
|
def show_user_message(content, created_at: nil, files: [], source: :web, skill_command: nil, skill_command_display: nil, steering: false, references: [])
|
|
@@ -129,7 +194,9 @@ module Clacky
|
|
|
129
194
|
context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
|
|
130
195
|
first = questions.first || {}
|
|
131
196
|
|
|
197
|
+
@feedback_request_id = Clacky::Tools::AskUser.fetch_key(args_data, :request_id)
|
|
132
198
|
emit("request_feedback",
|
|
199
|
+
request_id: @feedback_request_id,
|
|
133
200
|
question: first[:question].to_s,
|
|
134
201
|
context: context,
|
|
135
202
|
options: first[:options] || [],
|
|
@@ -336,6 +403,11 @@ module Clacky
|
|
|
336
403
|
# The frontend's appendToolStdout will attach to the last visible .tool-item
|
|
337
404
|
# even when _liveLastToolItem is null (after the tab re-loaded).
|
|
338
405
|
def replay_live_state
|
|
406
|
+
countdown = @mutex.synchronize do
|
|
407
|
+
pending = @pending_feedback
|
|
408
|
+
pending ? { request_id: pending[:id], deadline_ms: pending[:manual] ? nil : pending[:deadline_ms] } : { resolved: true }
|
|
409
|
+
end
|
|
410
|
+
emit("feedback_countdown", **countdown)
|
|
339
411
|
return unless @live_progress_state
|
|
340
412
|
|
|
341
413
|
# Replay complete progress state (not just message).
|
|
@@ -216,7 +216,7 @@ module Clacky
|
|
|
216
216
|
def all_sessions(current_dir: nil, limit: nil)
|
|
217
217
|
sessions = Dir.glob(File.join(@sessions_dir, "*.json")).filter_map do |filepath|
|
|
218
218
|
load_session_file(filepath)
|
|
219
|
-
end.sort_by { |s| s[:updated_at] || s[:created_at] || "" }.reverse
|
|
219
|
+
end.sort_by { |s| [s[:updated_at] || s[:created_at] || "", s[:session_id].to_s] }.reverse
|
|
220
220
|
|
|
221
221
|
if current_dir
|
|
222
222
|
current_sessions = sessions.select { |s| s[:working_dir] == current_dir }
|
|
@@ -363,7 +363,10 @@ module Clacky
|
|
|
363
363
|
metas = files.filter_map { |f| m = load_session_metadata(f); m && m.merge(_path: f) }
|
|
364
364
|
.sort_by { |s| s[:updated_at] || s[:created_at] || "" }.reverse
|
|
365
365
|
|
|
366
|
-
|
|
366
|
+
# Archived sessions are user-curated, so they are exempt from the rolling
|
|
367
|
+
# cap exactly like pinned ones — an archive that silently empties itself
|
|
368
|
+
# would be worse than no archive at all.
|
|
369
|
+
non_pinned = metas.reject { |s| s[:pinned] || s[:archived_at].to_s != "" }
|
|
367
370
|
|
|
368
371
|
groups = non_pinned.group_by { |s| group_key_for(s) }
|
|
369
372
|
|
|
@@ -433,6 +436,12 @@ module Clacky
|
|
|
433
436
|
Clacky::Tools::TrashManager.permanent_delete_trash_session(session_id, sessions_dir: @sessions_dir)
|
|
434
437
|
end
|
|
435
438
|
|
|
439
|
+
# Permanently delete exactly these sessions from the trash. Returns the count deleted.
|
|
440
|
+
def permanent_delete_trash_sessions(session_ids)
|
|
441
|
+
require_relative "tools/trash_manager"
|
|
442
|
+
Clacky::Tools::TrashManager.empty_trash_sessions(sessions_dir: @sessions_dir, ids: session_ids)
|
|
443
|
+
end
|
|
444
|
+
|
|
436
445
|
# Keep file-trash on the same rolling window as sessions-trash.
|
|
437
446
|
def cleanup_file_trash(days: 8)
|
|
438
447
|
require_relative "tools/trash_manager"
|
|
@@ -547,10 +556,11 @@ module Clacky
|
|
|
547
556
|
# Read only the small scalar header of a session file without parsing the
|
|
548
557
|
# (potentially multi-MB) messages / time_machine payload. Session JSON is
|
|
549
558
|
# written with a stable field order, so the fields we care about here
|
|
550
|
-
# (session_id, pinned, created_at, updated_at, source) always
|
|
551
|
-
# first few hundred bytes, before any large array/object.
|
|
552
|
-
# lives much later and is intentionally NOT read — callers
|
|
553
|
-
# fall back to load_session_file for the rare candidate that
|
|
559
|
+
# (session_id, pinned, archived_at, created_at, updated_at, source) always
|
|
560
|
+
# sit in the first few hundred bytes, before any large array/object.
|
|
561
|
+
# `project_id` lives much later and is intentionally NOT read — callers
|
|
562
|
+
# that need it fall back to load_session_file for the rare candidate that
|
|
563
|
+
# requires it.
|
|
554
564
|
#
|
|
555
565
|
# Returns a Hash with symbol keys, or nil if the header can't be read.
|
|
556
566
|
METADATA_HEADER_BYTES = 16 * 1024
|
|
@@ -564,6 +574,7 @@ module Clacky
|
|
|
564
574
|
{
|
|
565
575
|
session_id: sid,
|
|
566
576
|
pinned: head[/"pinned"\s*:\s*(true|false)/, 1] == "true",
|
|
577
|
+
archived_at: head[/"archived_at"\s*:\s*"([^"]*)"/, 1],
|
|
567
578
|
created_at: head[/"created_at"\s*:\s*"([^"]*)"/, 1],
|
|
568
579
|
updated_at: head[/"updated_at"\s*:\s*"([^"]*)"/, 1],
|
|
569
580
|
source: head[/"source"\s*:\s*"([^"]*)"/, 1]
|
|
@@ -572,6 +583,17 @@ module Clacky
|
|
|
572
583
|
nil
|
|
573
584
|
end
|
|
574
585
|
|
|
586
|
+
# How many sessions sit in the archive. The WebUI offers "delete all" on
|
|
587
|
+
# that page, so it needs the size of the whole archive rather than the page
|
|
588
|
+
# it happens to have loaded — and a header-only read keeps the cost at one
|
|
589
|
+
# small read per file.
|
|
590
|
+
def archived_count
|
|
591
|
+
Dir.glob(File.join(@sessions_dir, "*.json")).count do |filepath|
|
|
592
|
+
meta = load_session_metadata(filepath)
|
|
593
|
+
meta && meta[:archived_at].to_s != ""
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
575
597
|
# Remove Time Machine snapshots that no longer belong to any known session.
|
|
576
598
|
# Snapshots are keyed by full session_id; session files are named by the
|
|
577
599
|
# 8-char id prefix, so a snapshot dir is an orphan when its prefix matches
|
data/lib/clacky/skill.rb
CHANGED
|
@@ -319,7 +319,7 @@ module Clacky
|
|
|
319
319
|
> should either:
|
|
320
320
|
>
|
|
321
321
|
> 1. Ask the user to start the server in another terminal: `clacky server`
|
|
322
|
-
> (then retry — the CLI auto-detects it via
|
|
322
|
+
> (then retry — the CLI auto-detects it via `clacky-master-*.pid` in the configured PID directory), or
|
|
323
323
|
> 2. If the task can be completed without the server API, skip those steps
|
|
324
324
|
> and tell the user which parts require the server.
|
|
325
325
|
>
|
|
@@ -110,7 +110,7 @@ module Clacky
|
|
|
110
110
|
recommended = recommended.to_i if recommended.is_a?(Numeric) || recommended.to_s =~ /\A-?\d+\z/
|
|
111
111
|
recommended = nil unless recommended.is_a?(Integer) && recommended >= 0 && recommended < options.size
|
|
112
112
|
|
|
113
|
-
{
|
|
113
|
+
normalized = {
|
|
114
114
|
question: text,
|
|
115
115
|
description: fetch_key(item, :description).to_s.strip,
|
|
116
116
|
options: options,
|
|
@@ -119,6 +119,9 @@ module Clacky
|
|
|
119
119
|
allow_free_text: options.empty? || truthy?(fetch_key(item, :allow_free_text)),
|
|
120
120
|
recommended: recommended
|
|
121
121
|
}
|
|
122
|
+
metadata = fetch_key(item, :metadata)
|
|
123
|
+
normalized[:metadata] = metadata if metadata.is_a?(Hash)
|
|
124
|
+
normalized
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def self.fetch_key(hash, key)
|
data/lib/clacky/tools/base.rb
CHANGED
|
@@ -14,8 +14,9 @@ module Clacky
|
|
|
14
14
|
attr_accessor :receives_tool_call_id
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# Host agent, injected at registration for extension tools
|
|
18
|
-
#
|
|
17
|
+
# Host agent, injected at registration for extension tools and for built-ins
|
|
18
|
+
# that need its public API (config, fan_out_labeled, fork_subagent,
|
|
19
|
+
# skill_loader).
|
|
19
20
|
attr_accessor :agent
|
|
20
21
|
|
|
21
22
|
def name
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -543,12 +543,21 @@ module Clacky
|
|
|
543
543
|
# Save original (full-resolution) PNG to disk before any downscaling
|
|
544
544
|
original_path = save_screenshot_to_disk(image_block["data"], suffix: "original")
|
|
545
545
|
|
|
546
|
+
config = screenshot_config
|
|
547
|
+
limit = config["screenshot_max_base64_bytes"]
|
|
548
|
+
max_base64_bytes = limit.is_a?(Integer) && limit.positive? ? limit : SCREENSHOT_MAX_BASE64_BYTES
|
|
546
549
|
image_data = png_downscale_base64(image_block["data"], SCREENSHOT_MAX_WIDTH)
|
|
550
|
+
if image_data.bytesize > max_base64_bytes
|
|
551
|
+
image_data = Clacky::Utils::FileProcessor.quantize_png_base64(image_data)
|
|
552
|
+
end
|
|
547
553
|
|
|
548
|
-
if image_data.bytesize >
|
|
554
|
+
if image_data.bytesize > max_base64_bytes
|
|
549
555
|
size_kb = image_data.bytesize / 1024
|
|
556
|
+
saved = original_path ? " The full-resolution file is saved at #{original_path}." : ""
|
|
550
557
|
return { action: "screenshot", success: false, profile: "user",
|
|
551
|
-
|
|
558
|
+
original_path: original_path,
|
|
559
|
+
output: "Screenshot still too large after compression (#{size_kb}KB).#{saved} " \
|
|
560
|
+
"Use action=snapshot instead." }
|
|
552
561
|
end
|
|
553
562
|
|
|
554
563
|
# Save compressed (800px) PNG for AI reference
|
|
@@ -560,6 +569,13 @@ module Clacky
|
|
|
560
569
|
output: "Screenshot captured." }
|
|
561
570
|
end
|
|
562
571
|
|
|
572
|
+
private def screenshot_config
|
|
573
|
+
config = YAMLCompat.safe_load(File.read(BROWSER_CONFIG_PATH), permitted_classes: [Date, Time, Symbol])
|
|
574
|
+
config.is_a?(Hash) ? config : {}
|
|
575
|
+
rescue StandardError
|
|
576
|
+
{}
|
|
577
|
+
end
|
|
578
|
+
|
|
563
579
|
private def png_downscale_base64(b64, max_width)
|
|
564
580
|
Clacky::Utils::FileProcessor.downscale_image_base64(
|
|
565
581
|
b64, "image/png", max_width: max_width
|
|
@@ -46,7 +46,7 @@ module Clacky
|
|
|
46
46
|
MAX_CONTENT_CHARS = 60_000
|
|
47
47
|
|
|
48
48
|
# Maximum characters per line (prevent single huge lines from bloating tokens)
|
|
49
|
-
MAX_LINE_CHARS =
|
|
49
|
+
MAX_LINE_CHARS = 8_000
|
|
50
50
|
|
|
51
51
|
def execute(path:, max_lines: 1000, start_line: nil, end_line: nil, working_dir: nil, image_max_width: nil)
|
|
52
52
|
# Expand path relative to working_dir when provided
|
|
@@ -43,7 +43,9 @@ module Clacky
|
|
|
43
43
|
:read_offset, # Integer, bytes already returned by previous read calls
|
|
44
44
|
:mutex, # per-session mutex for PTY writes
|
|
45
45
|
:resource_group, # Optional cgroup for task limits and OOM attribution
|
|
46
|
+
:script_paths, # Internal script paths for cleanup and output masking
|
|
46
47
|
:shell_name, # "zsh" | "bash" | "sh" — informs marker syntax & rc reload
|
|
48
|
+
:echo_head, # String, last `{ CMD` line written; strips its wrapped echo
|
|
47
49
|
keyword_init: true
|
|
48
50
|
)
|
|
49
51
|
|
|
@@ -77,6 +79,7 @@ module Clacky
|
|
|
77
79
|
read_offset: 0,
|
|
78
80
|
mutex: Mutex.new,
|
|
79
81
|
resource_group: resource_group,
|
|
82
|
+
script_paths: [],
|
|
80
83
|
shell_name: shell_name
|
|
81
84
|
)
|
|
82
85
|
@sessions[session.id] = session
|
|
@@ -116,7 +119,21 @@ module Clacky
|
|
|
116
119
|
# Forget a session (after it has been killed/exited). Does NOT kill
|
|
117
120
|
# the process — callers should kill first.
|
|
118
121
|
def forget(id)
|
|
119
|
-
@mutex.synchronize { @sessions.delete(id) }
|
|
122
|
+
session = @mutex.synchronize { @sessions.delete(id) }
|
|
123
|
+
cleanup_scripts(session) if session
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Cleanup must never replace the command's result with an unlink error.
|
|
127
|
+
def cleanup_scripts(session)
|
|
128
|
+
# The reader may clean up before buffered output is packaged.
|
|
129
|
+
# Preserve paths so error messages can still mask internal filenames.
|
|
130
|
+
Array(session.script_paths).each do |path|
|
|
131
|
+
begin
|
|
132
|
+
File.unlink(path)
|
|
133
|
+
rescue SystemCallError
|
|
134
|
+
# Best effort; retain the path for a later cleanup attempt.
|
|
135
|
+
end
|
|
136
|
+
end
|
|
120
137
|
end
|
|
121
138
|
|
|
122
139
|
# Refresh status of one session in-place (mutex-held).
|
|
@@ -188,6 +205,7 @@ module Clacky
|
|
|
188
205
|
s.log_io&.close rescue nil
|
|
189
206
|
s.writer&.close rescue nil
|
|
190
207
|
s.reader&.close rescue nil
|
|
208
|
+
cleanup_scripts(s)
|
|
191
209
|
end
|
|
192
210
|
end
|
|
193
211
|
|