openclacky 1.5.21 → 1.5.22
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 +61 -0
- data/lib/clacky/billing/platform_billing.rb +3 -0
- data/lib/clacky/default_extensions/git/panels/git/view.js +1911 -398
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +199 -34
- data/lib/clacky/providers.rb +43 -17
- data/lib/clacky/search_config.rb +33 -11
- data/lib/clacky/server/git_panel.rb +360 -34
- data/lib/clacky/server/http_server.rb +145 -47
- data/lib/clacky/server/preview.rb +118 -17
- data/lib/clacky/server/scheduler.rb +81 -2
- data/lib/clacky/server/web_ui_controller.rb +35 -27
- data/lib/clacky/session_manager.rb +52 -5
- data/lib/clacky/utils/mac_app_detector.rb +8 -7
- data/lib/clacky/utils/model_pricing.rb +60 -2
- data/lib/clacky/utils/windows_app_detector.rb +3 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +650 -96
- data/lib/clacky/web/app.js +59 -6
- data/lib/clacky/web/auth.js +1 -1
- data/lib/clacky/web/components/composer.js +107 -8
- data/lib/clacky/web/components/image-zoom.js +165 -0
- data/lib/clacky/web/components/model-picker.js +6 -4
- data/lib/clacky/web/components/notify.js +2 -2
- data/lib/clacky/web/core/aside.js +60 -3
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/channels/view.js +5 -2
- data/lib/clacky/web/features/extensions/view.js +2 -2
- data/lib/clacky/web/features/new-session/store.js +79 -2
- data/lib/clacky/web/features/new-session/view.js +496 -58
- data/lib/clacky/web/features/skills/store.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +162 -51
- data/lib/clacky/web/features/workspace/store.js +13 -6
- data/lib/clacky/web/features/workspace/view.js +153 -163
- data/lib/clacky/web/i18n.js +238 -56
- data/lib/clacky/web/index.html +173 -157
- data/lib/clacky/web/projects.js +37 -4
- data/lib/clacky/web/sessions.js +181 -105
- data/lib/clacky/web/settings.js +26 -8
- data/lib/clacky/web/skills.js +2 -2
- data/lib/clacky/web/theme.js +17 -2
- data/lib/clacky/web/utils.js +17 -2
- data/lib/clacky/web/weixin-qr.html +2 -1
- data/lib/clacky/web/ws-dispatcher.js +8 -29
- metadata +2 -1
|
@@ -20,7 +20,7 @@ module Clacky
|
|
|
20
20
|
@broadcaster.call(@session_id, { type: "input_queue", session_id: @session_id, entries: entries })
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
attr_reader :session_id
|
|
23
|
+
attr_reader :session_id, :last_complete
|
|
24
24
|
|
|
25
25
|
def initialize(session_id, broadcaster)
|
|
26
26
|
@session_id = session_id
|
|
@@ -70,23 +70,24 @@ module Clacky
|
|
|
70
70
|
|
|
71
71
|
# Use the same contract as the CLI. The server owns the deadline;
|
|
72
72
|
# typing cancels automatic continuation but keeps the answer wait alive.
|
|
73
|
+
# Leaving auto mode ends the countdown like an intervention, so the task
|
|
74
|
+
# finishes as awaiting_feedback instead of blocking while marked running.
|
|
73
75
|
def request_feedback_with_countdown(seconds: 20)
|
|
74
76
|
pending = { id: @feedback_request_id || SecureRandom.uuid, cond: ConditionVariable.new,
|
|
75
77
|
deadline_ms: (Time.now.to_f * 1000).to_i + seconds * 1000 }
|
|
76
|
-
pending[:seconds] = seconds
|
|
77
78
|
pending[:deadline] = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
78
79
|
@mutex.synchronize do
|
|
79
|
-
|
|
80
|
+
return :intervened if @feedback_permission_mode == :confirm_all
|
|
80
81
|
@pending_feedback = pending
|
|
81
82
|
end
|
|
82
|
-
emit("feedback_countdown", request_id: pending[:id], deadline_ms: pending[:
|
|
83
|
+
emit("feedback_countdown", request_id: pending[:id], deadline_ms: pending[:deadline_ms])
|
|
83
84
|
@mutex.synchronize do
|
|
84
85
|
until pending[:result]
|
|
85
86
|
remaining = pending[:deadline] - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
86
|
-
if
|
|
87
|
+
if remaining <= 0
|
|
87
88
|
pending[:result] = :timeout
|
|
88
89
|
else
|
|
89
|
-
pending[:cond].wait(@mutex,
|
|
90
|
+
pending[:cond].wait(@mutex, remaining)
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
pending[:result]
|
|
@@ -105,21 +106,14 @@ module Clacky
|
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
def update_permission_mode(mode)
|
|
108
|
-
|
|
109
|
+
@mutex.synchronize do
|
|
109
110
|
@feedback_permission_mode = mode.to_sym
|
|
110
111
|
pending = @pending_feedback
|
|
111
|
-
if pending && !pending[:result]
|
|
112
|
-
|
|
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
|
|
112
|
+
if pending && !pending[:result] && mode.to_sym != :auto_approve
|
|
113
|
+
pending[:result] = :intervened
|
|
117
114
|
pending[:cond].signal
|
|
118
|
-
{ request_id: pending[:id],
|
|
119
|
-
deadline_ms: pending[:manual] ? nil : pending[:deadline_ms] }
|
|
120
115
|
end
|
|
121
116
|
end
|
|
122
|
-
emit("feedback_countdown", **countdown) if countdown
|
|
123
117
|
end
|
|
124
118
|
|
|
125
119
|
def intervene_feedback(request_id)
|
|
@@ -142,24 +136,37 @@ module Clacky
|
|
|
142
136
|
data[:skill_command] = skill_command if skill_command
|
|
143
137
|
data[:skill_command_display] = skill_command_display if skill_command_display
|
|
144
138
|
data[:references] = Array(references) unless Array(references).empty?
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
|
|
139
|
+
# Images keep using the lightbox renderer. Other attachments carry their
|
|
140
|
+
# path separately so the browser can open them through the Files viewer.
|
|
141
|
+
# A missing path is deliberately omitted: the card remains visible but
|
|
142
|
+
# is no longer presented as an actionable control.
|
|
143
|
+
rendered_images = []
|
|
144
|
+
rendered_attachments = []
|
|
145
|
+
Array(files).each do |f|
|
|
146
|
+
next if f[:reference] || f["reference"]
|
|
147
|
+
|
|
150
148
|
url = f[:data_url] || f["data_url"]
|
|
151
149
|
name = f[:name] || f["name"]
|
|
152
150
|
type = f[:type] || f["type"]
|
|
153
151
|
path = f[:path] || f["path"]
|
|
152
|
+
mime = f[:mime_type] || f["mime_type"]
|
|
153
|
+
image = type.to_s == "image" || mime.to_s.start_with?("image/")
|
|
154
|
+
|
|
154
155
|
if url
|
|
155
|
-
url
|
|
156
|
-
elsif
|
|
157
|
-
"/api/local-image?path=#{CGI.escape(path.to_s)}&v=#{File.mtime(path.to_s).to_i}"
|
|
156
|
+
rendered_images << url
|
|
157
|
+
elsif image && path && File.exist?(path.to_s)
|
|
158
|
+
rendered_images << "/api/local-image?path=#{CGI.escape(path.to_s)}&v=#{File.mtime(path.to_s).to_i}"
|
|
159
|
+
elsif image && name
|
|
160
|
+
rendered_images << "expired:#{name}"
|
|
158
161
|
elsif name
|
|
159
|
-
"
|
|
162
|
+
attachment_type = type.to_s.empty? ? (mime || "file") : type
|
|
163
|
+
attachment = { name: name, type: attachment_type }
|
|
164
|
+
attachment[:path] = path if path && File.exist?(path.to_s)
|
|
165
|
+
rendered_attachments << attachment
|
|
160
166
|
end
|
|
161
167
|
end
|
|
162
|
-
data[:images] =
|
|
168
|
+
data[:images] = rendered_images unless rendered_images.empty?
|
|
169
|
+
data[:attachments] = rendered_attachments unless rendered_attachments.empty?
|
|
163
170
|
emit("history_user_message", **data)
|
|
164
171
|
# Only forward to channel subscribers when the message originated from the WebUI,
|
|
165
172
|
# to avoid echoing channel messages back to the same channel.
|
|
@@ -270,6 +277,7 @@ module Clacky
|
|
|
270
277
|
data[:cache_stats] = cache_stats if cache_stats
|
|
271
278
|
data[:awaiting_user_feedback] = awaiting_user_feedback if awaiting_user_feedback
|
|
272
279
|
data[:cost_source] = cost_source.to_s if cost_source
|
|
280
|
+
@last_complete = data
|
|
273
281
|
emit("complete", **data)
|
|
274
282
|
forward_to_subscribers do |sub|
|
|
275
283
|
sub.show_complete(iterations: iterations, cost: cost, duration: duration,
|
|
@@ -405,7 +413,7 @@ module Clacky
|
|
|
405
413
|
def replay_live_state
|
|
406
414
|
countdown = @mutex.synchronize do
|
|
407
415
|
pending = @pending_feedback
|
|
408
|
-
pending ? { request_id: pending[:id], deadline_ms: pending[:
|
|
416
|
+
pending ? { request_id: pending[:id], deadline_ms: pending[:deadline_ms] } : { resolved: true }
|
|
409
417
|
end
|
|
410
418
|
emit("feedback_countdown", **countdown)
|
|
411
419
|
return unless @live_progress_state
|
|
@@ -20,8 +20,16 @@ module Clacky
|
|
|
20
20
|
SecureRandom.hex(8)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# How long a parsed session list is shared between callers, in seconds.
|
|
24
|
+
# Only meant to cover the burst of requests the first paint fires; see
|
|
25
|
+
# #load_all_sessions.
|
|
26
|
+
LIST_REUSE_WINDOW = 2.0
|
|
27
|
+
|
|
23
28
|
def initialize(sessions_dir: nil)
|
|
24
29
|
@sessions_dir = sessions_dir || SESSIONS_DIR
|
|
30
|
+
@recent_list = nil
|
|
31
|
+
@recent_list_at = 0.0
|
|
32
|
+
@list_mutex = Mutex.new
|
|
25
33
|
ensure_sessions_dir
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -34,6 +42,7 @@ module Clacky
|
|
|
34
42
|
FileUtils.chmod(0o600, filepath)
|
|
35
43
|
|
|
36
44
|
@last_saved_path = filepath
|
|
45
|
+
@recent_list_at = 0.0
|
|
37
46
|
|
|
38
47
|
run_cleanup_async
|
|
39
48
|
|
|
@@ -213,10 +222,12 @@ module Clacky
|
|
|
213
222
|
# Optional filters:
|
|
214
223
|
# current_dir: (String) if given, sessions matching working_dir come first
|
|
215
224
|
# limit: (Integer) max number of sessions to return
|
|
225
|
+
#
|
|
226
|
+
# Parsing every session file costs ~160ms for a few hundred sessions, and the
|
|
227
|
+
# first paint asks for this list from several endpoints at once, so the
|
|
228
|
+
# concurrent requests share a single scan (see #load_all_sessions).
|
|
216
229
|
def all_sessions(current_dir: nil, limit: nil)
|
|
217
|
-
sessions =
|
|
218
|
-
load_session_file(filepath)
|
|
219
|
-
end.sort_by { |s| [s[:updated_at] || s[:created_at] || "", s[:session_id].to_s] }.reverse
|
|
230
|
+
sessions = load_all_sessions
|
|
220
231
|
|
|
221
232
|
if current_dir
|
|
222
233
|
current_sessions = sessions.select { |s| s[:working_dir] == current_dir }
|
|
@@ -227,6 +238,36 @@ module Clacky
|
|
|
227
238
|
limit ? sessions.first(limit) : sessions
|
|
228
239
|
end
|
|
229
240
|
|
|
241
|
+
# Reuse a scan for a short window: the first paint fires several list
|
|
242
|
+
# requests at once and, without sharing, each one re-parses every session
|
|
243
|
+
# file and they queue up on the GVL. A snapshot older than the window is
|
|
244
|
+
# dropped before the next scan, so this does not grow into a long-lived cache.
|
|
245
|
+
private def load_all_sessions
|
|
246
|
+
if @recent_list && monotonic_now - @recent_list_at < LIST_REUSE_WINDOW
|
|
247
|
+
return @recent_list.dup
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
@list_mutex.synchronize do
|
|
251
|
+
now = monotonic_now
|
|
252
|
+
return @recent_list.dup if @recent_list && now - @recent_list_at < LIST_REUSE_WINDOW
|
|
253
|
+
|
|
254
|
+
# Release the old snapshot before scanning so its parsed messages can be
|
|
255
|
+
# collected instead of being held alongside the new ones.
|
|
256
|
+
@recent_list = nil
|
|
257
|
+
@recent_list = Dir.glob(File.join(@sessions_dir, "*.json"))
|
|
258
|
+
.filter_map { |filepath| load_session_file(filepath) }
|
|
259
|
+
.sort_by { |s| [s[:updated_at] || s[:created_at] || "", s[:session_id].to_s] }
|
|
260
|
+
.reverse
|
|
261
|
+
@recent_list_at = now
|
|
262
|
+
# Callers get their own array so a sort!/push cannot reorder the shared one.
|
|
263
|
+
@recent_list.dup
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
private def monotonic_now
|
|
268
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
269
|
+
end
|
|
270
|
+
|
|
230
271
|
# Full-text grep over session JSON + chunk MD files.
|
|
231
272
|
# Case-sensitive: BSD grep -i is ~30x slower; Chinese has no case.
|
|
232
273
|
# Returns Hash<short_id String => snippet String> (snippet around the first match).
|
|
@@ -415,13 +456,19 @@ module Clacky
|
|
|
415
456
|
# Soft-delete: stamp deleted_at, move JSON + chunks to sessions-trash/.
|
|
416
457
|
def soft_delete(session_id)
|
|
417
458
|
require_relative "tools/trash_manager"
|
|
418
|
-
Clacky::Tools::TrashManager.soft_delete_session(session_id, sessions_dir: @sessions_dir)
|
|
459
|
+
deleted = Clacky::Tools::TrashManager.soft_delete_session(session_id, sessions_dir: @sessions_dir)
|
|
460
|
+
# The file is moved outside this object's save path, so drop the shared
|
|
461
|
+
# list explicitly instead of waiting for the reuse window to lapse.
|
|
462
|
+
@recent_list_at = 0.0 if deleted
|
|
463
|
+
deleted
|
|
419
464
|
end
|
|
420
465
|
|
|
421
466
|
# Restore a soft-deleted session back to the active sessions directory.
|
|
422
467
|
def restore_session(session_id)
|
|
423
468
|
require_relative "tools/trash_manager"
|
|
424
|
-
Clacky::Tools::TrashManager.restore_session(session_id, sessions_dir: @sessions_dir)
|
|
469
|
+
restored = Clacky::Tools::TrashManager.restore_session(session_id, sessions_dir: @sessions_dir)
|
|
470
|
+
@recent_list_at = 0.0 if restored
|
|
471
|
+
restored
|
|
425
472
|
end
|
|
426
473
|
|
|
427
474
|
# List all soft-deleted sessions (newest-first).
|
|
@@ -59,16 +59,17 @@ module Clacky
|
|
|
59
59
|
return nil unless Utils::EnvironmentDetector.os_type == :macos
|
|
60
60
|
|
|
61
61
|
ext = File.extname(path).downcase.sub(/\A\./, "")
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
# LaunchServices answers for any URL, extension or not, so a Dockerfile
|
|
63
|
+
# resolves like any other file. Only extension answers are cacheable.
|
|
64
64
|
@default_app_cache ||= {}
|
|
65
|
-
return @default_app_cache[ext] if @default_app_cache.key?(ext)
|
|
65
|
+
return @default_app_cache[ext] if !ext.empty? && @default_app_cache.key?(ext)
|
|
66
66
|
|
|
67
67
|
bundle = default_app_bundle(path)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
app = if bundle && File.directory?(bundle)
|
|
69
|
+
{ "name" => File.basename(bundle, ".app"), "path" => bundle }
|
|
70
|
+
end
|
|
71
|
+
@default_app_cache[ext] = app unless ext.empty?
|
|
72
|
+
app
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# Query LaunchServices for the default handler of `path` via NSWorkspace.
|
|
@@ -389,6 +389,38 @@ module Clacky
|
|
|
389
389
|
}
|
|
390
390
|
},
|
|
391
391
|
|
|
392
|
+
# Kimi K3.1 Pro (1M context, native vision, tool calling).
|
|
393
|
+
# Source: https://platform.kimi.ai (USD / 1M tokens)
|
|
394
|
+
"kimi-k3.1-pro" => {
|
|
395
|
+
input: {
|
|
396
|
+
default: 2.00, # $2.00/MTok cache miss
|
|
397
|
+
over_200k: 2.00
|
|
398
|
+
},
|
|
399
|
+
output: {
|
|
400
|
+
default: 10.00, # $10.00/MTok
|
|
401
|
+
over_200k: 10.00
|
|
402
|
+
},
|
|
403
|
+
cache: {
|
|
404
|
+
write: 2.00, # bill at miss rate (= 5m write price)
|
|
405
|
+
read: 0.20 # $0.20/MTok cache hit
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
"kimi-k3.1-pro-highspeed" => {
|
|
410
|
+
input: {
|
|
411
|
+
default: 4.00, # $4.00/MTok cache miss
|
|
412
|
+
over_200k: 4.00
|
|
413
|
+
},
|
|
414
|
+
output: {
|
|
415
|
+
default: 20.00, # $20.00/MTok
|
|
416
|
+
over_200k: 20.00
|
|
417
|
+
},
|
|
418
|
+
cache: {
|
|
419
|
+
write: 4.00, # bill at miss rate (= 5m write price)
|
|
420
|
+
read: 0.20 # $0.20/MTok cache hit
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
|
|
392
424
|
# Kimi K3 flagship model (1M context, native vision, tool calling).
|
|
393
425
|
# Source: https://platform.moonshot.ai (USD / 1M tokens)
|
|
394
426
|
"kimi-k3" => {
|
|
@@ -597,6 +629,27 @@ module Clacky
|
|
|
597
629
|
output: { default: 2.22, over_200k: 2.22 },
|
|
598
630
|
cache: { write: 0.45, read: 0.09 }
|
|
599
631
|
},
|
|
632
|
+
"doubao-seed-2.1-lite" => {
|
|
633
|
+
input: { default: 0.12, over_200k: 0.12 },
|
|
634
|
+
output: { default: 0.40, over_200k: 0.40 },
|
|
635
|
+
cache: { write: 0.12, read: 0.03 }
|
|
636
|
+
},
|
|
637
|
+
# Ark's peak window (Mon-Fri 09-12/14-18 Beijing) equals DeepSeek's, so
|
|
638
|
+
# the economy tier reuses the deepseek peak/off-peak resolution. Cache
|
|
639
|
+
# reads keep three decimals: rounding up to the cent would double them.
|
|
640
|
+
"deepseek-v4.1-flash-economy" => {
|
|
641
|
+
deepseek: true,
|
|
642
|
+
peak: {
|
|
643
|
+
input: { default: 0.24, over_200k: 0.24 },
|
|
644
|
+
output: { default: 0.95, over_200k: 0.95 },
|
|
645
|
+
cache: { write: 0.24, read: 0.005 }
|
|
646
|
+
},
|
|
647
|
+
off_peak: {
|
|
648
|
+
input: { default: 0.12, over_200k: 0.12 },
|
|
649
|
+
output: { default: 0.48, over_200k: 0.48 },
|
|
650
|
+
cache: { write: 0.12, read: 0.003 }
|
|
651
|
+
}
|
|
652
|
+
},
|
|
600
653
|
"doubao-seed-2.0-lite" => {
|
|
601
654
|
input: { default: 0.14, over_200k: 0.27 },
|
|
602
655
|
output: { default: 0.80, over_200k: 1.60 },
|
|
@@ -1214,7 +1267,7 @@ module Clacky
|
|
|
1214
1267
|
# served through a third party.
|
|
1215
1268
|
# The gateway mirrors this list - see seriesDiscount in llm_proxy.
|
|
1216
1269
|
DISCOUNT_RULES = [
|
|
1217
|
-
{ match: /\Aabs-claude-/, rate: 0.
|
|
1270
|
+
{ match: /\Aabs-claude-/, rate: 0.45, reason: "channel" }, # Claude via ciyuan cc-max - 55% off
|
|
1218
1271
|
{ match: /\Aoc-glm-/, rate: 0.95, reason: "channel" }, # GLM via ciyuan glm - 5% off
|
|
1219
1272
|
{ match: /\Aoc-kimi-/, rate: 0.9, reason: "channel" }, # Kimi K3 via ciyuan - 10% off
|
|
1220
1273
|
{ match: /\Aoc-grok-/, rate: 0.3, reason: "channel" }, # Grok 4.7 via ciyuan grok - 70% off
|
|
@@ -1429,6 +1482,11 @@ module Clacky
|
|
|
1429
1482
|
"claude-3-5-haiku-20241022"
|
|
1430
1483
|
when /deepseek-v4-pro/i, /deepseek.*v4.*pro/i
|
|
1431
1484
|
"deepseek-v4-pro"
|
|
1485
|
+
# V4.1 Flash — canonical generation id; natively multimodal. Must be
|
|
1486
|
+
# matched BEFORE the v4-flash rules below, whose `.*v4.*flash` tails
|
|
1487
|
+
# would otherwise catch "v4.1-flash".
|
|
1488
|
+
when /deepseek-v4[.-]1-flash/i
|
|
1489
|
+
"deepseek-flash"
|
|
1432
1490
|
# Vision variant must be matched BEFORE the v4-flash rule below —
|
|
1433
1491
|
# "deepseek-v4-flash-vision-exp" would otherwise be substring-matched
|
|
1434
1492
|
# to v4-flash and billed at the wrong model's rate.
|
|
@@ -1436,7 +1494,7 @@ module Clacky
|
|
|
1436
1494
|
"deepseek-v4-flash-vision-exp"
|
|
1437
1495
|
when /deepseek-v4-flash/i, /deepseek.*v4.*flash/i
|
|
1438
1496
|
"deepseek-v4-flash"
|
|
1439
|
-
#
|
|
1497
|
+
# Bare upstream id for V4.1 Flash.
|
|
1440
1498
|
when /deepseek-flash/i
|
|
1441
1499
|
"deepseek-flash"
|
|
1442
1500
|
# Legacy aliases: deepseek-chat and deepseek-reasoner are being
|
|
@@ -219,7 +219,9 @@ module Clacky
|
|
|
219
219
|
return nil unless wsl?
|
|
220
220
|
|
|
221
221
|
ext = normalize_ext(File.extname(path))
|
|
222
|
-
|
|
222
|
+
# Windows has no extension to look up for a Dockerfile or Makefile;
|
|
223
|
+
# ask what handles plain text instead of reporting nothing.
|
|
224
|
+
ext = "txt" if ext.empty?
|
|
223
225
|
|
|
224
226
|
data = detect_for_ext(ext)
|
|
225
227
|
data ? data["default"] : nil
|
data/lib/clacky/version.rb
CHANGED