openclacky 0.9.34 → 0.9.35
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 +30 -0
- data/lib/clacky/agent/cost_tracker.rb +1 -1
- data/lib/clacky/agent/llm_caller.rb +14 -10
- data/lib/clacky/agent/memory_updater.rb +1 -1
- data/lib/clacky/agent/session_serializer.rb +2 -0
- data/lib/clacky/agent/skill_manager.rb +1 -1
- data/lib/clacky/agent/tool_executor.rb +13 -16
- data/lib/clacky/agent/tool_registry.rb +0 -3
- data/lib/clacky/agent.rb +63 -38
- data/lib/clacky/agent_config.rb +5 -1
- data/lib/clacky/brand_config.rb +11 -27
- data/lib/clacky/cli.rb +36 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +1 -1
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1 -1
- data/lib/clacky/default_skills/new/SKILL.md +1 -1
- data/lib/clacky/default_skills/product-help/SKILL.md +1 -1
- data/lib/clacky/default_skills/recall-memory/SKILL.md +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -1
- data/lib/clacky/idle_compression_timer.rb +8 -0
- data/lib/clacky/json_ui_controller.rb +2 -1
- data/lib/clacky/plain_ui_controller.rb +10 -3
- data/lib/clacky/platform_http_client.rb +161 -1
- data/lib/clacky/server/channel/channel_manager.rb +5 -3
- data/lib/clacky/server/channel/channel_ui_controller.rb +6 -2
- data/lib/clacky/server/http_server.rb +235 -40
- data/lib/clacky/server/scheduler.rb +17 -16
- data/lib/clacky/server/session_registry.rb +1 -5
- data/lib/clacky/server/web_ui_controller.rb +7 -6
- data/lib/clacky/session_manager.rb +22 -0
- data/lib/clacky/skill.rb +19 -3
- data/lib/clacky/skill_loader.rb +5 -59
- data/lib/clacky/tools/browser.rb +25 -73
- data/lib/clacky/tools/security.rb +326 -0
- data/lib/clacky/tools/terminal/output_cleaner.rb +63 -0
- data/lib/clacky/tools/terminal/persistent_session.rb +247 -0
- data/lib/clacky/tools/terminal/session_manager.rb +208 -0
- data/lib/clacky/tools/terminal.rb +818 -0
- data/lib/clacky/tools/todo_manager.rb +6 -16
- data/lib/clacky/tools/trash_manager.rb +2 -2
- data/lib/clacky/ui2/components/input_area.rb +11 -2
- data/lib/clacky/ui2/layout_manager.rb +438 -488
- data/lib/clacky/ui2/output_buffer.rb +310 -0
- data/lib/clacky/ui2/ui_controller.rb +72 -21
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/encoding.rb +1 -1
- data/lib/clacky/utils/environment_detector.rb +43 -0
- data/lib/clacky/utils/model_pricing.rb +3 -3
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +479 -178
- data/lib/clacky/web/app.js +146 -4
- data/lib/clacky/web/auth.js +101 -0
- data/lib/clacky/web/i18n.js +35 -1
- data/lib/clacky/web/index.html +9 -2
- data/lib/clacky/web/sessions.js +254 -15
- data/lib/clacky/web/skills.js +20 -6
- data/lib/clacky/web/tasks.js +54 -2
- data/lib/clacky/web/theme.js +58 -20
- data/lib/clacky/web/ws.js +11 -2
- data/lib/clacky.rb +2 -2
- metadata +8 -3
- data/lib/clacky/tools/safe_shell.rb +0 -608
- data/lib/clacky/tools/shell.rb +0 -522
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff82f5ba11ed8afdfb840119c249c078f0b10024e746230eedf093169f63ae55
|
|
4
|
+
data.tar.gz: 0fe062fa3b73f168aeddde5a3a81a936a24faaa7be1b99329f8707bf52d89fbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b7f42edce36076b5d467b6eefafdd02c40f381e25b9b010f0a32074ca51baea1a20545acdcde6a59467eb7a9b9b4fd930600f15a1858dd7aa54907ed855d391
|
|
7
|
+
data.tar.gz: edf9c74ae0704914ed4012984ee8642294e097092123ef9c79521e985857068df615946d74b6b059c69dd0868683e6706db971ec393b6fca44f961bbd190b403
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.35] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Unified Terminal tool**: merged the old `safe_shell` and `shell` tools into a single `terminal` tool with persistent PTY sessions — the agent can now keep a shell session alive across tool calls, send input to running prompts, poll long-running commands, and safely interrupt them (`Ctrl+C` / `Ctrl+D`). Replaces 1000+ lines of duplicated shell-handling logic with a cleaner, better-tested implementation.
|
|
14
|
+
- **Access key authentication for server mode**: start the Web UI server with `--access-key <key>` (or `CLACKY_ACCESS_KEY` env var) to require a login before anyone can open sessions — safe to expose the Web UI over the network or to share a running instance
|
|
15
|
+
- **Session debug download**: from the Web UI you can now download a full session bundle (messages, tool calls, config) as a zip for debugging or sharing — useful for bug reports and post-mortems
|
|
16
|
+
- **Scheduler now saves session state**: scheduled/cron tasks now persist their session after each run, so you can inspect what the scheduled task actually did from the Web UI just like a normal session
|
|
17
|
+
- **Web UI visual redesign**: substantial redesign of the sidebar, session list, settings panel, and theme — cleaner spacing, better contrast in both light and dark modes, smoother transitions
|
|
18
|
+
- **Web UI & channel message interrupt**: you can now cancel an in-progress agent reply from the Web UI or from an IM channel (Feishu/WeCom/WeChat) mid-flight instead of waiting for it to finish
|
|
19
|
+
- **Terminal tool UI tips**: the Web UI now surfaces helpful inline tips when the agent is running a terminal command (e.g. "waiting for input", "process still running"), making long-running commands easier to follow
|
|
20
|
+
|
|
21
|
+
### Improved
|
|
22
|
+
- **Smaller tool descriptions**: trimmed the system-prompt footprint of `terminal`, `browser`, and `todo_manager` tool descriptions by ~40% — fewer tokens burned on every API call, slightly faster startup, and meaningfully cheaper sessions over time
|
|
23
|
+
- **Download fallback for skills & brand assets**: when the primary platform download host is unreachable (common in certain regions), the client now automatically falls back to a secondary URL — skill installs and brand asset fetches succeed in more network environments
|
|
24
|
+
- **Session cost shows "N/A" for unknown-price models**: instead of displaying `$0.00` when a model's pricing isn't registered, sessions now show "N/A" so you can tell the difference between "free call" and "we don't know the cost"
|
|
25
|
+
- **Faster, more accurate progress updates**: removed a delay in the progress spinner so the "Agent is thinking..." and tool-running indicators update immediately on state changes instead of a second later
|
|
26
|
+
- **No Claude-specific skill auto-loading**: removed legacy logic that special-cased loading `.claude/` skills at startup — skill loading is now uniform regardless of provider, reducing surprise behavior and confusing "unknown skill" errors
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **`file://` links now render and open correctly** (C-5552, C-5553): file:// links are no longer stripped during streaming in the Web UI, and clicking them now opens the file via the backend (including proper foreground focus on WSL via `cmd.exe /c start`). Also fixes silent drop of `file://` links in the CLI.
|
|
30
|
+
- **Idle `Ctrl+C` no longer crashes the CLI**: pressing Ctrl+C while the CLI is idle (no task running) now exits cleanly instead of raising an error
|
|
31
|
+
- **Session pinned status persists correctly** (C-5556): pinning a session in the Web UI now survives server restarts and is correctly restored from disk
|
|
32
|
+
- **Brand skill names follow language switch**: brand-supplied skill names in the Web UI sidebar now update immediately when you toggle the UI language (previously stuck in the initial language until reload)
|
|
33
|
+
- **New sessions get the default model**: fixed a case where newly created sessions could end up on a different model than the configured default; the "lite UI" mode is no longer automatically forced either
|
|
34
|
+
|
|
35
|
+
### More
|
|
36
|
+
- Large refactor of the UI2 `LayoutManager` + new `OutputBuffer` for cleaner CLI output line handling
|
|
37
|
+
- Agent progress-emission refactor for more consistent spinner/tool state reporting across Web, CLI, and channel UIs
|
|
38
|
+
- Removed the `safe_shell_spec` and `shell_spec` suites; replaced with a single, comprehensive `terminal_spec` (500+ lines of coverage)
|
|
39
|
+
|
|
10
40
|
## [0.9.34] - 2026-04-21
|
|
11
41
|
|
|
12
42
|
### Added
|
|
@@ -41,7 +41,7 @@ module Clacky
|
|
|
41
41
|
token_data = collect_iteration_tokens(usage, iteration_cost)
|
|
42
42
|
|
|
43
43
|
# Update session bar cost in real-time (don't wait for agent.run to finish)
|
|
44
|
-
@ui&.update_sessionbar(cost: @total_cost)
|
|
44
|
+
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
45
45
|
|
|
46
46
|
# Track cache usage statistics (global)
|
|
47
47
|
@cache_stats[:total_requests] += 1
|
|
@@ -35,12 +35,20 @@ module Clacky
|
|
|
35
35
|
# user experiences no extra delay.
|
|
36
36
|
#
|
|
37
37
|
# @return [Hash] API response with :content, :tool_calls, :usage, etc.
|
|
38
|
+
# NOTE on progress lifecycle:
|
|
39
|
+
# call_llm intentionally does NOT start or stop the progress indicator.
|
|
40
|
+
# Ownership lives with the caller (Agent#think for normal/compression
|
|
41
|
+
# paths, Agent#trigger_idle_compression for idle compression). This
|
|
42
|
+
# avoids nested active/done pairs clobbering each other — a bug that
|
|
43
|
+
# silently dropped the idle-compression summary line.
|
|
44
|
+
#
|
|
45
|
+
# Inside call_llm we only *update in place* during retries, so the
|
|
46
|
+
# already-live progress slot shows meaningful transient status
|
|
47
|
+
# ("Network failed… attempt 2/10", etc.).
|
|
38
48
|
private def call_llm
|
|
39
49
|
# Transition :fallback_active → :probing if cooling-off has expired.
|
|
40
50
|
@config.maybe_start_probing
|
|
41
51
|
|
|
42
|
-
@ui&.show_progress
|
|
43
|
-
|
|
44
52
|
tools_to_send = @tool_registry.all_definitions
|
|
45
53
|
|
|
46
54
|
max_retries = 10
|
|
@@ -68,7 +76,6 @@ module Clacky
|
|
|
68
76
|
handle_probe_success if @config.probing?
|
|
69
77
|
|
|
70
78
|
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
71
|
-
@ui&.show_progress(phase: "done")
|
|
72
79
|
retries += 1
|
|
73
80
|
|
|
74
81
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -90,13 +97,12 @@ module Clacky
|
|
|
90
97
|
sleep retry_delay
|
|
91
98
|
retry
|
|
92
99
|
else
|
|
93
|
-
|
|
94
|
-
#
|
|
100
|
+
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
101
|
+
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
95
102
|
raise AgentError, "[LLM] Network connection failed after #{max_retries} retries: #{e.message}"
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
rescue RetryableError => e
|
|
99
|
-
@ui&.show_progress(phase: "done")
|
|
100
106
|
retries += 1
|
|
101
107
|
|
|
102
108
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -127,12 +133,10 @@ module Clacky
|
|
|
127
133
|
sleep retry_delay
|
|
128
134
|
retry
|
|
129
135
|
else
|
|
130
|
-
|
|
131
|
-
#
|
|
136
|
+
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
137
|
+
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
132
138
|
raise AgentError, "[LLM] Service unavailable after #{current_max} retries"
|
|
133
139
|
end
|
|
134
|
-
ensure
|
|
135
|
-
@ui&.show_progress(phase: "done")
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
# Track cost and collect token usage data.
|
|
@@ -130,7 +130,7 @@ module Clacky
|
|
|
130
130
|
For each qualifying topic:
|
|
131
131
|
a. If a matching file exists → read it with `file_reader(path: "~/.clacky/memories/<filename>")`, then write an updated version (merge new + old, drop stale)
|
|
132
132
|
b. If no matching file → create a new one at `~/.clacky/memories/<new-filename>.md`
|
|
133
|
-
Use the `write` tool to save each file. Do NOT use `
|
|
133
|
+
Use the `write` tool to save each file. Do NOT use `terminal` or `file_reader` to list the directory.
|
|
134
134
|
|
|
135
135
|
## Hard constraints (CRITICAL)
|
|
136
136
|
- Each file MUST stay under 4000 characters of content (after the frontmatter)
|
|
@@ -10,6 +10,7 @@ module Clacky
|
|
|
10
10
|
def restore_session(session_data)
|
|
11
11
|
@session_id = session_data[:session_id]
|
|
12
12
|
@name = session_data[:name] || ""
|
|
13
|
+
@pinned = session_data[:pinned] || false
|
|
13
14
|
@history = MessageHistory.new(session_data[:messages] || [])
|
|
14
15
|
@todos = session_data[:todos] || [] # Restore todos from session
|
|
15
16
|
@iterations = session_data.dig(:stats, :total_iterations) || 0
|
|
@@ -77,6 +78,7 @@ module Clacky
|
|
|
77
78
|
{
|
|
78
79
|
session_id: @session_id,
|
|
79
80
|
name: @name,
|
|
81
|
+
pinned: @pinned,
|
|
80
82
|
created_at: @created_at,
|
|
81
83
|
updated_at: Time.now.iso8601,
|
|
82
84
|
working_dir: @working_dir,
|
|
@@ -456,7 +456,7 @@ module Clacky
|
|
|
456
456
|
# the real cumulative spend across all subagents
|
|
457
457
|
subagent_cost = result[:total_cost_usd] || 0.0
|
|
458
458
|
@total_cost += subagent_cost
|
|
459
|
-
@ui&.update_sessionbar(cost: @total_cost)
|
|
459
|
+
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
460
460
|
|
|
461
461
|
# Log completion
|
|
462
462
|
@ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{subagent_cost.round(4)} (total: $#{@total_cost.round(4)})")
|
|
@@ -21,7 +21,7 @@ module Clacky
|
|
|
21
21
|
# confirm_all → human present, truly wait for user input
|
|
22
22
|
true
|
|
23
23
|
when :confirm_safes
|
|
24
|
-
# Use
|
|
24
|
+
# Use Security module to check auto-execution safety
|
|
25
25
|
is_safe_operation?(tool_name, tool_params)
|
|
26
26
|
else
|
|
27
27
|
false
|
|
@@ -33,13 +33,14 @@ module Clacky
|
|
|
33
33
|
# @param tool_params [Hash, String] Tool parameters
|
|
34
34
|
# @return [Boolean] true if safe operation
|
|
35
35
|
def is_safe_operation?(tool_name, tool_params = {})
|
|
36
|
-
# For
|
|
37
|
-
if tool_name.to_s.downcase == '
|
|
36
|
+
# For terminal commands, defer to Security layer for the verdict.
|
|
37
|
+
if tool_name.to_s.downcase == 'terminal'
|
|
38
38
|
params = tool_params.is_a?(String) ? JSON.parse(tool_params) : tool_params
|
|
39
39
|
command = params[:command] || params['command']
|
|
40
|
-
|
|
40
|
+
# No command = session_id continuation / kill / action → safe by default.
|
|
41
|
+
return true unless command
|
|
41
42
|
|
|
42
|
-
return Tools::
|
|
43
|
+
return Clacky::Tools::Security.command_safe_for_auto_execution?(command)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
if tool_name.to_s.downcase == 'edit' || tool_name.to_s.downcase == 'write'
|
|
@@ -140,10 +141,10 @@ module Clacky
|
|
|
140
141
|
else
|
|
141
142
|
"Write(#{filename}) - create new"
|
|
142
143
|
end
|
|
143
|
-
when "
|
|
144
|
+
when "terminal"
|
|
144
145
|
cmd = args[:command] || ''
|
|
145
146
|
display_cmd = cmd.length > 30 ? "#{cmd[0..27]}..." : cmd
|
|
146
|
-
"
|
|
147
|
+
"terminal(\"#{display_cmd}\")"
|
|
147
148
|
else
|
|
148
149
|
"Allow #{call[:name]}"
|
|
149
150
|
end
|
|
@@ -241,7 +242,7 @@ module Clacky
|
|
|
241
242
|
# @return [Boolean] true if tool is potentially slow
|
|
242
243
|
private def potentially_slow_tool?(tool_name, args)
|
|
243
244
|
case tool_name.to_s.downcase
|
|
244
|
-
when '
|
|
245
|
+
when 'terminal'
|
|
245
246
|
# Check if the command is a slow command
|
|
246
247
|
command = args[:command] || args['command']
|
|
247
248
|
return false unless command
|
|
@@ -257,24 +258,20 @@ module Clacky
|
|
|
257
258
|
/make\s+(test|build)/,
|
|
258
259
|
/pytest/,
|
|
259
260
|
/jest/,
|
|
260
|
-
/sleep\s+\d+/
|
|
261
|
+
/sleep\s+\d+/
|
|
261
262
|
]
|
|
262
263
|
|
|
263
264
|
slow_patterns.any? { |pattern| command.match?(pattern) }
|
|
264
265
|
when 'web_fetch', 'web_search'
|
|
265
|
-
true
|
|
266
|
+
true
|
|
266
267
|
else
|
|
267
|
-
false
|
|
268
|
+
false
|
|
268
269
|
end
|
|
269
270
|
end
|
|
270
271
|
|
|
271
|
-
# Build progress message for tool execution
|
|
272
|
-
# @param tool_name [String] Name of the tool
|
|
273
|
-
# @param args [Hash] Tool arguments
|
|
274
|
-
# @return [String] Progress message
|
|
275
272
|
private def build_tool_progress_message(tool_name, args)
|
|
276
273
|
case tool_name.to_s.downcase
|
|
277
|
-
when '
|
|
274
|
+
when 'terminal'
|
|
278
275
|
"Running command"
|
|
279
276
|
when 'web_fetch'
|
|
280
277
|
"Fetching web page"
|
|
@@ -11,9 +11,6 @@ module Clacky
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def get(name)
|
|
14
|
-
# Handle shell alias to safe_shell for backward compatibility
|
|
15
|
-
name = 'safe_shell' if name == 'shell' && @tools.key?('safe_shell') && !@tools.key?('shell')
|
|
16
|
-
|
|
17
14
|
@tools[name] || raise(Clacky::ToolCallError, "Tool not found: #{name}")
|
|
18
15
|
end
|
|
19
16
|
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Clacky
|
|
|
43
43
|
attr_reader :session_id, :name, :history, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
44
44
|
:cache_stats, :cost_source, :ui, :skill_loader, :agent_profile,
|
|
45
45
|
:status, :error, :updated_at, :source
|
|
46
|
+
attr_accessor :pinned
|
|
46
47
|
|
|
47
48
|
def permission_mode
|
|
48
49
|
@config&.permission_mode&.to_s || ""
|
|
@@ -57,6 +58,7 @@ module Clacky
|
|
|
57
58
|
@hooks = HookManager.new
|
|
58
59
|
@session_id = session_id
|
|
59
60
|
@name = ""
|
|
61
|
+
@pinned = false
|
|
60
62
|
@history = MessageHistory.new
|
|
61
63
|
@todos = [] # Store todos in memory
|
|
62
64
|
@iterations = 0
|
|
@@ -188,6 +190,14 @@ module Clacky
|
|
|
188
190
|
end
|
|
189
191
|
|
|
190
192
|
def run(user_input, files: [])
|
|
193
|
+
# Show the "thinking" indicator as early as possible so the user gets
|
|
194
|
+
# immediate feedback after sending a message. Without this the UI stays
|
|
195
|
+
# silent during synchronous setup work (system prompt assembly, file
|
|
196
|
+
# parsing, history compression checks) before the first LLM call. The
|
|
197
|
+
# subsequent `think` call will re-emit show_progress, which is an
|
|
198
|
+
# idempotent update on the same progress UI element.
|
|
199
|
+
@ui&.show_progress
|
|
200
|
+
|
|
191
201
|
# Start new task for Time Machine
|
|
192
202
|
task_id = start_new_task
|
|
193
203
|
|
|
@@ -477,6 +487,11 @@ module Clacky
|
|
|
477
487
|
# Always clean up memory update messages, even if interrupted or error occurred
|
|
478
488
|
cleanup_memory_messages
|
|
479
489
|
|
|
490
|
+
# Safety net: ensure any lingering progress spinner is stopped.
|
|
491
|
+
# Normal paths close their own spinners; this guards against exceptions
|
|
492
|
+
# raised between a progress slot's active/done pair.
|
|
493
|
+
@ui&.show_progress(phase: "done")
|
|
494
|
+
|
|
480
495
|
# Shred any decrypted-script tmpdirs created during this run for encrypted brand skills.
|
|
481
496
|
# This covers the inline-injection path; the subagent path shreds immediately after
|
|
482
497
|
# subagent.run returns (see execute_skill_with_subagent).
|
|
@@ -491,6 +506,12 @@ module Clacky
|
|
|
491
506
|
raise AgentError, "API key is not configured"
|
|
492
507
|
end
|
|
493
508
|
|
|
509
|
+
# Ensure a thinking progress indicator is live for the duration of this
|
|
510
|
+
# LLM turn. This is idempotent — if `run` already started one at task
|
|
511
|
+
# entry (or a previous iteration left one running), the UI recognizes
|
|
512
|
+
# the bare reentry and preserves the existing spinner.
|
|
513
|
+
@ui&.show_progress
|
|
514
|
+
|
|
494
515
|
# Check if compression is needed
|
|
495
516
|
compression_context = compress_messages_if_needed(force: false)
|
|
496
517
|
|
|
@@ -500,6 +521,14 @@ module Clacky
|
|
|
500
521
|
@ui&.show_info(
|
|
501
522
|
"Message history compression starting (~#{compression_context[:original_token_count]} tokens, #{compression_context[:original_message_count]} messages) - Level #{compression_context[:compression_level]}"
|
|
502
523
|
)
|
|
524
|
+
# Take over the progress slot with a compression-specific message.
|
|
525
|
+
# handle_compression_response will close it with the summary line on
|
|
526
|
+
# success; on failure the outer ensure below finalizes it.
|
|
527
|
+
@ui&.show_progress(
|
|
528
|
+
"Compressing message history...",
|
|
529
|
+
progress_type: "idle_compress",
|
|
530
|
+
phase: "active"
|
|
531
|
+
)
|
|
503
532
|
compression_message = compression_context[:compression_message]
|
|
504
533
|
@history.append(compression_message)
|
|
505
534
|
compression_handled = false
|
|
@@ -518,13 +547,24 @@ module Clacky
|
|
|
518
547
|
# (with the user's new message as the last entry), producing consecutive user messages
|
|
519
548
|
# that confuse the LLM into echoing compression instructions.
|
|
520
549
|
@compression_level -= 1
|
|
550
|
+
# Close the compression progress slot so the spinner does not linger.
|
|
551
|
+
@ui&.show_progress(phase: "done")
|
|
521
552
|
end
|
|
522
553
|
end
|
|
523
554
|
return nil
|
|
524
555
|
end
|
|
525
556
|
|
|
526
|
-
# Normal LLM call
|
|
527
|
-
|
|
557
|
+
# Normal LLM call. call_llm no longer manages the progress lifecycle;
|
|
558
|
+
# we keep the spinner live across the call and finalize it here so the
|
|
559
|
+
# UI transitions cleanly to the assistant message that follows.
|
|
560
|
+
response = nil
|
|
561
|
+
begin
|
|
562
|
+
response = call_llm
|
|
563
|
+
rescue
|
|
564
|
+
# Ensure the spinner is stopped on any error path before it bubbles up.
|
|
565
|
+
@ui&.show_progress(phase: "done")
|
|
566
|
+
raise
|
|
567
|
+
end
|
|
528
568
|
|
|
529
569
|
# Handle truncated responses (when max_tokens limit is reached)
|
|
530
570
|
if response[:finish_reason] == "length"
|
|
@@ -533,6 +573,7 @@ module Clacky
|
|
|
533
573
|
|
|
534
574
|
if @task_truncation_count >= 3
|
|
535
575
|
# Too many truncations - task is too complex
|
|
576
|
+
@ui&.show_progress(phase: "done")
|
|
536
577
|
@ui&.show_error("Response truncated multiple times. Task is too complex.")
|
|
537
578
|
|
|
538
579
|
# Create a response that tells the user to break down the task
|
|
@@ -578,6 +619,9 @@ module Clacky
|
|
|
578
619
|
truncated: true
|
|
579
620
|
})
|
|
580
621
|
|
|
622
|
+
# Close the current spinner so the warning appears cleanly;
|
|
623
|
+
# the recursive think() call below will reopen a new one.
|
|
624
|
+
@ui&.show_progress(phase: "done")
|
|
581
625
|
@ui&.show_warning("Response truncated (#{@task_truncation_count}/3). Retrying with smaller steps...")
|
|
582
626
|
|
|
583
627
|
# Recursively retry
|
|
@@ -600,6 +644,11 @@ module Clacky
|
|
|
600
644
|
msg[:reasoning_content] = response[:reasoning_content] if response[:reasoning_content]
|
|
601
645
|
@history.append(msg)
|
|
602
646
|
|
|
647
|
+
# Close the thinking spinner before returning. The caller (run loop)
|
|
648
|
+
# is about to render the assistant message and/or tool invocations,
|
|
649
|
+
# which should appear after the spinner disappears.
|
|
650
|
+
@ui&.show_progress(phase: "done")
|
|
651
|
+
|
|
603
652
|
response
|
|
604
653
|
end
|
|
605
654
|
|
|
@@ -686,46 +735,24 @@ module Clacky
|
|
|
686
735
|
args[:agent] = self
|
|
687
736
|
end
|
|
688
737
|
|
|
689
|
-
# For safe_shell, skip safety check if user has already confirmed
|
|
690
|
-
if call[:name] == "safe_shell" || call[:name] == "shell"
|
|
691
|
-
args[:skip_safety_check] = true
|
|
692
|
-
end
|
|
693
|
-
|
|
694
738
|
# Inject working_dir so tools don't rely on Dir.chdir global state
|
|
695
739
|
args[:working_dir] = @working_dir if @working_dir
|
|
696
740
|
|
|
697
|
-
#
|
|
741
|
+
# Show progress immediately for every tool execution so the user
|
|
742
|
+
# always knows the agent is working. (Previously we deferred this by
|
|
743
|
+
# 2 seconds to avoid flicker in the legacy CLI TUI; that trade-off is
|
|
744
|
+
# no longer desirable now that progress is a first-class UI state in
|
|
745
|
+
# the Web UI and structured JSON UIs.)
|
|
698
746
|
progress_shown = false
|
|
699
|
-
progress_timer = nil
|
|
700
|
-
|
|
701
747
|
if @ui
|
|
702
748
|
progress_message = build_tool_progress_message(call[:name], args)
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
# The callback fires immediately on each read_nonblock chunk — no polling delay.
|
|
706
|
-
if (call[:name] == "shell" || call[:name] == "safe_shell") &&
|
|
707
|
-
@ui.respond_to?(:show_tool_stdout)
|
|
708
|
-
args[:on_output] = ->(stream, data) {
|
|
709
|
-
@ui.show_tool_stdout([data]) if stream == :stdout
|
|
710
|
-
}
|
|
711
|
-
end
|
|
712
|
-
|
|
713
|
-
progress_timer = Thread.new do
|
|
714
|
-
sleep 2
|
|
715
|
-
@ui.show_progress(progress_message, prefix_newline: false)
|
|
716
|
-
progress_shown = true
|
|
717
|
-
# Streaming is handled by on_output callback — no polling loop needed here
|
|
718
|
-
end
|
|
749
|
+
@ui.show_progress(progress_message, prefix_newline: false)
|
|
750
|
+
progress_shown = true
|
|
719
751
|
end
|
|
720
752
|
|
|
721
753
|
begin
|
|
722
754
|
result = tool.execute(**args)
|
|
723
755
|
ensure
|
|
724
|
-
# Cancel timer and clear progress if shown
|
|
725
|
-
if progress_timer
|
|
726
|
-
progress_timer.kill
|
|
727
|
-
progress_timer.join
|
|
728
|
-
end
|
|
729
756
|
@ui&.show_progress(phase: "done") if progress_shown
|
|
730
757
|
end
|
|
731
758
|
|
|
@@ -927,7 +954,7 @@ module Clacky
|
|
|
927
954
|
end
|
|
928
955
|
|
|
929
956
|
private def register_builtin_tools
|
|
930
|
-
@tool_registry.register(Tools::
|
|
957
|
+
@tool_registry.register(Tools::Terminal.new)
|
|
931
958
|
@tool_registry.register(Tools::FileReader.new)
|
|
932
959
|
@tool_registry.register(Tools::Write.new)
|
|
933
960
|
@tool_registry.register(Tools::Edit.new)
|
|
@@ -936,7 +963,6 @@ module Clacky
|
|
|
936
963
|
@tool_registry.register(Tools::WebSearch.new)
|
|
937
964
|
@tool_registry.register(Tools::WebFetch.new)
|
|
938
965
|
@tool_registry.register(Tools::TodoManager.new)
|
|
939
|
-
# @tool_registry.register(Tools::RunProject.new) # temporarily disabled
|
|
940
966
|
@tool_registry.register(Tools::RequestUserFeedback.new)
|
|
941
967
|
@tool_registry.register(Tools::InvokeSkill.new)
|
|
942
968
|
@tool_registry.register(Tools::UndoTask.new)
|
|
@@ -1250,13 +1276,13 @@ module Clacky
|
|
|
1250
1276
|
# [Download report](file:///path/to/file.pdf)
|
|
1251
1277
|
# 
|
|
1252
1278
|
#
|
|
1253
|
-
# Returns { text: String
|
|
1254
|
-
#
|
|
1279
|
+
# Returns { text: String (original content, unmodified),
|
|
1280
|
+
# files: Array<{name:, path:, inline:}> }
|
|
1255
1281
|
private def parse_file_links(content)
|
|
1256
1282
|
return { text: content, files: [] } if content.nil? || content.empty?
|
|
1257
1283
|
|
|
1258
1284
|
files = []
|
|
1259
|
-
|
|
1285
|
+
content.scan(/(!?)\[([^\]]*)\]\(file:\/\/([^)]+)\)/) do
|
|
1260
1286
|
inline = $1 == "!"
|
|
1261
1287
|
# URL-decode percent-encoded characters (e.g. Chinese filenames encoded by AI)
|
|
1262
1288
|
raw_path = CGI.unescape($3)
|
|
@@ -1264,9 +1290,8 @@ module Clacky
|
|
|
1264
1290
|
path = File.expand_path(raw_path)
|
|
1265
1291
|
Clacky::Logger.info("[parse_file_links] raw=#{$3.inspect} expanded=#{path.inspect} exist=#{File.exist?(path)}")
|
|
1266
1292
|
files << { name: name, path: path, inline: inline }
|
|
1267
|
-
""
|
|
1268
1293
|
end
|
|
1269
|
-
{ text:
|
|
1294
|
+
{ text: content, files: files }
|
|
1270
1295
|
end
|
|
1271
1296
|
|
|
1272
1297
|
# Emit assistant message to UI, parsing any embedded file:// links first.
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -238,7 +238,11 @@ module Clacky
|
|
|
238
238
|
# The injected lite model is runtime-only (not persisted to config.yml)
|
|
239
239
|
inject_provider_lite_model(models)
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
# Find the index of the model marked as "default" (type: default)
|
|
242
|
+
# Fall back to 0 if no model has type: default
|
|
243
|
+
default_index = models.find_index { |m| m["type"] == "default" } || 0
|
|
244
|
+
|
|
245
|
+
new(models: models, current_model_index: default_index)
|
|
242
246
|
end
|
|
243
247
|
|
|
244
248
|
# Auto-inject a lite model entry if the default model's provider supports one
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -533,9 +533,11 @@ module Clacky
|
|
|
533
533
|
dest_dir = File.join(brand_skills_dir, slug)
|
|
534
534
|
FileUtils.mkdir_p(dest_dir)
|
|
535
535
|
|
|
536
|
-
# Download the zip file to a temp path
|
|
536
|
+
# Download the zip file to a temp path via PlatformHttpClient so the
|
|
537
|
+
# primary → fallback host failover applies uniformly to every download.
|
|
537
538
|
tmp_zip = File.join(brand_skills_dir, "#{slug}.zip")
|
|
538
|
-
download_file(url, tmp_zip)
|
|
539
|
+
dl = platform_client.download_file(url, tmp_zip)
|
|
540
|
+
raise dl[:error].to_s unless dl[:success]
|
|
539
541
|
|
|
540
542
|
# Extract into dest_dir (overwrite existing files).
|
|
541
543
|
# Auto-detect whether the zip has a single root folder to strip.
|
|
@@ -1072,32 +1074,14 @@ module Clacky
|
|
|
1072
1074
|
end
|
|
1073
1075
|
|
|
1074
1076
|
# Download a remote URL to a local file path.
|
|
1077
|
+
#
|
|
1078
|
+
# Deprecated: this method now delegates to
|
|
1079
|
+
# Clacky::PlatformHttpClient#download_file so that every brand-skill download
|
|
1080
|
+
# benefits from primary → fallback host failover. Kept as a thin wrapper
|
|
1081
|
+
# so existing callers / tests that stub it continue to work.
|
|
1075
1082
|
private def download_file(url, dest, max_redirects: 10)
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
uri = URI.parse(url)
|
|
1080
|
-
max_redirects.times do
|
|
1081
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
1082
|
-
open_timeout: 15, read_timeout: 60) do |http|
|
|
1083
|
-
http.request_get(uri.request_uri) do |resp|
|
|
1084
|
-
case resp.code.to_i
|
|
1085
|
-
when 200
|
|
1086
|
-
File.open(dest, "wb") { |f| resp.read_body { |chunk| f.write(chunk) } }
|
|
1087
|
-
return
|
|
1088
|
-
when 301, 302, 303, 307, 308
|
|
1089
|
-
location = resp["location"]
|
|
1090
|
-
raise "Redirect with no Location header" if location.nil? || location.empty?
|
|
1091
|
-
|
|
1092
|
-
uri = URI.parse(location)
|
|
1093
|
-
break # break out of Net::HTTP.start, re-enter loop with new uri
|
|
1094
|
-
else
|
|
1095
|
-
raise "HTTP #{resp.code}"
|
|
1096
|
-
end
|
|
1097
|
-
end
|
|
1098
|
-
end
|
|
1099
|
-
end
|
|
1100
|
-
raise "Too many redirects"
|
|
1083
|
+
result = platform_client.download_file(url, dest)
|
|
1084
|
+
raise result[:error].to_s unless result[:success]
|
|
1101
1085
|
end
|
|
1102
1086
|
|
|
1103
1087
|
# Persist installed skill metadata to brand_skills.json.
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -663,6 +663,18 @@ module Clacky
|
|
|
663
663
|
|
|
664
664
|
# Set up interrupt handler
|
|
665
665
|
ui_controller.on_interrupt do |input_was_empty:|
|
|
666
|
+
# Priority 1: if idle compression work is actually in flight,
|
|
667
|
+
# Ctrl+C should stop compression — not exit the program. The
|
|
668
|
+
# compress thread rolls back history cleanly on AgentInterrupted.
|
|
669
|
+
if idle_timer.compressing?
|
|
670
|
+
idle_timer.cancel
|
|
671
|
+
ui_controller.show_progress(phase: "done")
|
|
672
|
+
ui_controller.set_idle_status
|
|
673
|
+
ui_controller.show_warning("Compression interrupted by user")
|
|
674
|
+
ui_controller.clear_input
|
|
675
|
+
next
|
|
676
|
+
end
|
|
677
|
+
|
|
666
678
|
if (not current_task_thread&.alive?) && input_was_empty
|
|
667
679
|
# Save final session state before exit
|
|
668
680
|
if session_manager && agent.total_tasks > 0
|
|
@@ -822,6 +834,30 @@ module Clacky
|
|
|
822
834
|
option :brand_test, type: :boolean, default: false,
|
|
823
835
|
desc: "Enable brand test mode: mock license activation without calling remote API"
|
|
824
836
|
def server
|
|
837
|
+
# ── Security gate ──────────────────────────────────────────────────────
|
|
838
|
+
# Binding to 0.0.0.0 exposes the server to the public network.
|
|
839
|
+
# Refuse to start unless CLACKY_ACCESS_KEY env var is set.
|
|
840
|
+
if options[:host] == "0.0.0.0" && ENV.fetch("CLACKY_ACCESS_KEY", "").strip.empty?
|
|
841
|
+
puts <<~MSG
|
|
842
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
843
|
+
║ ⚠️ Security Warning: Refusing to start ║
|
|
844
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
845
|
+
║ ║
|
|
846
|
+
║ Binding to 0.0.0.0 exposes Clacky to the public network. ║
|
|
847
|
+
║ You must set CLACKY_ACCESS_KEY before starting the server. ║
|
|
848
|
+
║ ║
|
|
849
|
+
║ Generate a secure key: ║
|
|
850
|
+
║ openssl rand -hex 32 ║
|
|
851
|
+
║ ║
|
|
852
|
+
║ Then export it: ║
|
|
853
|
+
║ export CLACKY_ACCESS_KEY=<your-generated-key> ║
|
|
854
|
+
║ ║
|
|
855
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
856
|
+
MSG
|
|
857
|
+
exit(1)
|
|
858
|
+
end
|
|
859
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
860
|
+
|
|
825
861
|
if ENV["CLACKY_WORKER"] == "1"
|
|
826
862
|
# ── Worker mode ───────────────────────────────────────────────────────
|
|
827
863
|
# Spawned by Master. Inherit the listen socket from the file descriptor
|
|
@@ -92,7 +92,7 @@ Run the setup script (full path is available in the supporting files list above)
|
|
|
92
92
|
```bash
|
|
93
93
|
ruby "SKILL_DIR/feishu_setup.rb"
|
|
94
94
|
```
|
|
95
|
-
**Important**: call `
|
|
95
|
+
**Important**: call `terminal` with `timeout: 180` — the script may wait up to 90s for a WebSocket connection in Phase 4.
|
|
96
96
|
|
|
97
97
|
**If exit code is 0:**
|
|
98
98
|
- The script completed successfully.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Ensure all output is flushed immediately so users see live progress
|
|
4
|
-
# even when the script is run inside a subprocess (
|
|
4
|
+
# even when the script is run inside a subprocess (terminal / Open3).
|
|
5
5
|
$stdout.sync = true
|
|
6
6
|
$stderr.sync = true
|
|
7
7
|
|
|
@@ -100,7 +100,7 @@ Do you want to reuse this binding? (y/n)
|
|
|
100
100
|
|
|
101
101
|
#### 3.2 Run the cloud project init script
|
|
102
102
|
Use the `cloud_project_init.sh` script from Supporting Files.
|
|
103
|
-
Run it with
|
|
103
|
+
Run it with `terminal` from the project directory (no arguments needed — it auto-reads `~/.clacky/clacky_cloud.yml`):
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
bash <absolute_path_to_cloud_project_init.sh>
|
|
@@ -45,7 +45,7 @@ file_reader(path: "~/.clacky/memories/<filename>")
|
|
|
45
45
|
|
|
46
46
|
2. Touch the file to update its mtime (LRU signal — keeps it surfaced in future recalls):
|
|
47
47
|
```
|
|
48
|
-
|
|
48
|
+
terminal(command: "touch ~/.clacky/memories/<filename>")
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Return ONLY the memory content, structured as:
|
|
@@ -62,7 +62,7 @@ Fast, opinionated skill creation without user interaction. This mode is used by
|
|
|
62
62
|
```
|
|
63
63
|
invoke_skill(
|
|
64
64
|
skill_name: "skill-creator",
|
|
65
|
-
task: "Create a skill to extract and summarize content from URLs. The skill should: 1) fetch the URL using
|
|
65
|
+
task: "Create a skill to extract and summarize content from URLs. The skill should: 1) fetch the URL using terminal with curl, 2) parse the HTML to extract main text content, 3) generate a concise markdown summary. Expected input: URL string. Expected output: markdown summary with title and key points.",
|
|
66
66
|
mode: "quick",
|
|
67
67
|
suggested_name: "url-summarizer"
|
|
68
68
|
)
|