openclacky 1.3.11 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/lib/clacky/agent/session_serializer.rb +11 -4
  4. data/lib/clacky/agent/skill_manager.rb +1 -0
  5. data/lib/clacky/agent.rb +4 -5
  6. data/lib/clacky/brand_config.rb +53 -3
  7. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +202 -3
  8. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +610 -60
  9. data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
  10. data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
  11. data/lib/clacky/extension/packager.rb +21 -1
  12. data/lib/clacky/extension/verifier.rb +1 -1
  13. data/lib/clacky/media/generator.rb +41 -0
  14. data/lib/clacky/media/volcengine.rb +394 -0
  15. data/lib/clacky/platform_http_client.rb +9 -7
  16. data/lib/clacky/providers.rb +4 -4
  17. data/lib/clacky/server/http_server.rb +67 -22
  18. data/lib/clacky/tools/grep.rb +6 -1
  19. data/lib/clacky/tools/terminal.rb +24 -8
  20. data/lib/clacky/ui2/layout_manager.rb +4 -0
  21. data/lib/clacky/ui2/screen_buffer.rb +31 -3
  22. data/lib/clacky/utils/environment_detector.rb +16 -9
  23. data/lib/clacky/utils/file_processor.rb +30 -28
  24. data/lib/clacky/utils/model_pricing.rb +49 -0
  25. data/lib/clacky/version.rb +1 -1
  26. data/lib/clacky/web/app.css +229 -31
  27. data/lib/clacky/web/components/onboard.js +14 -4
  28. data/lib/clacky/web/features/backup/view.js +4 -3
  29. data/lib/clacky/web/features/extensions/store.js +2 -2
  30. data/lib/clacky/web/features/extensions/view.js +37 -7
  31. data/lib/clacky/web/features/mcp/view.js +5 -2
  32. data/lib/clacky/web/features/skills/view.js +61 -1
  33. data/lib/clacky/web/features/workspace/store.js +11 -0
  34. data/lib/clacky/web/features/workspace/view.js +10 -4
  35. data/lib/clacky/web/i18n.js +14 -2
  36. data/lib/clacky/web/index.html +29 -13
  37. data/lib/clacky/web/sessions.js +45 -8
  38. data/scripts/uninstall.sh +1 -1
  39. metadata +2 -1
@@ -607,9 +607,22 @@ module Clacky
607
607
  ""
608
608
  )
609
609
 
610
- # Pass 1: anchored strip — the full wrapper echoed at the start,
611
- # possibly spanning multiple real newlines.
612
- text = text.sub(/\A\{.*?"\$__clacky_ec"\s*\n?/m, "")
610
+ # Pass 1: anchored strip — remove ONLY the wrapper's echoed head
611
+ # line (`{ USER_CMD`) at the very start of the buffer, and ONLY
612
+ # when a matching wrapper tail fingerprint (`}...; __clacky_ec=$?`)
613
+ # actually exists somewhere in the buffer. The guard is what keeps
614
+ # us from eating a real first line of user output that merely
615
+ # happens to start with `{` (e.g. JSON like `{"key": ...}`).
616
+ # We test the tail with a separate anchor-free `match?` (linear, no
617
+ # backtracking) rather than a lookahead, to avoid catastrophic
618
+ # regex backtracking on large `{`-prefixed output. We strip a
619
+ # single line (`[^\n]*`, not cross-line `.*?`): a multi-line command
620
+ # echoed in cooked mode puts the wrapper tail *after* the real
621
+ # output, so cross-line greed would swallow that output whole. The
622
+ # tail itself is removed by the fingerprint passes below.
623
+ if text.start_with?("{") && text.match?(/\}[^\n;]*; *__clacky_ec=\$\?/)
624
+ text = text.sub(/\A\{[^\n]*\n/, "")
625
+ end
613
626
 
614
627
  # Pass 2: token-aware global strip — remove any leftover wrapper
615
628
  # echo fragment, wherever it sits. Requires the session token so
@@ -629,9 +642,11 @@ module Clacky
629
642
  # 2b. Single-line shape: everything collapsed onto one line.
630
643
  # Strip from the wrapper's `}; __clacky_ec=$?` pivot (or the
631
644
  # opening `{` if still present on that line) through the end of
632
- # the printf invocation (`"$__clacky_ec"`).
645
+ # the printf invocation (`"$__clacky_ec"`). The optional
646
+ # `[^\n;]*` between `}` and `;` absorbs a WSL `</dev/null`
647
+ # stdin redirect spliced onto the group.
633
648
  text = text.gsub(
634
- /[^\n]*\}; *__clacky_ec=\$\?; *printf[^\n]*__CLACKY_DONE_#{token_re}_[^\n]*"\$__clacky_ec"[^\n]*\n?/,
649
+ /[^\n]*\}[^\n;]*; *__clacky_ec=\$\?; *printf[^\n]*__CLACKY_DONE_#{token_re}_[^\n]*"\$__clacky_ec"[^\n]*\n?/,
635
650
  ""
