openclacky 0.7.7 → 0.7.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c3815310a11bea9ccad7ab6891b4ad85e248635af6919606e8963096699a1d7
4
- data.tar.gz: 17f8059179701394a6951966ec0284ee6321750e2651c4de0ecbf37a4542c7fc
3
+ metadata.gz: 9b2a72212b8b9e8072acb09a055df1afdd8225ee81de41f11dce460a68ff1781
4
+ data.tar.gz: e8048bb38dc4a8ae8c85937165e10a855f8642d6759c8a2a94aa336b084e4188
5
5
  SHA512:
6
- metadata.gz: b7b32ce62e12f9aa12db59b1f6640e4a6234c34c485d9b679df80654dc8722a369687e1fc0b5165a78a12f6210fcd02959133d6d3753c5c22873d071251b07fb
7
- data.tar.gz: 20f4498c976579333b7e261490d287a33cec89b888c5ae1a640205c25f48c70c3863a551e1bef6173804b7670970ead88ed9a67702987da97963c0a981cd1e2a
6
+ metadata.gz: '095710e060b7c29882eb128be1a4008281435c13af07e7da01ec9ad1af27c30e252eb19e77f557882628409058d504f29024ea0dbb396ed20d326b26cfe1925c'
7
+ data.tar.gz: 2875ac5911322341be5fe396ad44a98e4cbcea51c95ed7d832e3978d5790542ca94ae0f065f6170484d9900be2f1ca190f6db0d25c14a33366f924f9d13a147d
@@ -1,7 +1,7 @@
1
1
  ---
2
+ ---
2
3
  name: commit
3
4
  description: Smart Git commit helper that analyzes changes and creates semantic commits
4
- disable-model-invocation: false
5
5
  user-invocable: true
6
6
  ---
7
7
 
@@ -472,4 +472,4 @@ This skill works best:
472
472
 
473
473
  - Created: 2025-02-01
474
474
  - Purpose: Improve commit quality and development workflow
475
- - Compatible with: Any git repository
475
+ - Compatible with: Any git repository
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.8] - 2026-03-06
11
+
12
+ ### Added
13
+ - Skills panel in web UI: list all skills, enable/disable with toggle, view skill details
14
+ - Hash-based routing (`#session/:id`, `#tasks`, `#skills`, `#settings`) with deep-link and refresh support
15
+ - REST API endpoints for skills management (`GET /api/skills`, `PATCH /api/skills/:name/toggle`)
16
+ - `disabled?` helper on `Skill` model for quick enabled/disabled state checks
17
+
18
+ ### Improved
19
+ - Centralized `Router` object in web UI — single source of truth for all panel switching and sidebar highlight state
20
+ - Web UI frontend split further: `skills.js` extracted as standalone module
21
+ - Ctrl-C in web server now exits immediately via `StartCallback` trap override
22
+ - Skill enable/disable now writes `disable-model-invocation: false` (retains field) instead of deleting it
23
+
24
+ ### Fixed
25
+ - Sidebar highlight for Tasks and Skills stuck active after navigating away
26
+ - Router correctly restores last view on page refresh via hash URL
27
+
28
+ ### Changed
29
+ - Removed `plan_only` permission mode from agent, CLI, and web UI
30
+
10
31
  ## [0.7.7] - 2026-03-04
11
32
 
12
33
  ### Added
@@ -16,8 +16,6 @@ module Clacky
16
16
  when :confirm_safes
17
17
  # Use SafeShell integration for safety check
18
18
  is_safe_operation?(tool_name, tool_params)
19
- when :plan_only
20
- false
21
19
  else
22
20
  false
23
21
  end
@@ -217,16 +215,6 @@ module Clacky
217
215
  }
218
216
  end
219
217
 
220
- # Build planned result for plan-only mode
221
- # @param call [Hash] Tool call
222
- # @return [Hash] Formatted planned result
223
- def build_planned_result(call)
224
- {
225
- id: call[:id],
226
- content: JSON.generate({ planned: true, message: "Tool execution skipped (plan mode)" })
227
- }
228
- end
229
-
230
218
  # Check if a tool is potentially slow and should show progress
