openclacky 0.9.35 → 0.9.36
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 +12 -0
- data/lib/clacky/agent/skill_manager.rb +20 -8
- data/lib/clacky/agent.rb +22 -17
- data/lib/clacky/agent_config.rb +166 -40
- data/lib/clacky/cli.rb +32 -13
- data/lib/clacky/server/http_server.rb +141 -59
- data/lib/clacky/tools/terminal.rb +89 -15
- data/lib/clacky/ui2/ui_controller.rb +16 -7
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +75 -149
- data/lib/clacky/web/app.js +23 -17
- data/lib/clacky/web/i18n.js +10 -0
- data/lib/clacky/web/index.html +8 -18
- data/lib/clacky/web/sessions.js +85 -56
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a7f74cba226bc2332676ed9f0a72c27f3fb6b45bafacd60198f7ec9b59a84c7
|
|
4
|
+
data.tar.gz: 7396d2258a38e203a5ffe217f0409fb101487bdb74dad8f224649a09b8adb775
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f10c234d4a22d825e3623bff7e0a0faf55af4cea34dc15a939fbde3e6e3792bf1a11cd97d68e0c6890dd37c011814019cfed7dea70c724fcb2fc11b49cff15a
|
|
7
|
+
data.tar.gz: 12dcd062214b42191f33e4b2e09ec62f24cb9f91206451ff23861672a2cc8b680d89e5498cf0da6c53b74d4da2933bd53863666cee608539edb398f016226df5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.36] - 2026-04-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Session deletion now works correctly**: fixed disk-based session deletion that was failing with proper error handling in the Web UI (C-9d1ea93)
|
|
14
|
+
- **Model switching improved**: better model ID validation and normalization when switching models in Web UI — handles various ID formats correctly (C-b61e22e)
|
|
15
|
+
- **Terminal tool word wrapping**: fixed terminal output word wrapping issues that could break long command outputs (C-5989d02)
|
|
16
|
+
- **Heartbeat mechanism stability**: improved async heartbeat logic in server mode for more reliable connection status tracking (C-5989d02)
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- **UI polish**: removed session topbar clutter and added empty state messages for better first-time user experience (C-003d613)
|
|
20
|
+
- **Cleaner logging**: reduced noisy debug logs in skill manager for quieter operation (C-c27bbec)
|
|
21
|
+
|
|
10
22
|
## [0.9.35] - 2026-04-23
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -68,15 +68,27 @@ module Clacky
|
|
|
68
68
|
all_skills = all_skills.reject(&:invalid?)
|
|
69
69
|
auto_invocable = all_skills.select(&:model_invocation_allowed?)
|
|
70
70
|
|
|
71
|
-
# Enforce system prompt injection limit to control token usage
|
|
71
|
+
# Enforce system prompt injection limit to control token usage.
|
|
72
|
+
# Warn only when the set of dropped skills *changes* — this message
|
|
73
|
+
# is otherwise emitted once per agent turn (build_skill_context is
|
|
74
|
+
# called during every system prompt assembly) and floods the log.
|
|
72
75
|
if auto_invocable.size > MAX_CONTEXT_SKILLS
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
kept = auto_invocable.first(MAX_CONTEXT_SKILLS)
|
|
77
|
+
dropped = auto_invocable.drop(MAX_CONTEXT_SKILLS)
|
|
78
|
+
dropped_names = dropped.map(&:identifier)
|
|
79
|
+
signature = dropped_names.sort.join(",")
|
|
80
|
+
|
|
81
|
+
if @skill_limit_warned_signature != signature
|
|
82
|
+
@skill_limit_warned_signature = signature
|
|
83
|
+
Clacky::Logger.warn(
|
|
84
|
+
"Skill context limit: #{auto_invocable.size} auto-invocable skills found, " \
|
|
85
|
+
"only injecting first #{MAX_CONTEXT_SKILLS} " \
|
|
86
|
+
"(#{dropped.size} dropped — will NOT be auto-discovered by the agent: " \
|
|
87
|
+
"#{dropped_names.join(", ")}). " \
|
|
88
|
+
"Remove unused skills to restore full visibility."
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
auto_invocable = kept
|
|
80
92
|
end
|
|
81
93
|
|
|
82
94
|
return "" if auto_invocable.empty?
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -131,14 +131,21 @@ module Clacky
|
|
|
131
131
|
@hooks.add(event, &block)
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
# Switch to a different model by
|
|
135
|
-
#
|
|
134
|
+
# Switch this session to a different model, identified by its stable
|
|
135
|
+
# runtime id. Ids survive list reorders, additions, and field edits,
|
|
136
|
+
# which is why we no longer expose an index-based API.
|
|
137
|
+
# @param id [String] Model id (see AgentConfig#parse_models)
|
|
136
138
|
# @return [Boolean] true if switched successfully, false otherwise
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
def switch_model_by_id(id)
|
|
140
|
+
return false unless @config.switch_model_by_id(id)
|
|
141
|
+
|
|
142
|
+
rebuild_client_for_current_model!
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Rebuild the underlying Client (and dependent components) to pick up
|
|
147
|
+
# credentials/model name from the currently-selected model in @config.
|
|
148
|
+
private def rebuild_client_for_current_model!
|
|
142
149
|
@client = Clacky::Client.new(
|
|
143
150
|
@config.api_key,
|
|
144
151
|
base_url: @config.base_url,
|
|
@@ -147,11 +154,9 @@ module Clacky
|
|
|
147
154
|
)
|
|
148
155
|
# Update message compressor with new client and model
|
|
149
156
|
@message_compressor = MessageCompressor.new(@client, model: current_model)
|
|
150
|
-
|
|
157
|
+
|
|
151
158
|
# Inject a new session context to notify the AI of the model switch
|
|
152
159
|
inject_session_context
|
|
153
|
-
|
|
154
|
-
true
|
|
155
160
|
end
|
|
156
161
|
|
|
157
162
|
# Change the working directory for this session
|
|
@@ -987,16 +992,16 @@ module Clacky
|
|
|
987
992
|
if model == "lite"
|
|
988
993
|
# Special keyword: use lite model if available, otherwise fall back to default
|
|
989
994
|
lite_model = subagent_config.lite_model
|
|
990
|
-
if lite_model
|
|
991
|
-
|
|
992
|
-
subagent_config.switch_model(model_index) if model_index
|
|
995
|
+
if lite_model && lite_model["id"]
|
|
996
|
+
subagent_config.switch_model_by_id(lite_model["id"])
|
|
993
997
|
end
|
|
994
998
|
# If no lite model, just use current (default) model
|
|
995
999
|
else
|
|
996
|
-
# Regular model name lookup
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
+
# Regular model name lookup — find the first model with a matching
|
|
1001
|
+
# name and switch by its stable id.
|
|
1002
|
+
target = subagent_config.models.find { |m| m["model"] == model }
|
|
1003
|
+
if target && target["id"]
|
|
1004
|
+
subagent_config.switch_model_by_id(target["id"])
|
|
1000
1005
|
else
|
|
1001
1006
|
raise AgentError, "Model '#{model}' not found in config. Available models: #{subagent_config.model_names.join(', ')}"
|
|
1002
1007
|
end
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
|
|
6
7
|
module Clacky
|
|
7
8
|
# ClaudeCode environment variable compatibility layer
|
|
@@ -152,7 +153,7 @@ module Clacky
|
|
|
152
153
|
|
|
153
154
|
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
154
155
|
:enable_compression, :enable_prompt_caching,
|
|
155
|
-
:models, :current_model_index,
|
|
156
|
+
:models, :current_model_index, :current_model_id,
|
|
156
157
|
:memory_update_enabled, :skill_evolution
|
|
157
158
|
|
|
158
159
|
def initialize(options = {})
|
|
@@ -165,7 +166,22 @@ module Clacky
|
|
|
165
166
|
|
|
166
167
|
# Models configuration
|
|
167
168
|
@models = options[:models] || []
|
|
169
|
+
# Ensure every model has a stable runtime id — this is the single
|
|
170
|
+
# invariant the rest of the system relies on. Regardless of how the
|
|
171
|
+
# config was built (load from yml, direct .new in tests, add_model,
|
|
172
|
+
# api_save_config), every model in @models will have an id.
|
|
173
|
+
@models.each { |m| m["id"] ||= SecureRandom.uuid }
|
|
174
|
+
|
|
168
175
|
@current_model_index = options[:current_model_index] || 0
|
|
176
|
+
# Stable runtime id for the currently-selected model. Preferred over
|
|
177
|
+
# @current_model_index because ids are immune to list reordering,
|
|
178
|
+
# additions, and edits to model fields. Ids are injected at load time
|
|
179
|
+
# and never persisted to config.yml (backward compatible with old files).
|
|
180
|
+
# If caller didn't specify current_model_id, prefer the model marked
|
|
181
|
+
# as `type: default` (the documented convention), falling back to
|
|
182
|
+
# models[current_model_index] only if no default marker exists.
|
|
183
|
+
@current_model_id = options[:current_model_id] ||
|
|
184
|
+
(@models.find { |m| m["type"] == "default" } || @models[@current_model_index])&.dig("id")
|
|
169
185
|
|
|
170
186
|
# Memory and skill evolution configuration
|
|
171
187
|
@memory_update_enabled = options[:memory_update_enabled].nil? ? true : options[:memory_update_enabled]
|
|
@@ -238,11 +254,17 @@ module Clacky
|
|
|
238
254
|
# The injected lite model is runtime-only (not persisted to config.yml)
|
|
239
255
|
inject_provider_lite_model(models)
|
|
240
256
|
|
|
257
|
+
# Ensure every model has a stable runtime id — covers env-injected
|
|
258
|
+
# models (CLACKY_XXX, CLAUDE_XXX) that don't go through parse_models.
|
|
259
|
+
# Ids are NOT persisted to config.yml (see to_yaml).
|
|
260
|
+
models.each { |m| m["id"] ||= SecureRandom.uuid }
|
|
261
|
+
|
|
241
262
|
# Find the index of the model marked as "default" (type: default)
|
|
242
263
|
# Fall back to 0 if no model has type: default
|
|
243
264
|
default_index = models.find_index { |m| m["type"] == "default" } || 0
|
|
265
|
+
default_id = models[default_index] && models[default_index]["id"]
|
|
244
266
|
|
|
245
|
-
new(models: models, current_model_index: default_index)
|
|
267
|
+
new(models: models, current_model_index: default_index, current_model_id: default_id)
|
|
246
268
|
end
|
|
247
269
|
|
|
248
270
|
# Auto-inject a lite model entry if the default model's provider supports one
|
|
@@ -270,19 +292,35 @@ module Clacky
|
|
|
270
292
|
"model" => lite_model_name,
|
|
271
293
|
"anthropic_format" => default_model["anthropic_format"] || false,
|
|
272
294
|
"type" => "lite",
|
|
273
|
-
"auto_injected" => true # Mark as auto-injected (not saved to file)
|
|
295
|
+
"auto_injected" => true, # Mark as auto-injected (not saved to file)
|
|
296
|
+
"id" => SecureRandom.uuid # Runtime id for stable session references
|
|
274
297
|
}
|
|
275
298
|
end
|
|
276
299
|
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
300
|
+
# Create a per-session copy of this config.
|
|
301
|
+
#
|
|
302
|
+
# Plan B (shared models): we deliberately share the SAME @models array
|
|
303
|
+
# reference with all sessions (no deep clone). This is the key design
|
|
304
|
+
# decision that keeps session and global views in sync:
|
|
305
|
+
# - User adds a model in Settings → every live session sees it instantly.
|
|
306
|
+
# - User edits api_key/base_url → every live session's next API call
|
|
307
|
+
# picks up the new credentials (via current_model lookup).
|
|
308
|
+
# - Model ids are stable across edits, so each session's
|
|
309
|
+
# @current_model_id continues to resolve correctly.
|
|
310
|
+
#
|
|
311
|
+
# Per-session state that MUST stay isolated (permission_mode,
|
|
312
|
+
# @current_model_id, @current_model_index, fallback state) are scalar
|
|
313
|
+
# copies via `dup` and don't leak between sessions.
|
|
314
|
+
#
|
|
315
|
+
# Before Plan B, sessions held deep-copied @models — which silently
|
|
316
|
+
# diverged from the global list any time the user added/edited a model
|
|
317
|
+
# in Settings, producing bugs like "Failed to switch model" for newly
|
|
318
|
+
# added models on Windows and Linux. See http_server.rb#api_switch_session_model
|
|
319
|
+
# and http_server.rb#api_save_config for the companion logic.
|
|
282
320
|
def deep_copy
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
321
|
+
# dup gives us a new AgentConfig with independent scalar ivars but
|
|
322
|
+
# the same @models reference — exactly what we want.
|
|
323
|
+
dup
|
|
286
324
|
end
|
|
287
325
|
|
|
288
326
|
def save(config_file = CONFIG_FILE)
|
|
@@ -295,8 +333,14 @@ module Clacky
|
|
|
295
333
|
# Convert to YAML format (top-level array)
|
|
296
334
|
# Auto-injected lite models (auto_injected: true) are excluded from persistence —
|
|
297
335
|
# they are regenerated at load time from the provider preset.
|
|
336
|
+
# Runtime-only fields (id, auto_injected) are stripped before writing so
|
|
337
|
+
# config.yml remains backward compatible with users on older versions.
|
|
338
|
+
RUNTIME_ONLY_FIELDS = %w[id auto_injected].freeze
|
|
339
|
+
|
|
298
340
|
def to_yaml
|
|
299
|
-
persistable = @models.reject { |m| m["auto_injected"] }
|
|
341
|
+
persistable = @models.reject { |m| m["auto_injected"] }.map do |m|
|
|
342
|
+
m.reject { |k, _| RUNTIME_ONLY_FIELDS.include?(k) }
|
|
343
|
+
end
|
|
300
344
|
YAML.dump(persistable)
|
|
301
345
|
end
|
|
302
346
|
|
|
@@ -305,32 +349,80 @@ module Clacky
|
|
|
305
349
|
!@models.empty? && !current_model.nil?
|
|
306
350
|
end
|
|
307
351
|
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
@models[@current_model_index]
|
|
312
|
-
end
|
|
352
|
+
# NOTE: current_model is defined below (near the id-aware lookup path)
|
|
353
|
+
# — the earlier duplicate definition was removed. Ruby silently picks the
|
|
354
|
+
# last definition, but keeping only one avoids confusion.
|
|
313
355
|
|
|
314
356
|
# Get model by index
|
|
315
357
|
def get_model(index)
|
|
316
358
|
@models[index]
|
|
317
359
|
end
|
|
318
360
|
|
|
319
|
-
# Switch to model by
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
361
|
+
# Switch the current session to a specific model, identified by its
|
|
362
|
+
# stable runtime id.
|
|
363
|
+
#
|
|
364
|
+
# This is a **per-session** operation:
|
|
365
|
+
# - Updates this AgentConfig's `@current_model_id` (primary truth)
|
|
366
|
+
# - Updates `@current_model_index` for back-compat observers
|
|
367
|
+
# - Does NOT mutate the shared `@models` array's `type: "default"`
|
|
368
|
+
# marker. The "default model" is a global setting (initial model
|
|
369
|
+
# for new sessions) and is only changed via the Settings UI
|
|
370
|
+
# "save config" flow (`api_save_config`).
|
|
371
|
+
#
|
|
372
|
+
# @param id [String] the model's runtime id (see parse_models)
|
|
373
|
+
# @return [Boolean] true if switched, false if id not found
|
|
374
|
+
def switch_model_by_id(id)
|
|
375
|
+
return false if id.nil? || id.to_s.empty?
|
|
376
|
+
|
|
377
|
+
index = @models.find_index { |m| m["id"] == id }
|
|
378
|
+
return false if index.nil?
|
|
379
|
+
|
|
380
|
+
@current_model_id = id
|
|
332
381
|
@current_model_index = index
|
|
333
|
-
|
|
382
|
+
|
|
383
|
+
true
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Set the **global** default model marker (`type: "default"`).
|
|
387
|
+
#
|
|
388
|
+
# This is separate from `switch_model_by_id`:
|
|
389
|
+
# - `switch_model_by_id` only changes this session's current model.
|
|
390
|
+
# - `set_default_model_by_id` mutates the shared `@models` array by
|
|
391
|
+
# moving the `type: "default"` marker to the given model.
|
|
392
|
+
#
|
|
393
|
+
# Use cases:
|
|
394
|
+
# - CLI (single-session): when the user picks a model, we both switch
|
|
395
|
+
# this session AND update the global default so future CLI launches
|
|
396
|
+
# use the same model. Caller must `save` to persist.
|
|
397
|
+
# - Web UI Settings save flow: also uses this (via payload).
|
|
398
|
+
#
|
|
399
|
+
# Do NOT call from per-session model switching in multi-session contexts
|
|
400
|
+
# (Web UI session-level switch), since it would leak into other sessions
|
|
401
|
+
# and change what new sessions start with.
|
|
402
|
+
#
|
|
403
|
+
# Only one model may carry `type: "default"` at a time — this method
|
|
404
|
+
# clears the marker on any other model that had it.
|
|
405
|
+
#
|
|
406
|
+
# Note: if the target model currently has `type: "lite"`, this method
|
|
407
|
+
# will overwrite it with `"default"`. That matches the existing
|
|
408
|
+
# single-slot `type` field semantics in the codebase.
|
|
409
|
+
#
|
|
410
|
+
# @param id [String] the model's runtime id
|
|
411
|
+
# @return [Boolean] true if marker was moved, false if id not found
|
|
412
|
+
def set_default_model_by_id(id)
|
|
413
|
+
return false if id.nil? || id.to_s.empty?
|
|
414
|
+
|
|
415
|
+
target = @models.find { |m| m["id"] == id }
|
|
416
|
+
return false if target.nil?
|
|
417
|
+
|
|
418
|
+
# Clear existing default marker(s) — there should only be one, but
|
|
419
|
+
# be defensive in case of corrupted config.
|
|
420
|
+
@models.each do |m|
|
|
421
|
+
next if m["id"] == id
|
|
422
|
+
m.delete("type") if m["type"] == "default"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
target["type"] = "default"
|
|
334
426
|
true
|
|
335
427
|
end
|
|
336
428
|
|
|
@@ -385,6 +477,7 @@ module Clacky
|
|
|
385
477
|
# Add a new model configuration
|
|
386
478
|
def add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil)
|
|
387
479
|
@models << {
|
|
480
|
+
"id" => SecureRandom.uuid,
|
|
388
481
|
"api_key" => api_key,
|
|
389
482
|
"base_url" => base_url,
|
|
390
483
|
"model" => model,
|
|
@@ -486,15 +579,35 @@ module Clacky
|
|
|
486
579
|
end
|
|
487
580
|
end
|
|
488
581
|
|
|
489
|
-
# Get current model configuration
|
|
490
|
-
#
|
|
582
|
+
# Get current model configuration.
|
|
583
|
+
#
|
|
584
|
+
# Resolution order:
|
|
585
|
+
# 1. @current_model_id (primary source of truth — stable across list edits)
|
|
586
|
+
# 2. type: default (for config.yml that sets a default explicitly)
|
|
587
|
+
# 3. @current_model_index (back-compat for very old code paths)
|
|
491
588
|
def current_model
|
|
492
589
|
return nil if @models.empty?
|
|
590
|
+
|
|
591
|
+
if @current_model_id
|
|
592
|
+
m = @models.find { |mm| mm["id"] == @current_model_id }
|
|
593
|
+
return m if m
|
|
594
|
+
# id no longer exists (model was deleted). Fall through to other
|
|
595
|
+
# resolution strategies below, and clear the stale id.
|
|
596
|
+
@current_model_id = nil
|
|
597
|
+
end
|
|
598
|
+
|
|
493
599
|
default_model = find_model_by_type("default")
|
|
494
|
-
|
|
495
|
-
|
|
600
|
+
if default_model
|
|
601
|
+
# Opportunistically re-anchor to this default's id so subsequent
|
|
602
|
+
# lookups are O(1) and survive list reordering.
|
|
603
|
+
@current_model_id = default_model["id"]
|
|
604
|
+
return default_model
|
|
605
|
+
end
|
|
606
|
+
|
|
496
607
|
# Fallback to index-based for backward compatibility
|
|
497
|
-
@models[@current_model_index]
|
|
608
|
+
m = @models[@current_model_index]
|
|
609
|
+
@current_model_id = m["id"] if m
|
|
610
|
+
m
|
|
498
611
|
end
|
|
499
612
|
|
|
500
613
|
# Set a model's type (default or lite)
|
|
@@ -528,14 +641,20 @@ module Clacky
|
|
|
528
641
|
# Don't allow removing the last model
|
|
529
642
|
return false if @models.length <= 1
|
|
530
643
|
return false if index < 0 || index >= @models.length
|
|
531
|
-
|
|
532
|
-
@models.delete_at(index)
|
|
533
|
-
|
|
644
|
+
|
|
645
|
+
removed = @models.delete_at(index)
|
|
646
|
+
|
|
534
647
|
# Adjust current_model_index if necessary
|
|
535
648
|
if @current_model_index >= @models.length
|
|
536
649
|
@current_model_index = @models.length - 1
|
|
537
650
|
end
|
|
538
|
-
|
|
651
|
+
|
|
652
|
+
# If the removed model was the current one, clear @current_model_id.
|
|
653
|
+
# current_model will then fall back to type: default / current_model_index.
|
|
654
|
+
if removed && @current_model_id == removed["id"]
|
|
655
|
+
@current_model_id = nil
|
|
656
|
+
end
|
|
657
|
+
|
|
539
658
|
true
|
|
540
659
|
end
|
|
541
660
|
|
|
@@ -615,6 +734,13 @@ module Clacky
|
|
|
615
734
|
}
|
|
616
735
|
end
|
|
617
736
|
|
|
737
|
+
# Inject a runtime-only stable id for each model. Ids are NOT written
|
|
738
|
+
# back to config.yml (see `to_yaml`) so this is fully backward
|
|
739
|
+
# compatible — old yml files without ids just get fresh ids on load.
|
|
740
|
+
# The id is the source of truth for session→model identity and is
|
|
741
|
+
# immune to list reordering, additions, and field edits (api_key, etc).
|
|
742
|
+
models.each { |m| m["id"] ||= SecureRandom.uuid }
|
|
743
|
+
|
|
618
744
|
models
|
|
619
745
|
end
|
|
620
746
|
end
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -271,26 +271,45 @@ module Clacky
|
|
|
271
271
|
end
|
|
272
272
|
|
|
273
273
|
if brand.heartbeat_due?
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
# Fire-and-forget heartbeat in a background thread.
|
|
275
|
+
#
|
|
276
|
+
# Rationale: a slow/unreachable license server would otherwise block
|
|
277
|
+
# CLI startup for up to ~92s (2 hosts × 2 attempts × 23s timeout)
|
|
278
|
+
# before the user sees the prompt. Heartbeat is "best-effort" by
|
|
279
|
+
# design — its only job is to refresh `last_heartbeat` / `expires_at`
|
|
280
|
+
# on disk for the next run's grace-period calculation. Missing a
|
|
281
|
+
# single heartbeat is harmless; the next launch will try again.
|
|
282
|
+
#
|
|
283
|
+
# Consequence: if this run was going to trigger the
|
|
284
|
+
# grace_period_exceeded warning, the user will see it on the *next*
|
|
285
|
+
# launch instead of this one. Acceptable trade-off.
|
|
286
|
+
Clacky::Logger.info("[Brand] check_brand_license_cli: heartbeat due, dispatching async...")
|
|
287
|
+
Thread.new do
|
|
288
|
+
begin
|
|
289
|
+
result = brand.heartbeat!
|
|
290
|
+
if result[:success]
|
|
291
|
+
Clacky::Logger.info("[Brand] async heartbeat OK")
|
|
286
292
|
else
|
|
287
|
-
|
|
293
|
+
Clacky::Logger.warn("[Brand] async heartbeat failed — #{result[:message]}")
|
|
288
294
|
end
|
|
295
|
+
rescue StandardError => e
|
|
296
|
+
Clacky::Logger.warn("[Brand] async heartbeat raised: #{e.class}: #{e.message}")
|
|
289
297
|
end
|
|
290
298
|
end
|
|
291
299
|
else
|
|
292
300
|
Clacky::Logger.debug("[Brand] check_brand_license_cli: heartbeat not due yet")
|
|
293
301
|
end
|
|
302
|
+
|
|
303
|
+
# Surface the grace-period warning based on *already-persisted* state
|
|
304
|
+
# (computed from last_heartbeat on disk). This works whether the
|
|
305
|
+
# previous run's heartbeat succeeded, failed, or was interrupted —
|
|
306
|
+
# grace_period_exceeded? reads last_heartbeat, not this run's result.
|
|
307
|
+
if brand.grace_period_exceeded?
|
|
308
|
+
say ""
|
|
309
|
+
say "WARNING: Could not reach the #{brand.product_name} license server.", :yellow
|
|
310
|
+
say "License has been offline for more than 3 days. Please check your connection.", :yellow
|
|
311
|
+
say ""
|
|
312
|
+
end
|
|
294
313
|
end
|
|
295
314
|
|
|
296
315
|
# Interactive license key prompt using tty-prompt.
|