636
651
  )
637
652
 
@@ -653,9 +668,10 @@ module Clacky
653
668
  # private double-underscore var name that user code effectively
654
669
  # never emits — so we strip anything between them (non-greedy,
655
670
  # multiline-aware) to also handle width-wrap that inserted
656
- # real \n breaks inside the echo.
671
+ # real \n breaks inside the echo. `[^\n;]*` between `}` and `;`
672
+ # absorbs a WSL `</dev/null` stdin redirect on the group.
657
673
  text = text.gsub(
658
- /[^\n]*\}; *__clacky_ec=\$\?.*?"\$__clacky_ec"[^\n]*\n?/m,
674
+ /[^\n]*\}[^\n;]*; *__clacky_ec=\$\?.*?"\$__clacky_ec"[^\n]*\n?/m,
659
675
  ""
660
676
  )
661
677
 
@@ -663,7 +679,7 @@ module Clacky
663
679
  # truncation cut off everything after `__clacky_ec=$?`). Still a
664
680
  # reliable fingerprint thanks to the `__clacky_ec` var name.
665
681
  text = text.gsub(
666
- /[^\n]*\}; *__clacky_ec=\$\?;?[^\n]*\n?/,
682
+ /[^\n]*\}[^\n;]*; *__clacky_ec=\$\?;?[^\n]*\n?/,
667
683
  ""
668
684
  )
669
685
 
@@ -402,6 +402,10 @@ module Clacky
402
402
  screen.show_cursor
403
403
  screen.flush
404
404
  end
405
+ # Restore cooked mode so the terminal is usable after exit, regardless
406
+ # of which exit path was taken (some call exit(0) and bypass the
407
+ # input_loop ensure). Idempotent; safe to call more than once.
408
+ screen.disable_raw_mode
405
409
  end
406
410
 
407
411
  # /clear: wipe output area + buffer, keep fixed area.
@@ -118,14 +118,42 @@ module Clacky
118
118
  @height = TTY::Screen.height
119
119
  end
120
120
 
121
- # Enable raw mode (disable line buffering)
121
+ # Enable raw mode (disable line buffering). The very first time it runs it
122
+ # snapshots the terminal's original settings (via `stty -g`) so the exact
123
+ # pre-clacky state can be restored on exit — `IO#cooked!` only applies a
124
+ # generic cooked profile and does not restore flow-control (IXON) and other
125
+ # bits, which on macOS leaves the shell in a broken input state.
122
126
  def enable_raw_mode
127
+ return unless $stdin.tty?
128
+
129
+ @original_stty ||= capture_stty
123
130
  $stdin.raw!
124
131
  end
125
132
 
126
- # Disable raw mode
133
+ # Disable raw mode. Idempotent and tty-guarded so it can be safely called
134
+ # from any exit path. Restores the exact original terminal settings when a
135
+ # snapshot exists, then flushes pending input so stray bytes don't leak to
136
+ # the shell. Falls back to `IO#cooked!` when no snapshot is available.
127
137
  def disable_raw_mode
128
- $stdin.cooked!
138
+ return unless $stdin.tty?
139
+
140
+ $stdin.cooked! unless @original_stty && restore_stty(@original_stty)
141
+ $stdin.ioflush
142
+ rescue IOError, Errno::ENOTTY
143
+ nil
144
+ end
145
+
146
+ private def capture_stty
147
+ out = Clacky::Utils::Encoding.cmd_to_utf8(`stty -g 2>/dev/null`).strip
148
+ out.empty? ? nil : out
149
+ rescue StandardError
150
+ nil
151
+ end
152
+
153
+ private def restore_stty(state)
154
+ system("stty", state, out: File::NULL, err: File::NULL)
155
+ rescue StandardError
156
+ false
129
157
  end
130
158
 
131
159
  # Read a single character without echo
@@ -52,17 +52,24 @@ module Clacky
52
52
  end
53
53
  end
54
54
 
55
- # Convert a Windows-style path to a WSL/Linux-side path.
56
- # e.g. "C:/Users/foo/file.txt" "/mnt/c/Users/foo/file.txt"
57
- # Returns the original path unchanged on non-WSL or if already a Linux path.
58
- # @param path [String]
59
- # @return [String]
55
+ # Convert a Windows-style path (incl. the "/C:/" three-slash-URL form) to
56
+ # a WSL /mnt path. No-op on non-WSL or non-drive-letter paths.
60
57
  def self.win_to_linux_path(path)