231
219
  # @param tool_name [String] Name of the tool
232
220
  # @param args [Hash] Tool arguments
data/lib/clacky/agent.rb CHANGED
@@ -389,12 +389,6 @@ module Clacky
389
389
  end
390
390
  else
391
391
  # Permission check (if not in auto-approve mode)
392
- if @config.is_plan_only?
393
- @ui&.show_info("Planned: #{call[:name]}")
394
- results << build_planned_result(call)
395
- next
396
- end
397
-
398
392
  confirmation = confirm_tool_use?(call)
399
393
  unless confirmation[:approved]
400
394
  # Show denial warning only for user-initiated denials (not system-injected preview errors)
@@ -148,7 +148,7 @@ module Clacky
148
148
  # Default model for ClaudeCode environment
149
149
  CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-5"
150
150
 
151
- PERMISSION_MODES = [:auto_approve, :confirm_safes, :plan_only].freeze
151
+ PERMISSION_MODES = [:auto_approve, :confirm_safes].freeze
152
152
 
153
153
  attr_accessor :permission_mode, :max_tokens, :verbose,
154
154
  :enable_compression, :enable_prompt_caching,
@@ -397,10 +397,6 @@ module Clacky
397
397
  true
398
398
  end
399
399
 
400
- def is_plan_only?
401
- @permission_mode == :plan_only
402
- end
403
-
404
400
  private def validate_permission_mode(mode)
405
401
  mode ||= :confirm_safes
406
402
  mode = mode.to_sym
data/lib/clacky/cli.rb CHANGED
@@ -27,7 +27,6 @@ module Clacky
27
27
  Permission modes:
28
28
  auto_approve - Automatically execute all tools (use with caution)
29
29
  confirm_safes - Auto-approve safe operations, confirm risky ones (default)
30
- plan_only - Generate plan without executing
31
30
 
32
31
  UI themes:
33
32
  hacker - Matrix/hacker-style with bracket symbols (default)
@@ -42,7 +41,7 @@ module Clacky
42
41
  $ clacky agent --mode=auto_approve --path /path/to/project
43
42
  LONGDESC
44
43
  option :mode, type: :string, default: "confirm_safes",
45
- desc: "Permission mode: auto_approve, confirm_safes, plan_only"
44
+ desc: "Permission mode: auto_approve, confirm_safes"
46
45
  option :theme, type: :string, default: "hacker",
47
46
  desc: "UI theme: hacker, minimal (default: hacker)"
48
47
  option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Show detailed output"
@@ -33,14 +33,19 @@ module Clacky
33
33
  session_registry: @registry,
34
34
  session_builder: method(:build_session)
35
35
  )
36
+ @skill_loader = Clacky::SkillLoader.new
36
37
  end
37
38
 
38
39
  def start
40
+ # Override WEBrick's built-in signal traps via StartCallback,
41
+ # which fires after WEBrick sets its own INT/TERM handlers.
42
+ # This ensures Ctrl-C always exits immediately.
39
43
  server = WEBrick::HTTPServer.new(
40
44
  BindAddress: @host,
41
45
  Port: @port,
42
46
  Logger: WEBrick::Log.new(File::NULL),
43
- AccessLog: []
47
+ AccessLog: [],
48
+ StartCallback: proc { trap("INT") { exit(0) }; trap("TERM") { exit(0) } }
44
49
  )
45
50
 
46
51
  # Mount API + WebSocket handler (takes priority).
@@ -69,10 +74,6 @@ module Clacky
69
74
  res["Pragma"] = "no-cache"
70
75
  end
71
76
 
72
- # Graceful shutdown on Ctrl-C
73
- trap("INT") { @scheduler.stop; server.shutdown }
74
- trap("TERM") { @scheduler.stop; server.shutdown }
75
-
76
77
  puts "🌐 Clacky Web UI running at http://#{@host}:#{@port}"
77
78
  puts " Press Ctrl-C to stop."
78
79
 
@@ -108,6 +109,7 @@ module Clacky
108
109
  when ["GET", "/api/tasks"] then api_list_tasks(res)
