openclacky 1.3.4 → 1.3.5
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 +27 -0
- data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
- data/lib/clacky/agent/session_serializer.rb +3 -2
- data/lib/clacky/agent/tool_executor.rb +0 -12
- data/lib/clacky/agent.rb +74 -9
- data/lib/clacky/api_extension.rb +81 -0
- data/lib/clacky/api_extension_loader.rb +13 -1
- data/lib/clacky/client.rb +14 -17
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +22 -0
- data/lib/clacky/default_agents/base_prompt.md +1 -0
- data/lib/clacky/default_extensions/meeting/handler.rb +331 -0
- data/lib/clacky/default_extensions/meeting/meeting.js +790 -0
- data/lib/clacky/default_extensions/meeting/meta.yml +3 -0
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +44 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +63 -0
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +114 -0
- data/lib/clacky/json_ui_controller.rb +1 -1
- data/lib/clacky/media/base.rb +60 -0
- data/lib/clacky/media/dashscope.rb +385 -21
- data/lib/clacky/media/gemini.rb +9 -0
- data/lib/clacky/media/generator.rb +52 -0
- data/lib/clacky/media/openai_compat.rb +166 -0
- data/lib/clacky/null_ui_controller.rb +13 -0
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/providers.rb +50 -2
- data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
- data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/http_server.rb +144 -9
- data/lib/clacky/server/session_registry.rb +4 -2
- data/lib/clacky/server/web_ui_controller.rb +3 -2
- data/lib/clacky/skill_loader.rb +14 -2
- data/lib/clacky/tools/terminal/output_cleaner.rb +1 -3
- data/lib/clacky/tools/terminal.rb +0 -43
- data/lib/clacky/ui2/components/modal_component.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +140 -31
- data/lib/clacky/ui_interface.rb +10 -1
- data/lib/clacky/utils/encoding.rb +25 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +145 -22
- data/lib/clacky/web/components/onboard.js +1 -14
- data/lib/clacky/web/features/brand/view.js +8 -5
- data/lib/clacky/web/features/channels/store.js +1 -20
- data/lib/clacky/web/features/mcp/store.js +1 -20
- data/lib/clacky/web/features/profile/store.js +1 -13
- data/lib/clacky/web/features/profile/view.js +16 -4
- data/lib/clacky/web/features/skills/store.js +6 -21
- data/lib/clacky/web/features/version/store.js +2 -0
- data/lib/clacky/web/i18n.js +24 -1
- data/lib/clacky/web/index.html +15 -0
- data/lib/clacky/web/sessions.js +141 -51
- data/lib/clacky/web/settings.js +34 -2
- data/lib/clacky/web/ws-dispatcher.js +11 -3
- data/lib/clacky.rb +12 -5
- metadata +8 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ui_interface"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
# A UI controller that swallows every event. Used for detached/background
|
|
7
|
+
# subagents whose intermediate output must never reach a real UI stream
|
|
8
|
+
# (e.g. the WebUI chat transcript). All UIInterface methods are inherited
|
|
9
|
+
# as no-ops, so nothing this agent emits is broadcast anywhere.
|
|
10
|
+
class NullUIController
|
|
11
|
+
include Clacky::UIInterface
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -88,6 +88,30 @@ module Clacky
|
|
|
88
88
|
"or-tts-gemini-2-5-pro" => "Gemini 2.5 Pro TTS"
|
|
89
89
|
},
|
|
90
90
|
"default_audio_model" => "or-tts-gemini-2-5-flash",
|
|
91
|
+
# Speech-to-text models served by the openclacky gateway, which
|
|
92
|
+
# routes them to Vertex AI Gemini (generateContent with inline
|
|
93
|
+
# audio parts). The gateway returns transcription text.
|
|
94
|
+
"stt_models" => [
|
|
95
|
+
"or-stt-gemini-3-5-flash",
|
|
96
|
+
"or-stt-gemini-1-5-pro"
|
|
97
|
+
],
|
|
98
|
+
"stt_model_aliases" => {
|
|
99
|
+
"or-stt-gemini-3-5-flash" => "Gemini 3.5 Flash STT",
|
|
100
|
+
"or-stt-gemini-1-5-pro" => "Gemini 1.5 Pro STT"
|
|
101
|
+
},
|
|
102
|
+
"default_stt_model" => "or-stt-gemini-3-5-flash",
|
|
103
|
+
# Video understanding models served by the openclacky gateway, which
|
|
104
|
+
# routes video frames to Gemini (generateContent with inline image
|
|
105
|
+
# parts). The gateway returns analysis text.
|
|
106
|
+
"video_understanding_models" => [
|
|
107
|
+
"or-gemini-3-5-flash",
|
|
108
|
+
"or-gemini-3-1-pro"
|
|
109
|
+
],
|
|
110
|
+
"video_understanding_model_aliases" => {
|
|
111
|
+
"or-gemini-3-5-flash" => "Gemini 3.5 Flash",
|
|
112
|
+
"or-gemini-3-1-pro" => "Gemini 3.1 Pro"
|
|
113
|
+
},
|
|
114
|
+
"default_video_understanding_model" => "or-gemini-3-5-flash",
|
|
91
115
|
# Default OCR sidecar — used when the primary model is text-only.
|
|
92
116
|
# Candidates are derived from the provider's vision-capable models;
|
|
93
117
|
# this just picks the cheap+fast default to surface in "auto" mode.
|
|
@@ -418,7 +442,7 @@ module Clacky
|
|
|
418
442
|
|
|
419
443
|
}.freeze
|
|
420
444
|
|
|
421
|
-
MEDIA_KINDS = %w[image video audio].freeze
|
|
445
|
+
MEDIA_KINDS = %w[image video audio stt video_understanding].freeze
|
|
422
446
|
|
|
423
447
|
class << self
|
|
424
448
|
# Check if a provider preset exists
|
|
@@ -549,11 +573,33 @@ module Clacky
|
|
|
549
573
|
preset&.dig("audio_model_aliases") || {}
|
|
550
574
|
end
|
|
551
575
|
|
|
576
|
+
def stt_models(provider_id)
|
|
577
|
+
preset = PRESETS[provider_id]
|
|
578
|
+
preset&.dig("stt_models") || []
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def stt_model_aliases(provider_id)
|
|
582
|
+
preset = PRESETS[provider_id]
|
|
583
|
+
preset&.dig("stt_model_aliases") || {}
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def video_understanding_models(provider_id)
|
|
587
|
+
preset = PRESETS[provider_id]
|
|
588
|
+
preset&.dig("video_understanding_models") || []
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def video_understanding_model_aliases(provider_id)
|
|
592
|
+
preset = PRESETS[provider_id]
|
|
593
|
+
preset&.dig("video_understanding_model_aliases") || {}
|
|
594
|
+
end
|
|
595
|
+
|
|
552
596
|
def media_model_aliases(provider_id, kind)
|
|
553
597
|
case kind.to_s
|
|
554
598
|
when "image" then image_model_aliases(provider_id)
|
|
555
599
|
when "video" then video_model_aliases(provider_id)
|
|
556
600
|
when "audio" then audio_model_aliases(provider_id)
|
|
601
|
+
when "stt" then stt_model_aliases(provider_id)
|
|
602
|
+
when "video_understanding" then video_understanding_model_aliases(provider_id)
|
|
557
603
|
else {}
|
|
558
604
|
end
|
|
559
605
|
end
|
|
@@ -599,13 +645,15 @@ module Clacky
|
|
|
599
645
|
|
|
600
646
|
# Unified entry for media model lookup by kind.
|
|
601
647
|
# @param provider_id [String]
|
|
602
|
-
# @param kind [String] one of "image" / "video" / "audio"
|
|
648
|
+
# @param kind [String] one of "image" / "video" / "audio" / "stt"
|
|
603
649
|
# @return [Array<String>]
|
|
604
650
|
def media_models(provider_id, kind)
|
|
605
651
|
case kind.to_s
|
|
606
652
|
when "image" then image_models(provider_id)
|
|
607
653
|
when "video" then video_models(provider_id)
|
|
608
654
|
when "audio" then audio_models(provider_id)
|
|
655
|
+
when "stt" then stt_models(provider_id)
|
|
656
|
+
when "video_understanding" then video_understanding_models(provider_id)
|
|
609
657
|
else []
|
|
610
658
|
end
|
|
611
659
|
end
|
|
@@ -145,7 +145,7 @@ module Clacky
|
|
|
145
145
|
send_text("Warning: #{message}")
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
-
def show_error(message, code: nil, top_up_url: nil)
|
|
148
|
+
def show_error(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
149
149
|
text = "Error: #{message}"
|
|
150
150
|
text += "\n#{top_up_url}" if top_up_url
|
|
151
151
|
send_text(text)
|
|
@@ -368,6 +368,8 @@ module Clacky
|
|
|
368
368
|
res.body = html
|
|
369
369
|
elsif req.path.start_with?("/webui_ext/")
|
|
370
370
|
self.send(:serve_webui_ext, req, res)
|
|
371
|
+
elsif req.path.start_with?("/builtin_ext/")
|
|
372
|
+
self.send(:serve_builtin_ext, req, res)
|
|
371
373
|
elsif req.path.start_with?("/agent_ui/")
|
|
372
374
|
self.send(:serve_agent_ui, req, res)
|
|
373
375
|
elsif req.path.start_with?("/panel_ui/")
|
|
@@ -462,6 +464,10 @@ module Clacky
|
|
|
462
464
|
600
|
|
463
465
|
elsif path == "/api/media/audio/speech"
|
|
464
466
|
120
|
|
467
|
+
elsif path == "/api/media/audio/transcriptions"
|
|
468
|
+
30
|
|
469
|
+
elsif path == "/api/media/video/understand"
|
|
470
|
+
60
|
|
465
471
|
elsif path.start_with?("/api/backup/download") || path == "/api/backup/run"
|
|
466
472
|
# Building a tar.gz of ~/.clacky (with session history) can take a while.
|
|
467
473
|
120
|
|
@@ -549,6 +555,8 @@ module Clacky
|
|
|
549
555
|
when ["POST", "/api/media/image"] then api_media_image(req, res)
|
|
550
556
|
when ["POST", "/api/media/video"] then api_media_video(req, res)
|
|
551
557
|
when ["POST", "/api/media/audio/speech"] then api_media_audio_speech(req, res)
|
|
558
|
+
when ["POST", "/api/media/audio/transcriptions"] then api_media_audio_transcriptions(req, res)
|
|
559
|
+
when ["POST", "/api/media/video/understand"] then api_media_video_understand(req, res)
|
|
552
560
|
when ["GET", "/api/media/types"] then api_media_types(res)
|
|
553
561
|
when ["GET", "/api/version"] then api_get_version(res)
|
|
554
562
|
when ["POST", "/api/version/upgrade"] then api_upgrade_version(req, res)
|
|
@@ -677,7 +685,7 @@ module Clacky
|
|
|
677
685
|
elsif method == "DELETE" && path.match?(%r{^/api/config/models/[^/]+$})
|
|
678
686
|
id = path.sub("/api/config/models/", "")
|
|
679
687
|
api_delete_model(id, res)
|
|
680
|
-
elsif method == "PATCH" && path.match?(%r{^/api/config/media/(image|video|audio)$})
|
|
688
|
+
elsif method == "PATCH" && path.match?(%r{^/api/config/media/(image|video|audio|stt|video_understanding)$})
|
|
681
689
|
kind = path.sub("/api/config/media/", "")
|
|
682
690
|
api_update_media_config(kind, req, res)
|
|
683
691
|
elsif method == "POST" && path.match?(%r{^/api/cron-tasks/[^/]+/run$})
|
|
@@ -1033,6 +1041,9 @@ module Clacky
|
|
|
1033
1041
|
# Absolute path to the user's WebUI extension directory.
|
|
1034
1042
|
WEBUI_EXT_ROOT = File.expand_path("~/.clacky/webui_ext")
|
|
1035
1043
|
|
|
1044
|
+
# Built-in extensions bundled with the gem (default_extensions/*/**.js).
|
|
1045
|
+
BUILTIN_EXT_ROOT = File.expand_path("../default_extensions", __dir__)
|
|
1046
|
+
|
|
1036
1047
|
# Build the full <script> payload injected at {{EXT_SCRIPTS}}:
|
|
1037
1048
|
# 1. global extensions — ~/.clacky/webui_ext/**/*.js (all agents)
|
|
1038
1049
|
# 2. agent-scoped UI — agents/<name>/webui/**/*.js (data-agent)
|
|
@@ -1042,6 +1053,7 @@ module Clacky
|
|
|
1042
1053
|
private def webui_ext_script_tags
|
|
1043
1054
|
[
|
|
1044
1055
|
panel_agents_script,
|
|
1056
|
+
builtin_ext_script_tags,
|
|
1045
1057
|
global_ext_script_tags,
|
|
1046
1058
|
agent_webui_script_tags,
|
|
1047
1059
|
official_panel_script_tags,
|
|
@@ -1093,6 +1105,21 @@ module Clacky
|
|
|
1093
1105
|
dirs
|
|
1094
1106
|
end
|
|
1095
1107
|
|
|
1108
|
+
# Built-in extensions from default_extensions/*/*.js — loaded before user
|
|
1109
|
+
# extensions so user ~/.clacky/webui_ext/ can override by ext id.
|
|
1110
|
+
private def builtin_ext_script_tags
|
|
1111
|
+
return "" unless Dir.exist?(BUILTIN_EXT_ROOT)
|
|
1112
|
+
|
|
1113
|
+
Dir.glob(File.join(BUILTIN_EXT_ROOT, "*", "**", "*.js")).sort.map do |abs|
|
|
1114
|
+
rel = abs.delete_prefix(BUILTIN_EXT_ROOT + "/")
|
|
1115
|
+
ext_id = "builtin/" + rel.delete_suffix(".js")
|
|
1116
|
+
src = "/builtin_ext/#{rel}"
|
|
1117
|
+
"<script>Clacky.ext._extBegin(#{ext_id.to_json}, #{nil.to_json}, #{nil.to_json})</script>" \
|
|
1118
|
+
"<script src=#{src.to_json} data-ext-id=#{ext_id.to_json}></script>" \
|
|
1119
|
+
"<script>Clacky.ext._extEnd()</script>"
|
|
1120
|
+
end.join("\n")
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1096
1123
|
# Global extensions — visible for all agents (unchanged legacy behavior).
|
|
1097
1124
|
private def global_ext_script_tags
|
|
1098
1125
|
ext_script_block(WEBUI_EXT_ROOT, "/webui_ext")
|
|
@@ -1173,6 +1200,31 @@ module Clacky
|
|
|
1173
1200
|
res.body = File.read(abs)
|
|
1174
1201
|
end
|
|
1175
1202
|
|
|
1203
|
+
private def serve_builtin_ext(req, res)
|
|
1204
|
+
rel = req.path.delete_prefix("/builtin_ext/")
|
|
1205
|
+
abs = File.expand_path(File.join(BUILTIN_EXT_ROOT, rel))
|
|
1206
|
+
|
|
1207
|
+
unless abs.start_with?(BUILTIN_EXT_ROOT + File::SEPARATOR) && File.file?(abs)
|
|
1208
|
+
res.status = 404
|
|
1209
|
+
res.body = "not found"
|
|
1210
|
+
return
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
ext = File.extname(abs)
|
|
1214
|
+
ctype = { ".js" => "application/javascript", ".css" => "text/css" }[ext]
|
|
1215
|
+
unless ctype
|
|
1216
|
+
res.status = 415
|
|
1217
|
+
res.body = "unsupported media type"
|
|
1218
|
+
return
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
res.status = 200
|
|
1222
|
+
res["Content-Type"] = ctype
|
|
1223
|
+
res["Cache-Control"] = "no-store"
|
|
1224
|
+
res["Pragma"] = "no-cache"
|
|
1225
|
+
res.body = File.read(abs)
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1176
1228
|
# Serve agents/<name>/webui/<file> from built-in or user agent dir.
|
|
1177
1229
|
# Path: /agent_ui/<name>/<rel>. User dir wins on name collision.
|
|
1178
1230
|
private def serve_agent_ui(req, res)
|
|
@@ -1405,6 +1457,59 @@ module Clacky
|
|
|
1405
1457
|
json_response(res, 500, { error: e.message })
|
|
1406
1458
|
end
|
|
1407
1459
|
|
|
1460
|
+
def api_media_audio_transcriptions(req, res)
|
|
1461
|
+
body = parse_json_body(req)
|
|
1462
|
+
return json_response(res, 400, { error: "Invalid JSON" }) unless body
|
|
1463
|
+
|
|
1464
|
+
audio_b64 = body["audio_base64"].to_s
|
|
1465
|
+
if audio_b64.empty?
|
|
1466
|
+
return json_response(res, 422, { error: "audio_base64 is required" })
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1469
|
+
mime_type = body["mime_type"].to_s
|
|
1470
|
+
mime_type = "audio/webm" if mime_type.empty?
|
|
1471
|
+
|
|
1472
|
+
result = Clacky::Media::Generator.new(@agent_config).generate_transcription(
|
|
1473
|
+
audio_base64: audio_b64,
|
|
1474
|
+
mime_type: mime_type
|
|
1475
|
+
)
|
|
1476
|
+
if result["success"]
|
|
1477
|
+
Clacky::Logger.info("[Media] stt generated model=#{result["model"]} provider=#{result["provider"]} cost_usd=#{result["cost_usd"].to_f}")
|
|
1478
|
+
end
|
|
1479
|
+
status = result["success"] ? 200 : 422
|
|
1480
|
+
json_response(res, status, result)
|
|
1481
|
+
rescue StandardError => e
|
|
1482
|
+
json_response(res, 500, { error: e.message })
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
def api_media_video_understand(req, res)
|
|
1486
|
+
body = parse_json_body(req)
|
|
1487
|
+
return json_response(res, 400, { error: "Invalid JSON" }) unless body
|
|
1488
|
+
|
|
1489
|
+
video_b64 = body["video_base64"].to_s
|
|
1490
|
+
if video_b64.empty?
|
|
1491
|
+
return json_response(res, 422, { error: "video_base64 is required" })
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
mime_type = body["mime_type"].to_s
|
|
1495
|
+
mime_type = "image/png" if mime_type.empty?
|
|
1496
|
+
|
|
1497
|
+
prompt = body["prompt"].to_s
|
|
1498
|
+
|
|
1499
|
+
result = Clacky::Media::Generator.new(@agent_config).understand_video(
|
|
1500
|
+
video_base64: video_b64,
|
|
1501
|
+
mime_type: mime_type,
|
|
1502
|
+
prompt: prompt
|
|
1503
|
+
)
|
|
1504
|
+
if result["success"]
|
|
1505
|
+
Clacky::Logger.info("[Media] video_understanding generated model=#{result["model"]} provider=#{result["provider"]} cost_usd=#{result["cost_usd"].to_f}")
|
|
1506
|
+
end
|
|
1507
|
+
status = result["success"] ? 200 : 422
|
|
1508
|
+
json_response(res, status, result)
|
|
1509
|
+
rescue StandardError => e
|
|
1510
|
+
json_response(res, 500, { error: e.message })
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1408
1513
|
private def log_media_usage(result, prompt:)
|
|
1409
1514
|
usage = result["usage"]
|
|
1410
1515
|
cost = result["cost_usd"]
|
|
@@ -1967,6 +2072,7 @@ module Clacky
|
|
|
1967
2072
|
product_name: brand.product_name,
|
|
1968
2073
|
homepage_url: brand.homepage_url,
|
|
1969
2074
|
logo_url: brand.logo_url,
|
|
2075
|
+
theme_color: brand.theme_color,
|
|
1970
2076
|
test_mode: @brand_test,
|
|
1971
2077
|
distribution_refresh_pending: refresh_pending
|
|
1972
2078
|
})
|
|
@@ -2038,7 +2144,8 @@ module Clacky
|
|
|
2038
2144
|
ok: true,
|
|
2039
2145
|
product_name: result[:product_name] || brand.product_name,
|
|
2040
2146
|
user_id: result[:user_id] || brand.license_user_id,
|
|
2041
|
-
user_licensed: brand.user_licensed
|
|
2147
|
+
user_licensed: brand.user_licensed?,
|
|
2148
|
+
theme_color: brand.theme_color
|
|
2042
2149
|
})
|
|
2043
2150
|
else
|
|
2044
2151
|
json_response(res, 422, { ok: false, error: result[:message] })
|
|
@@ -5820,6 +5927,22 @@ module Clacky
|
|
|
5820
5927
|
Clacky::Logger.error("[interrupt] force_close_session_sockets error: #{e.class}: #{e.message}")
|
|
5821
5928
|
end
|
|
5822
5929
|
|
|
5930
|
+
# Run a task in a session immediately in the background, without waiting
|
|
5931
|
+
# for the client to subscribe. The user bubble is persisted via
|
|
5932
|
+
# display_text (Agent#run → history → replay_history), so the frontend
|
|
5933
|
+
# only needs to navigate over and load history — no realtime broadcast,
|
|
5934
|
+
# no subscribe-timing race. This is the stable entry point for
|
|
5935
|
+
# programmatic "create session + run now" flows (spawn, extensions).
|
|
5936
|
+
def run_session_task(session_id, prompt, display_message: nil)
|
|
5937
|
+
return unless @registry.exist?(session_id)
|
|
5938
|
+
|
|
5939
|
+
agent = nil
|
|
5940
|
+
@registry.with_session(session_id) { |s| agent = s[:agent] }
|
|
5941
|
+
return unless agent
|
|
5942
|
+
|
|
5943
|
+
run_agent_task(session_id, agent) { agent.run(prompt, display_text: display_message) }
|
|
5944
|
+
end
|
|
5945
|
+
|
|
5823
5946
|
# Start the pending task for a session.
|
|
5824
5947
|
# Called when the client sends "run_task" over WS — by that point the
|
|
5825
5948
|
# client has already subscribed, so every broadcast will be delivered.
|
|
@@ -5827,18 +5950,29 @@ module Clacky
|
|
|
5827
5950
|
return unless @registry.exist?(session_id)
|
|
5828
5951
|
|
|
5829
5952
|
session = @registry.get(session_id)
|
|
5830
|
-
prompt
|
|
5831
|
-
working_dir
|
|
5953
|
+
prompt = session[:pending_task]
|
|
5954
|
+
working_dir = session[:pending_working_dir]
|
|
5955
|
+
display_message = session[:pending_display_message]
|
|
5832
5956
|
return unless prompt # nothing pending
|
|
5833
5957
|
|
|
5834
5958
|
# Clear the pending fields so a re-connect doesn't re-run
|
|
5835
|
-
@registry.update(session_id, pending_task: nil, pending_working_dir: nil)
|
|
5959
|
+
@registry.update(session_id, pending_task: nil, pending_working_dir: nil, pending_display_message: nil)
|
|
5836
5960
|
|
|
5837
5961
|
agent = nil
|
|
5838
5962
|
@registry.with_session(session_id) { |s| agent = s[:agent] }
|
|
5839
5963
|
return unless agent
|
|
5840
5964
|
|
|
5841
|
-
|
|
5965
|
+
# Surface a user message on screen before the agent starts thinking, so
|
|
5966
|
+
# programmatically-submitted tasks (e.g. meeting summarization) don't
|
|
5967
|
+
# appear as a thinking spinner with no preceding message. When a short
|
|
5968
|
+
# display_message is provided we show that instead of the full prompt.
|
|
5969
|
+
if display_message
|
|
5970
|
+
web_ui = nil
|
|
5971
|
+
@registry.with_session(session_id) { |s| web_ui = s[:ui] }
|
|
5972
|
+
web_ui&.show_user_message(display_message, source: :web)
|
|
5973
|
+
end
|
|
5974
|
+
|
|
5975
|
+
run_agent_task(session_id, agent) { agent.run(prompt, display_text: display_message) }
|
|
5842
5976
|
end
|
|
5843
5977
|
|
|
5844
5978
|
# Interrupt every running agent thread and persist its session state.
|
|
@@ -5921,10 +6055,11 @@ module Clacky
|
|
|
5921
6055
|
top_up_url = preset && preset["website_url"]
|
|
5922
6056
|
end
|
|
5923
6057
|
user_message = e.respond_to?(:display_message) && e.display_message ? e.display_message : e.message
|
|
5924
|
-
|
|
6058
|
+
raw_message = e.respond_to?(:raw_message) ? e.raw_message : nil
|
|
6059
|
+
@registry.update(session_id, status: :error, error: user_message, error_code: code, top_up_url: top_up_url, raw_message: raw_message)
|
|
5925
6060
|
broadcast_session_update(session_id)
|
|
5926
|
-
web_ui&.show_error(user_message, code: code, top_up_url: top_up_url)
|
|
5927
|
-
@session_manager.save(agent.to_session_data(status: :error, error_message: user_message))
|
|
6061
|
+
web_ui&.show_error(user_message, code: code, top_up_url: top_up_url, raw_message: raw_message)
|
|
6062
|
+
@session_manager.save(agent.to_session_data(status: :error, error_message: user_message, raw_message: raw_message))
|
|
5928
6063
|
end
|
|
5929
6064
|
@registry.with_session(session_id) { |s| s[:thread] = thread }
|
|
5930
6065
|
end
|
|
@@ -44,6 +44,7 @@ module Clacky
|
|
|
44
44
|
error: nil,
|
|
45
45
|
error_code: nil,
|
|
46
46
|
top_up_url: nil,
|
|
47
|
+
raw_message: nil,
|
|
47
48
|
updated_at: nil,
|
|
48
49
|
agent: nil,
|
|
49
50
|
ui: nil,
|
|
@@ -169,7 +170,7 @@ module Clacky
|
|
|
169
170
|
live_name = s[:agent]&.name
|
|
170
171
|
live_name = nil if live_name&.empty?
|
|
171
172
|
live_cost_source = s[:agent]&.cost_source
|
|
172
|
-
{ status: s[:status], error: s[:error], error_code: s[:error_code], top_up_url: s[:top_up_url],
|
|
173
|
+
{ status: s[:status], error: s[:error], error_code: s[:error_code], top_up_url: s[:top_up_url], raw_message: s[:raw_message],
|
|
173
174
|
updated_at: s[:updated_at]&.iso8601,
|
|
174
175
|
model: model_info&.dig(:model), model_id: model_info&.dig(:id), name: live_name,
|
|
175
176
|
total_tasks: s[:agent]&.total_tasks, total_cost: s[:agent]&.total_cost,
|
|
@@ -269,7 +270,7 @@ module Clacky
|
|
|
269
270
|
model_info = s[:agent]&.current_model_info
|
|
270
271
|
live_name = s[:agent]&.name
|
|
271
272
|
live_name = nil if live_name&.empty?
|
|
272
|
-
{ status: s[:status], error: s[:error], error_code: s[:error_code], top_up_url: s[:top_up_url],
|
|
273
|
+
{ status: s[:status], error: s[:error], error_code: s[:error_code], top_up_url: s[:top_up_url], raw_message: s[:raw_message],
|
|
273
274
|
updated_at: s[:updated_at]&.iso8601,
|
|
274
275
|
model: model_info&.dig(:model), model_id: model_info&.dig(:id),
|
|
275
276
|
name: live_name, total_tasks: s[:agent]&.total_tasks,
|
|
@@ -296,6 +297,7 @@ module Clacky
|
|
|
296
297
|
error: ls ? ls[:error] : nil,
|
|
297
298
|
error_code: ls&.dig(:error_code),
|
|
298
299
|
top_up_url: ls&.dig(:top_up_url),
|
|
300
|
+
raw_message: ls&.dig(:raw_message),
|
|
299
301
|
model: ls&.dig(:model),
|
|
300
302
|
model_id: ls&.dig(:model_id),
|
|
301
303
|
card_model: ls&.dig(:card_model),
|
|
@@ -230,12 +230,13 @@ module Clacky
|
|
|
230
230
|
@broadcaster.call(@session_id, event)
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
def show_error(message, code: nil, top_up_url: nil)
|
|
233
|
+
def show_error(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
234
234
|
payload = { message: message }
|
|
235
235
|
payload[:code] = code if code
|
|
236
236
|
payload[:top_up_url] = top_up_url if top_up_url
|
|
237
|
+
payload[:raw_message] = raw_message if raw_message
|
|
237
238
|
emit("error", **payload)
|
|
238
|
-
forward_to_subscribers { |sub| sub.show_error(message, code: code, top_up_url: top_up_url) }
|
|
239
|
+
forward_to_subscribers { |sub| sub.show_error(message, code: code, top_up_url: top_up_url, raw_message: raw_message) }
|
|
239
240
|
end
|
|
240
241
|
|
|
241
242
|
def show_success(message)
|
data/lib/clacky/skill_loader.rb
CHANGED
|
@@ -441,13 +441,11 @@ module Clacky
|
|
|
441
441
|
|
|
442
442
|
# Load default skills from gem's default_skills directory
|
|
443
443
|
private def load_default_skills
|
|
444
|
-
# Get the gem's lib directory
|
|
445
444
|
gem_lib_dir = File.expand_path("../", __dir__)
|
|
446
445
|
default_skills_dir = File.join(gem_lib_dir, "clacky", "default_skills")
|
|
447
446
|
|
|
448
447
|
return unless Dir.exist?(default_skills_dir)
|
|
449
448
|
|
|
450
|
-
# Load each skill directory
|
|
451
449
|
Dir.glob(File.join(default_skills_dir, "*/SKILL.md")).each do |skill_file|
|
|
452
450
|
skill_dir = File.dirname(skill_file)
|
|
453
451
|
skill_name = File.basename(skill_dir)
|
|
@@ -459,6 +457,20 @@ module Clacky
|
|
|
459
457
|
@errors << "Failed to load default skill #{skill_name}: #{e.message}"
|
|
460
458
|
end
|
|
461
459
|
end
|
|
460
|
+
|
|
461
|
+
# Also load skills bundled inside default_extensions/*/skills/
|
|
462
|
+
ext_skills_dir = File.join(gem_lib_dir, "clacky", "default_extensions")
|
|
463
|
+
Dir.glob(File.join(ext_skills_dir, "*/skills/*/SKILL.md")).each do |skill_file|
|
|
464
|
+
skill_dir = File.dirname(skill_file)
|
|
465
|
+
skill_name = File.basename(skill_dir)
|
|
466
|
+
|
|
467
|
+
begin
|
|
468
|
+
skill = Skill.new(Pathname.new(skill_dir))
|
|
469
|
+
register_skill(skill, source: :default)
|
|
470
|
+
rescue StandardError => e
|
|
471
|
+
@errors << "Failed to load default skill #{skill_name}: #{e.message}"
|
|
472
|
+
end
|
|
473
|
+
end
|
|
462
474
|
end
|
|
463
475
|
end
|
|
464
476
|
end
|
|
@@ -35,9 +35,7 @@ module Clacky
|
|
|
35
35
|
def clean(raw)
|
|
36
36
|
return "" if raw.nil? || raw.empty?
|
|
37
37
|
|
|
38
|
-
s = raw
|
|
39
|
-
s.force_encoding(Encoding::UTF_8)
|
|
40
|
-
s = s.scrub("?") unless s.valid_encoding?
|
|
38
|
+
s = Clacky::Utils::Encoding.pty_to_utf8(raw)
|
|
41
39
|
|
|
42
40
|
s = s.gsub(CSI_REGEX, "")
|
|
43
41
|
s = s.gsub(OSC_REGEX, "")
|
|
@@ -340,10 +340,6 @@ module Clacky
|
|
|
340
340
|
project_root: cwd || Dir.pwd
|
|
341
341
|
)
|
|
342
342
|
|
|
343
|
-
# PowerShell 5 on Chinese Windows emits CP936/GBK by default; force
|
|
344
|
-
# UTF-8 so our PTY (which decodes as UTF-8) doesn't see ??? bytes.
|
|
345
|
-
safe_command = force_powershell_utf8(safe_command)
|
|
346
|
-
|
|
347
343
|
# Background / dedicated path — never reuse the persistent shell,
|
|
348
344
|
# because these commands stay running and would occupy the slot.
|
|
349
345
|
if background
|
|
@@ -1522,45 +1518,6 @@ module Clacky
|
|
|
1522
1518
|
return "" if lines.empty?
|
|
1523
1519
|
lines.last(DISPLAY_TAIL_LINES).join("\n")
|
|
1524
1520
|
end
|
|
1525
|
-
|
|
1526
|
-
# PowerShell 5 on Chinese Windows defaults [Console]::OutputEncoding
|
|
1527
|
-
# to CP936/GBK; our PTY decodes as UTF-8 so non-ASCII output becomes
|
|
1528
|
-
# `???`. Inject UTF-8 setup into the user's PowerShell command so the
|
|
1529
|
-
# shell emits UTF-8 bytes regardless of host locale.
|
|
1530
|
-
POWERSHELL_PREAMBLE =
|
|
1531
|
-
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
|
|
1532
|
-
|
|
1533
|
-
# Only rewrites simple `powershell[.exe]` / `pwsh[.exe]` invocations.
|
|
1534
|
-
# Skips -File / -EncodedCommand / commands already handling encoding /
|
|
1535
|
-
# pipelines (anything risky to splice).
|
|
1536
|
-
private def force_powershell_utf8(command)
|
|
1537
|
-
cmd = command.to_s
|
|
1538
|
-
return command unless cmd =~ /\A\s*(?:powershell(?:\.exe)?|pwsh(?:\.exe)?)\b/i
|
|
1539
|
-
return command if cmd =~ /OutputEncoding/i
|
|
1540
|
-
return command if cmd =~ /\s-(?:File|EncodedCommand|enc|f)\b/i
|
|
1541
|
-
|
|
1542
|
-
# `-Command "..."` form: pipeline / chain characters inside the
|
|
1543
|
-
# quoted body are PowerShell-internal, not shell-level, so we splice
|
|
1544
|
-
# safely into the quoted string.
|
|
1545
|
-
if (m = cmd.match(/\A(\s*(?:powershell(?:\.exe)?|pwsh(?:\.exe)?)\s+(?:[^"'\s]+\s+)*?-(?:Command|c)\s+)(["'])(.*)\2(\s*(?:<\s*\S+\s*)?)\z/i))
|
|
1546
|
-
head, quote, body, tail = m[1], m[2], m[3], m[4].to_s
|
|
1547
|
-
return "#{head}#{quote}#{POWERSHELL_PREAMBLE}#{body}#{quote}#{tail}"
|
|
1548
|
-
end
|
|
1549
|
-
|
|
1550
|
-
# Outside the quoted-Command form, refuse to splice if there's any
|
|
1551
|
-
# shell-level pipe / chain — too risky to get the boundaries right.
|
|
1552
|
-
return command if cmd =~ /[|&;]/
|
|
1553
|
-
|
|
1554
|
-
if (m = cmd.match(/\A(\s*(?:powershell(?:\.exe)?|pwsh(?:\.exe)?))(.*)\z/i))
|
|
1555
|
-
exe, rest = m[1], m[2].to_s.strip
|
|
1556
|
-
return command if rest.start_with?("-") && rest !~ /\A-(?:Command|c)\b/i
|
|
1557
|
-
rest = rest.sub(/\A-(?:Command|c)\b\s*/i, "")
|
|
1558
|
-
inner = rest.empty? ? POWERSHELL_PREAMBLE.chomp(";") : "#{POWERSHELL_PREAMBLE}#{rest}"
|
|
1559
|
-
return %Q{#{exe} -Command "#{inner}"}
|
|
1560
|
-
end
|
|
1561
|
-
|
|
1562
|
-
command
|
|
1563
|
-
end
|
|
1564
1521
|
end
|
|
1565
1522
|
end
|
|
1566
1523
|
end
|