61
- return path unless os_type == :wsl && path.match?(/\A[A-Za-z]:[\/\\]/)
58
+ return path unless os_type == :wsl
59
+
60
+ m = path.match(%r{\A/?([A-Za-z]):[/\\](.*)}m)
61
+ return path unless m
62
+
63
+ "/mnt/#{m[1].downcase}/#{m[2].gsub("\\", "/")}"
64
+ end
62
65
 
63
- drive = path[0].downcase
64
- rest = path[2..].gsub("\\", "/")
65
- "/mnt/#{drive}#{rest}"
66
+ # Resolve a "file://" URL or bare path into a real local path:
67
+ # strip "file://", percent-decode, WSL drive-letter normalize, then expand.
68
+ def self.resolve_local_path(href)
69
+ path = href.to_s.sub(%r{\Afile://}, "")
70
+ path = CGI.unescape(path)
71
+ path = win_to_linux_path(path)
72
+ File.expand_path(path)
66
73
  end
67
74
 
68
75
  # Convert a Linux-side path to a Windows-style path via wslpath.
@@ -6,6 +6,7 @@ require "securerandom"
6
6
  require "stringio"
7
7
 
8
8
  require_relative "parser_manager"
9
+ require_relative "environment_detector"
9
10
  require "zip"
10
11
 
11
12
  module Clacky
@@ -595,71 +596,63 @@ module Clacky
595
596
  return content if content.nil? || content.empty?
596
597
 
597
598
  # Rewrite markdown image syntax ![alt](file:///path) → proxy URL
598
- content = content.gsub(/!\[([^\]]*)\]\(((?:file:\/\/)?\/[^)]+)\)/) do |_match|
599
+ content = content.gsub(/!\[([^\]]*)\]\(((?:file:\/\/)?(?:~\/|\/)[^)]+)\)/) do |_match|
599
600
  alt = Regexp.last_match(1)
600
601
  href = Regexp.last_match(2)
601
602
 
602
- path = href.sub(%r{\Afile://}, "")
603
- path = CGI.unescape(path)
603
+ real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
604
604
 
605
- ext = File.extname(path).downcase
606
- if LOCAL_MEDIA_EXTENSIONS.include?(ext) && File.exist?(path)
607
- encoded = CGI.escape(href)
608
- "![#{alt}](/api/local-image?path=#{encoded})"
605
+ ext = File.extname(real).downcase
606
+ if LOCAL_MEDIA_EXTENSIONS.include?(ext) && File.exist?(real)
607
+ "![#{alt}](#{local_image_proxy_url(href, real)})"
609
608
  else
610
609
  _match
611
610
  end
612
611
  end
613
612
 
614
613
  # Rewrite <video src="file:///path/vid.mp4" ...> → proxy URL
615
- content = content.gsub(/<video\b([^>]*)\bsrc="((?:file:\/\/)?\/[^"]+)"([^>]*)>/) do |_match|
614
+ content = content.gsub(/<video\b([^>]*)\bsrc="((?:file:\/\/)?(?:~\/|\/)[^"]+)"([^>]*)>/) do |_match|
616
615
  pre = Regexp.last_match(1) || ""
617
616
  href = Regexp.last_match(2)
618
617
  post = Regexp.last_match(3) || ""
619
618
 
620
- path = href.sub(%r{\Afile://}, "")
621
- path = CGI.unescape(path)
619
+ real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
622
620
 
623
- ext = File.extname(path).downcase
624
- if LOCAL_VIDEO_EXTENSIONS.include?(ext) && File.exist?(path)
625
- encoded = CGI.escape(href)
626
- "<video#{pre} src=\"/api/local-image?path=#{encoded}\"#{post}>"
621
+ ext = File.extname(real).downcase
622
+ if LOCAL_VIDEO_EXTENSIONS.include?(ext) && File.exist?(real)
623
+ "<video#{pre} src=\"#{local_image_proxy_url(href, real)}\"#{post}>"
627
624
  else
628
625
  _match
629
626
  end
630
627
  end
631
628
 
632
629
  # Rewrite <audio src="file:///path/audio.wav" ...> → proxy URL
633
- content = content.gsub(/<audio\b([^>]*)\bsrc="((?:file:\/\/)?\/[^"]+)"([^>]*)>/) do |_match|
630
+ content = content.gsub(/<audio\b([^>]*)\bsrc="((?:file:\/\/)?(?:~\/|\/)[^"]+)"([^>]*)>/) do |_match|
634
631
  pre = Regexp.last_match(1) || ""
635
632
  href = Regexp.last_match(2)
636
633
  post = Regexp.last_match(3) || ""
637
634
 