109
110
  when ["POST", "/api/tasks"] then api_create_task(req, res)
110
111
  when ["POST", "/api/tasks/run"] then api_run_task(req, res)
112
+ when ["GET", "/api/skills"] then api_list_skills(res)
111
113
  when ["GET", "/api/config"] then api_get_config(res)
112
114
  when ["POST", "/api/config"] then api_save_config(req, res)
113
115
  when ["POST", "/api/config/test"] then api_test_config(req, res)
@@ -125,6 +127,9 @@ module Clacky
125
127
  elsif method == "DELETE" && path.start_with?("/api/tasks/")
126
128
  name = URI.decode_www_form_component(path.sub("/api/tasks/", ""))
127
129
  api_delete_task(name, res)
130
+ elsif method == "PATCH" && path.match?(%r{^/api/skills/[^/]+/toggle$})
131
+ name = URI.decode_www_form_component(path.sub("/api/skills/", "").sub("/toggle", ""))
132
+ api_toggle_skill(name, req, res)
128
133
  else
129
134
  not_found(res)
130
135
  end
@@ -138,15 +143,12 @@ module Clacky
138
143
  end
139
144
 
140
145
  def api_create_session(req, res)
141
- body = parse_json_body(req)
146
+ body = parse_json_body(req)
142
147
  name = body["name"]
143
- working_dir = default_working_dir
148
+ working_dir = body["working_dir"]&.then { |d| File.expand_path(d) } || default_working_dir
144
149
 
145
- # Validate working directory
146
- unless Dir.exist?(working_dir)
147
- json_response(res, 422, { error: "Directory does not exist: #{working_dir}" })
148
- return
149
- end
150
+ # Auto-create the working directory if it does not exist yet
151
+ FileUtils.mkdir_p(working_dir)
150
152
 
151
153
  session_id = build_session(name: name, working_dir: working_dir)
152
154
  json_response(res, 201, { session: @registry.list.find { |s| s[:id] == session_id } })
@@ -266,6 +268,40 @@ module Clacky
266
268
  end
267
269
  end
268
270
 
271
+ # ── Skills API ────────────────────────────────────────────────────────────
272
+
273
+ # GET /api/skills — list all loaded skills with metadata
274
+ def api_list_skills(res)
275
+ @skill_loader.load_all # refresh from disk on each request
276
+ skills = @skill_loader.all_skills.map do |skill|
277
+ source = @skill_loader.loaded_from[skill.identifier]
278
+ {
279
+ name: skill.identifier,
280
+ description: skill.context_description,
281
+ source: source,
282
+ enabled: !skill.disabled?
283
+ }
284
+ end
285
+ json_response(res, 200, { skills: skills })
286
+ end
287
+
288
+ # PATCH /api/skills/:name/toggle — enable or disable a skill
289
+ # Body: { enabled: true/false }
290
+ def api_toggle_skill(name, req, res)
291
+ body = parse_json_body(req)
292
+ enabled = body["enabled"]
293
+
294
+ if enabled.nil?
295
+ json_response(res, 422, { error: "enabled field required" })
296
+ return
297
+ end
298
+
299
+ skill = @skill_loader.toggle_skill(name, enabled: enabled)
300
+ json_response(res, 200, { ok: true, name: skill.identifier, enabled: !skill.disabled? })
301
+ rescue Clacky::AgentError => e
302
+ json_response(res, 422, { error: e.message })
303
+ end
304
+
269
305
  # ── Config API ────────────────────────────────────────────────────────────
270
306
 
271
307
  # GET /api/config — return current model configurations
@@ -529,7 +565,6 @@ module Clacky
529
565
  broadcast_session_update(session_id)
530
566
  broadcast(session_id, { type: "error", session_id: session_id, message: e.message })
531
567
  end
532
-
533
568
  @registry.with_session(session_id) { |s| s[:thread] = thread }
534
569
  end
535
570
 
@@ -579,7 +614,6 @@ module Clacky
579
614
  broadcast_session_update(session_id)
580
615
  broadcast(session_id, { type: "error", session_id: session_id, message: e.message })
581
616
  end
582
-
583
617
  @registry.with_session(session_id) { |s| s[:thread] = thread }
584
618
  end
585
619
 
@@ -158,7 +158,7 @@ module Clacky
158
158
 
159
159
  $stdout.puts "[Clacky Scheduler] Firing task '#{task_name}' (session: #{session_id})"
160
160
 
161
- # Run the agent in a background thread so the scheduler tick is non-blocking
161
+ # Run the agent in a background thread so the scheduler tick is non-blocking.
162
162
  Thread.new do
163
163
  session = @registry.get(session_id)
164
164
  agent = nil
@@ -174,6 +174,7 @@ module Clacky
174
174
  @registry.update(session_id, status: :error, error: e.message)
175
175
  $stderr.puts "[Clacky Scheduler] Task '#{task_name}' failed: #{e.message}"
176
176
  end
177
+
177
178
  rescue => e
178
179
  $stderr.puts "[Clacky Scheduler] Failed to fire task '#{schedule["task"]}': #{e.message}"
179
180
  end
data/lib/clacky/skill.rb CHANGED
@@ -30,6 +30,13 @@ module Clacky
30
30
  attr_reader :allowed_tools, :context, :agent_type, :argument_hint, :hooks
31
31
  attr_reader :fork_agent, :model, :forbidden_tools, :auto_summarize
32
32
 
33
+ # Check if this skill is disabled (disable-model-invocation: true)
34
+ # @return [Boolean]
35
+ def disabled?
36
+ @disable_model_invocation == true
37
+ end
38
+
39
+
33
40
  # @param directory [Pathname, String] Path to the skill directory
34
41
  # @param source_path [Pathname, String, nil] Optional source path for priority resolution
35
42
  def initialize(directory, source_path: nil)
@@ -208,6 +208,34 @@ module Clacky
208
208
  load_single_skill(skill_dir, skill_dir, name, source_type)
209
209
  end
210
210
 
211
+ # Toggle a skill's disable-model-invocation field in its SKILL.md.
212
+ # System skills (source: :default) cannot be toggled — raises AgentError.
213
+ # @param name [String] Skill identifier
214
+ # @param enabled [Boolean] true = enable, false = disable
215
+ # @return [Skill] The reloaded skill
216
+ def toggle_skill(name, enabled:)
217
+ skill = @skills[name]
218
+ raise Clacky::AgentError, "Skill not found: #{name}" unless skill
219
+ raise Clacky::AgentError, "Cannot toggle system skill: #{name}" if @loaded_from[name] == :default
220
+
221
+ skill_file = skill.directory.join("SKILL.md")
222
+ fm = (skill.frontmatter || {}).dup
223
+
224
+ if enabled
225
+ fm["disable-model-invocation"] = false
226
+ else
227
+ fm["disable-model-invocation"] = true
228
+ end
229
+
230
+ skill_file.write(build_skill_content(fm, skill.content))
231
+
232
+ # Reload into registry
233
+ reloaded = Skill.new(skill.directory, source_path: skill.source_path)
234
+ @skills[reloaded.identifier] = reloaded
235
+ @skills_by_command[reloaded.slash_command] = reloaded
236
+ reloaded
237
+ end
238
+
211
239
  # Delete a skill
212
240
  # @param name [String] Skill name
213
241
  # @return [Boolean] True if deleted, false if not found
@@ -1232,8 +1232,6 @@ module Clacky
1232
1232
  :magenta
1233
1233
  when /confirm_safes/
1234
1234
  :cyan
1235
- when /plan_only/
1236
- :blue
1237
1235
  else
1238
1236
  :white
1239
1237
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.7.7"
4
+ VERSION = "0.7.8"
5
5
  end