638
- path = href.sub(%r{\Afile://}, "")
639
- path = CGI.unescape(path)
635
+ real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
640
636
 
641
- ext = File.extname(path).downcase
642
- if LOCAL_AUDIO_EXTENSIONS.include?(ext) && File.exist?(path)
643
- encoded = CGI.escape(href)
644
- "<audio#{pre} src=\"/api/local-image?path=#{encoded}\"#{post}>"
637
+ ext = File.extname(real).downcase
638
+ if LOCAL_AUDIO_EXTENSIONS.include?(ext) && File.exist?(real)
639
+ "<audio#{pre} src=\"#{local_image_proxy_url(href, real)}\"#{post}>"
645
640
  else
646
641
  _match
647
642
  end
648
643
  end
649
644
 
650
645
  # Rewrite video/audio markdown links [text](file:///path) → proxy URL
651
- content = content.gsub(/(?<!!)\[([^\]]*)\]\(((?:file:\/\/)?\/[^)]+)\)/) do |_match|
646
+ content = content.gsub(/(?<!!)\[([^\]]*)\]\(((?:file:\/\/)?(?:~\/|\/)[^)]+)\)/) do |_match|
652
647
  text = Regexp.last_match(1)
653
648
  href = Regexp.last_match(2)
654
649
 
655
- path = href.sub(%r{\Afile://}, "")
656
- path = CGI.unescape(path)
650
+ real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
657
651
 
658
- ext = File.extname(path).downcase
652
+ ext = File.extname(real).downcase
659
653
  if LOCAL_VIDEO_EXTENSIONS.include?(ext) || LOCAL_AUDIO_EXTENSIONS.include?(ext)
660
- if File.exist?(path)
661
- encoded = CGI.escape(href)
662
- "[#{text}](/api/local-image?path=#{encoded})"
654
+ if File.exist?(real)
655
+ "[#{text}](#{local_image_proxy_url(href, real)})"
663
656
  else
664
657
  _match
665
658
  end
@@ -670,6 +663,15 @@ module Clacky
670
663
 
671
664
  content
672
665
  end
666
+
667
+ # Build a proxy URL for a local media file, appending a version param
668
+ # derived from the file mtime so an overwritten same-name file produces a
669
+ # different URL and defeats the browser's in-memory image cache.
670
+ def self.local_image_proxy_url(href, path)
671
+ encoded = CGI.escape(href)
672
+ version = File.mtime(path).to_i
673
+ "/api/local-image?path=#{encoded}&v=#{version}"
674
+ end
673
675
  end
674
676
  end
675
677
  end
@@ -237,6 +237,55 @@ module Clacky
237
237
  }
238
238
  },
239
239
 
240
+ # Kimi K3 flagship model (1M context, native vision, tool calling).
241
+ # Source: https://platform.moonshot.ai (USD / 1M tokens)
242
+ "kimi-k3" => {
243
+ input: {
244
+ default: 3.00, # $3.00/MTok cache miss
245
+ over_200k: 3.00
246
+ },
247
+ output: {
248
+ default: 15.00, # $15.00/MTok
249
+ over_200k: 15.00
250
+ },
251
+ cache: {
252
+ write: 3.00, # no separate write charge; bill at miss rate
253
+ read: 0.30 # $0.30/MTok cache hit
254
+ }
255
+ },
256
+
257
+ # Kimi K2.7 Code (256K context, multimodal coding model).
258
+ # Source: https://platform.moonshot.ai (USD / 1M tokens)
259
+ "kimi-k2.7-code" => {
260
+ input: {
261
+ default: 0.95, # $0.95/MTok cache miss
262
+ over_200k: 0.95
263
+ },
264
+ output: {
265
+ default: 4.00, # $4.00/MTok
266
+ over_200k: 4.00
267
+ },
268
+ cache: {
269
+ write: 0.95, # no separate write charge; bill at miss rate
270
+ read: 0.19 # $0.19/MTok cache hit
271
+ }
272
+ },
273
+
274
+ "kimi-k2.7-code-highspeed" => {
275
+ input: {
276
+ default: 1.90, # $1.90/MTok cache miss
277
+ over_200k: 1.90
278
+ },
279
+ output: {
280
+ default: 8.00, # $8.00/MTok
281
+ over_200k: 8.00
282
+ },
283
+ cache: {
284
+ write: 1.90, # no separate write charge; bill at miss rate
285
+ read: 0.38 # $0.38/MTok cache hit
286
+ }
287
+ },
288
+
240
289
  # Google Gemini 3 series (via Vertex AI). Tiered at 200K input tokens
241
290
  # for Pro; Flash has flat pricing.
242
291
  "gemini-3.1-pro" => {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.3.11"
4
+ VERSION = "1.4.1"
5
5
  end