@@ -779,6 +779,248 @@ body {
779
779
  .form-textarea:focus { outline: none; border-color: #58a6ff; }
780
780
  .form-hint { font-size: 11px; color: #8b949e; margin-top: 4px; }
781
781
 
782
+ /* ── Skills Panel ────────────────────────────────────────────────────────── */
783
+ #skills-panel {
784
+ display: flex;
785
+ flex-direction: column;
786
+ height: 100%;
787
+ overflow: hidden;
788
+ }
789
+ #skills-header {
790
+ padding: 12px 20px;
791
+ border-bottom: 1px solid #30363d;
792
+ display: flex;
793
+ align-items: center;
794
+ justify-content: space-between;
795
+ background: #161b22;
796
+ flex-shrink: 0;
797
+ font-weight: 600;
798
+ font-size: 15px;
799
+ }
800
+ #btn-create-skill {
801
+ padding: 5px 12px;
802
+ background: #238636;
803
+ color: #fff;
804
+ border: none;
805
+ border-radius: 6px;
806
+ font-size: 13px;
807
+ cursor: pointer;
808
+ white-space: nowrap;
809
+ }
810
+ #btn-create-skill:hover { background: #2ea043; }
811
+ #skills-body {
812
+ flex: 1;
813
+ overflow: hidden;
814
+ display: flex;
815
+ flex-direction: column;
816
+ padding: 20px 24px;
817
+ gap: 16px;
818
+ }
819
+
820
+ /* ── Skills Tabs ─────────────────────────────────────────────────────────── */
821
+ #skills-tabs {
822
+ display: flex;
823
+ gap: 4px;
824
+ border-bottom: 1px solid #30363d;
825
+ flex-shrink: 0;
826
+ }
827
+ .skills-tab {
828
+ background: none;
829
+ border: none;
830
+ border-bottom: 2px solid transparent;
831
+ color: #8b949e;
832
+ cursor: pointer;
833
+ font-size: 13px;
834
+ font-weight: 500;
835
+ padding: 8px 16px;
836
+ margin-bottom: -1px;
837
+ transition: color .15s, border-color .15s;
838
+ }
839
+ .skills-tab:hover { color: #e6edf3; }
840
+ .skills-tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }
841
+
842
+ .skills-tab-content {
843
+ flex: 1;
844
+ overflow-y: auto;
845
+ }
846
+
847
+ /* ── My Skills list ──────────────────────────────────────────────────────── */
848
+ #skills-list {
849
+ display: flex;
850
+ flex-direction: column;
851
+ gap: 8px;
852
+ padding-top: 12px;
853
+ }
854
+ .skills-empty {
855
+ color: #8b949e;
856
+ font-size: 13px;
857
+ padding: 20px 0;
858
+ text-align: center;
859
+ }
860
+
861
+ /* ── Skill Card ──────────────────────────────────────────────────────────── */
862
+ .skill-card {
863
+ background: #161b22;
864
+ border: 1px solid #30363d;
865
+ border-radius: 8px;
866
+ padding: 14px 16px;
867
+ transition: border-color .15s;
868
+ }
869
+ .skill-card:hover { border-color: #484f58; }
870
+ .skill-card-main {
871
+ display: flex;
872
+ align-items: center;
873
+ gap: 12px;
874
+ }
875
+ .skill-card-info { flex: 1; min-width: 0; }
876
+ .skill-card-title {
877
+ display: flex;
878
+ align-items: center;
879
+ gap: 8px;
880
+ margin-bottom: 4px;
881
+ }
882
+ .skill-name {
883
+ font-size: 13px;
884
+ font-weight: 600;
885
+ color: #e6edf3;
886
+ }
887
+ .skill-card-desc {
888
+ font-size: 12px;
889
+ color: #8b949e;
890
+ line-height: 1.5;
891
+ white-space: nowrap;
892
+ overflow: hidden;
893
+ text-overflow: ellipsis;
894
+ }
895
+
896
+ /* ── Skill Badges ────────────────────────────────────────────────────────── */
897
+ .skill-badge {
898
+ font-size: 10px;
899
+ font-weight: 600;
900
+ padding: 2px 7px;
901
+ border-radius: 10px;
902
+ letter-spacing: 0.3px;
903
+ flex-shrink: 0;
904
+ }
905
+ .skill-badge-system {
906
+ background: #1f3a5f;
907
+ color: #79c0ff;
908
+ border: 1px solid #1d4070;
909
+ }
910
+ .skill-badge-custom {
911
+ background: #1f3024;
912
+ color: #56d364;
913
+ border: 1px solid #1c4028;
914
+ }
915
+
916
+ /* ── Toggle Switch ───────────────────────────────────────────────────────── */
917
+ .skill-toggle {
918
+ position: relative;
919
+ display: inline-flex;
920
+ align-items: center;
921
+ cursor: pointer;
922
+ flex-shrink: 0;
923
+ }
924
+ .skill-toggle-disabled {
925
+ cursor: not-allowed;
926
+ opacity: 0.4;
927
+ }
928
+ .skill-toggle-input {
929
+ opacity: 0;
930
+ width: 0;
931
+ height: 0;
932
+ position: absolute;
933
+ }
934
+ .skill-toggle-track {
935
+ display: inline-block;
936
+ width: 36px;
937
+ height: 20px;
938
+ background: #30363d;
939
+ border-radius: 10px;
940
+ transition: background .2s;
941
+ position: relative;
942
+ }
943
+ .skill-toggle-track::after {
944
+ content: "";
945
+ position: absolute;
946
+ width: 14px;
947
+ height: 14px;
948
+ background: #fff;
949
+ border-radius: 50%;
950
+ top: 3px;
951
+ left: 3px;
952
+ transition: left .2s;
953
+ }
954
+ .skill-toggle-input:checked + .skill-toggle-track {
955
+ background: #238636;
956
+ }
957
+ .skill-toggle-input:checked + .skill-toggle-track::after {
958
+ left: 19px;
959
+ }
960
+
961
+ /* ── Store Grid ──────────────────────────────────────────────────────────── */
962
+ #skills-store-grid {
963
+ display: flex;
964
+ flex-direction: column;
965
+ gap: 10px;
966
+ padding-top: 12px;
967
+ }
968
+ .store-card {
969
+ background: #161b22;
970
+ border: 1px solid #30363d;
971
+ border-radius: 8px;
972
+ padding: 16px;
973
+ display: flex;
974
+ align-items: center;
975
+ gap: 14px;
976
+ transition: border-color .15s;
977
+ }
978
+ .store-card:hover { border-color: #484f58; }
979
+ .store-card-icon {
980
+ font-size: 28px;
981
+ flex-shrink: 0;
982
+ width: 44px;
983
+ height: 44px;
984
+ display: flex;
985
+ align-items: center;
986
+ justify-content: center;
987
+ background: #0d1117;
988
+ border-radius: 8px;
989
+ }
990
+ .store-card-body { flex: 1; min-width: 0; }
991
+ .store-card-title {
992
+ font-size: 13px;
993
+ font-weight: 600;
994
+ color: #e6edf3;
995
+ margin-bottom: 4px;
996
+ }
997
+ .store-card-desc {
998
+ font-size: 12px;
999
+ color: #8b949e;
1000
+ line-height: 1.5;
1001
+ }
1002
+ .store-card-actions { flex-shrink: 0; }
1003
+ .btn-store-install {
1004
+ background: #238636;
1005
+ color: #fff;
1006
+ border: none;
1007
+ border-radius: 6px;
1008
+ padding: 6px 14px;
1009
+ font-size: 12px;
1010
+ font-weight: 600;
1011
+ cursor: pointer;
1012
+ transition: background .15s;
1013
+ }
1014
+ .btn-store-install:hover { background: #2ea043; }
1015
+ .store-badge-installed {
1016
+ font-size: 12px;
1017
+ color: #56d364;
1018
+ font-weight: 600;
1019
+ }
1020
+
1021
+ /* ── Skills sidebar section ──────────────────────────────────────────────── */
1022
+ #skill-list-items { padding: 0 8px 8px; display: flex; flex-direction: column; gap: 2px; }
1023
+
782
1024
  /* ── Scrollbar ───────────────────────────────────────────────────────────── */
783
1025
  ::-webkit-scrollbar { width: 6px; }
784
1026
  ::-webkit-scrollbar-track { background: transparent; }