openclacky 0.6.2 ā 0.6.4
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 +30 -0
- data/docs/why-openclacky.md +267 -0
- data/lib/clacky/agent.rb +579 -99
- data/lib/clacky/cli.rb +350 -9
- data/lib/clacky/client.rb +519 -58
- data/lib/clacky/config.rb +71 -4
- data/lib/clacky/default_skills/skill-add/SKILL.md +66 -0
- data/lib/clacky/skill.rb +236 -0
- data/lib/clacky/skill_loader.rb +320 -0
- data/lib/clacky/tools/edit.rb +111 -8
- data/lib/clacky/tools/file_reader.rb +112 -9
- data/lib/clacky/tools/glob.rb +9 -2
- data/lib/clacky/tools/grep.rb +9 -14
- data/lib/clacky/tools/safe_shell.rb +14 -8
- data/lib/clacky/tools/shell.rb +89 -52
- data/lib/clacky/tools/web_fetch.rb +81 -18
- data/lib/clacky/ui2/components/command_suggestions.rb +273 -0
- data/lib/clacky/ui2/components/inline_input.rb +34 -15
- data/lib/clacky/ui2/components/input_area.rb +105 -83
- data/lib/clacky/ui2/layout_manager.rb +89 -33
- data/lib/clacky/ui2/line_editor.rb +142 -2
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +38 -47
- data/lib/clacky/utils/file_ignore_helper.rb +10 -12
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +4 -1
- metadata +6 -1
data/lib/clacky/cli.rb
CHANGED
|
@@ -62,6 +62,13 @@ module Clacky
|
|
|
62
62
|
end
|
|
63
63
|
config = Clacky::Config.load
|
|
64
64
|
|
|
65
|
+
# Show message when using ClaudeCode environment variables
|
|
66
|
+
if config.config_source == "claude_code"
|
|
67
|
+
say "š Using API key from ClaudeCode environment variables", :cyan
|
|
68
|
+
say " (#{config.base_url})", :white
|
|
69
|
+
say ""
|
|
70
|
+
end
|
|
71
|
+
|
|
65
72
|
unless config.api_key
|
|
66
73
|
say "Error: API key not found. Please run 'clacky config set' first.", :red
|
|
67
74
|
exit 1
|
|
@@ -251,6 +258,338 @@ module Clacky
|
|
|
251
258
|
say "https://www.anthropic.com/pricing\n\n", :blue
|
|
252
259
|
end
|
|
253
260
|
|
|
261
|
+
desc "skills", "Manage and list skills"
|
|
262
|
+
long_desc <<-LONGDESC
|
|
263
|
+
Manage and list skills that extend Claude's capabilities.
|
|
264
|
+
|
|
265
|
+
Skills are reusable prompts with YAML frontmatter that define
|
|
266
|
+
when and how Claude should use them. Skills can be invoked
|
|
267
|
+
directly with /skill-name or loaded automatically based on context.
|
|
268
|
+
|
|
269
|
+
Skill locations (in priority order):
|
|
270
|
+
- .clacky/skills/ (project, highest priority)
|
|
271
|
+
- ~/.clacky/skills/ (user global)
|
|
272
|
+
- .claude/skills/ (project, compatibility)
|
|
273
|
+
- ~/.claude/skills/ (user global, compatibility)
|
|
274
|
+
|
|
275
|
+
Subcommands:
|
|
276
|
+
list - List all available skills
|
|
277
|
+
show <name> - Show details of a specific skill
|
|
278
|
+
|
|
279
|
+
Examples:
|
|
280
|
+
$ clacky skills list
|
|
281
|
+
$ clacky skills show explain-code
|
|
282
|
+
LONGDESC
|
|
283
|
+
subcommand_option_names = []
|
|
284
|
+
|
|
285
|
+
# Main skills command - delegates to subcommands or shows help
|
|
286
|
+
def skills(*args)
|
|
287
|
+
if args.empty?
|
|
288
|
+
invoke :help, ["skills"]
|
|
289
|
+
else
|
|
290
|
+
subcommand = args.shift
|
|
291
|
+
case subcommand
|
|
292
|
+
when "list"
|
|
293
|
+
skills_list
|
|
294
|
+
when "show"
|
|
295
|
+
skills_show(args.first)
|
|
296
|
+
when "create"
|
|
297
|
+
# Parse options for create
|
|
298
|
+
name = args.first
|
|
299
|
+
opts = {}
|
|
300
|
+
i = 1
|
|
301
|
+
while i < args.length
|
|
302
|
+
if args[i] == "--description"
|
|
303
|
+
opts[:description] = args[i + 1]
|
|
304
|
+
i += 2
|
|
305
|
+
elsif args[i] == "--content"
|
|
306
|
+
opts[:content] = args[i + 1]
|
|
307
|
+
i += 2
|
|
308
|
+
elsif args[i] == "--project"
|
|
309
|
+
opts[:project] = true
|
|
310
|
+
i += 1
|
|
311
|
+
else
|
|
312
|
+
i += 1
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
skills_create_with_opts(name, opts)
|
|
316
|
+
when "delete"
|
|
317
|
+
skills_delete(args.first)
|
|
318
|
+
else
|
|
319
|
+
say "Unknown skill subcommand: #{subcommand}", :red
|
|
320
|
+
invoke :help, ["skills"]
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
desc "skills list", "List all available skills"
|
|
326
|
+
long_desc <<-LONGDESC
|
|
327
|
+
List all available skills from all configured locations:
|
|
328
|
+
- Project skills (.clacky/skills/)
|
|
329
|
+
- Global skills (~/.clacky/skills/)
|
|
330
|
+
- Compatible skills (.claude/skills/, ~/.claude/skills/)
|
|
331
|
+
|
|
332
|
+
Each skill shows:
|
|
333
|
+
- Name and slash command
|
|
334
|
+
- Description
|
|
335
|
+
- Whether it can be auto-invoked by Claude
|
|
336
|
+
- Whether it supports user invocation
|
|
337
|
+
LONGDESC
|
|
338
|
+
def skills_list
|
|
339
|
+
loader = Clacky::SkillLoader.new(Dir.pwd)
|
|
340
|
+
all_skills = loader.load_all
|
|
341
|
+
|
|
342
|
+
if all_skills.empty?
|
|
343
|
+
say "\nš No skills found.\n", :yellow
|
|
344
|
+
say "\nCreate your first skill:", :cyan
|
|
345
|
+
say " ~/.clacky/skills/<skill-name>/SKILL.md", :white
|
|
346
|
+
say " or .clacky/skills/<skill-name>/SKILL.md\n", :white
|
|
347
|
+
return
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
say "\nš Available Skills (#{all_skills.size})\n\n", :green
|
|
351
|
+
|
|
352
|
+
all_skills.each do |skill|
|
|
353
|
+
# Build status indicators
|
|
354
|
+
indicators = []
|
|
355
|
+
indicators << "š¤" if skill.model_invocation_allowed?
|
|
356
|
+
indicators << "š¤" if skill.user_invocable?
|
|
357
|
+
indicators << "š" if skill.forked_context?
|
|
358
|
+
|
|
359
|
+
say " /#{skill.identifier}", :cyan
|
|
360
|
+
say " #{indicators.join(' ')}" unless indicators.empty?
|
|
361
|
+
say "\n"
|
|
362
|
+
|
|
363
|
+
# Show description (truncated if too long)
|
|
364
|
+
desc = skill.context_description
|
|
365
|
+
if desc.length > 60
|
|
366
|
+
desc = desc[0..57] + "..."
|
|
367
|
+
end
|
|
368
|
+
say " #{desc}\n", :white
|
|
369
|
+
|
|
370
|
+
# Show location with priority indicator
|
|
371
|
+
location = case loader.loaded_from[skill.identifier]
|
|
372
|
+
when :default
|
|
373
|
+
"built-in"
|
|
374
|
+
when :project_clacky
|
|
375
|
+
"project .clacky"
|
|
376
|
+
when :project_claude
|
|
377
|
+
"project .claude (compat)"
|
|
378
|
+
when :global_clacky
|
|
379
|
+
"global .clacky"
|
|
380
|
+
when :global_claude
|
|
381
|
+
"global .claude (compat)"
|
|
382
|
+
else
|
|
383
|
+
"unknown"
|
|
384
|
+
end
|
|
385
|
+
say " [#{location}]\n", :yellow
|
|
386
|
+
|
|
387
|
+
say "\n"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Show errors if any
|
|
391
|
+
if loader.errors.any?
|
|
392
|
+
say "\nā ļø Warnings:\n", :yellow
|
|
393
|
+
loader.errors.each do |error|
|
|
394
|
+
say " - #{error}\n", :red
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
desc "skills show NAME", "Show details of a specific skill"
|
|
400
|
+
long_desc <<-LONGDESC
|
|
401
|
+
Show the full content and metadata of a specific skill.
|
|
402
|
+
|
|
403
|
+
NAME is the skill name (without the leading /).
|
|
404
|
+
|
|
405
|
+
Examples:
|
|
406
|
+
$ clacky skills show explain-code
|
|
407
|
+
LONGDESC
|
|
408
|
+
def skills_show(name = nil)
|
|
409
|
+
unless name
|
|
410
|
+
say "Error: Skill name required.\n", :red
|
|
411
|
+
say "Usage: clacky skills show <name>\n", :yellow
|
|
412
|
+
exit 1
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
loader = Clacky::SkillLoader.new(Dir.pwd)
|
|
416
|
+
all_skills = loader.load_all
|
|
417
|
+
|
|
418
|
+
# Try to find the skill
|
|
419
|
+
skill = all_skills.find { |s| s.identifier == name }
|
|
420
|
+
|
|
421
|
+
unless skill
|
|
422
|
+
# Try prefix matching
|
|
423
|
+
matching = all_skills.select { |s| s.identifier.start_with?(name) }
|
|
424
|
+
if matching.size == 1
|
|
425
|
+
skill = matching.first
|
|
426
|
+
else
|
|
427
|
+
say "\nā Skill '#{name}' not found.\n", :red
|
|
428
|
+
say "\nAvailable skills:\n", :yellow
|
|
429
|
+
all_skills.each { |s| say " /#{s.identifier}\n", :cyan }
|
|
430
|
+
exit 1
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Display skill details
|
|
435
|
+
say "\nš Skill: /#{skill.identifier}\n\n", :green
|
|
436
|
+
|
|
437
|
+
say "Description:\n", :yellow
|
|
438
|
+
say " #{skill.context_description}\n\n", :white
|
|
439
|
+
|
|
440
|
+
say "Status:\n", :yellow
|
|
441
|
+
say " Auto-invokable: #{skill.model_invocation_allowed? ? 'Yes' : 'No'}\n", :white
|
|
442
|
+
say " User-invokable: #{skill.user_invocable? ? 'Yes' : 'No'}\n", :white
|
|
443
|
+
say " Forked context: #{skill.forked_context? ? 'Yes' : 'No'}\n", :white
|
|
444
|
+
|
|
445
|
+
if skill.allowed_tools
|
|
446
|
+
say " Allowed tools: #{skill.allowed_tools.join(', ')}\n", :white
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
say "\nLocation: #{skill.source_path}\n\n", :yellow
|
|
450
|
+
|
|
451
|
+
say "Content:\n", :yellow
|
|
452
|
+
say "-" * 60 + "\n", :white
|
|
453
|
+
say skill.content, :white
|
|
454
|
+
say "\n" + "-" * 60 + "\n", :white
|
|
455
|
+
|
|
456
|
+
# Show supporting files if any
|
|
457
|
+
if skill.has_supporting_files?
|
|
458
|
+
say "\nSupporting files:\n", :yellow
|
|
459
|
+
skill.supporting_files.each do |file|
|
|
460
|
+
say " - #{file.relative_path_from(Pathname.new(Dir.pwd))}\n", :cyan
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
desc "skills create NAME", "Create a new skill"
|
|
466
|
+
long_desc <<-LONGDESC
|
|
467
|
+
Create a new skill in the global skills directory.
|
|
468
|
+
|
|
469
|
+
NAME is the skill name (lowercase letters, numbers, and hyphens only).
|
|
470
|
+
|
|
471
|
+
This creates a new directory at ~/.clacky/skills/NAME/SKILL.md
|
|
472
|
+
with a template skill file.
|
|
473
|
+
|
|
474
|
+
Options:
|
|
475
|
+
--description Set the skill description
|
|
476
|
+
--content Set the skill content (use - for stdin)
|
|
477
|
+
--project Create in project .clacky/skills/ instead
|
|
478
|
+
|
|
479
|
+
Examples:
|
|
480
|
+
$ clacky skills create explain-code --description "Explain code with diagrams"
|
|
481
|
+
$ clacky skills create deploy --description "Deploy application" --project
|
|
482
|
+
LONGDESC
|
|
483
|
+
option :description, type: :string, desc: "Skill description"
|
|
484
|
+
option :content, type: :string, desc: "Skill content (use - for stdin)"
|
|
485
|
+
option :project, type: :boolean, desc: "Create in project directory"
|
|
486
|
+
def skills_create(name = nil)
|
|
487
|
+
unless name
|
|
488
|
+
say "Error: Skill name required.\n", :red
|
|
489
|
+
say "Usage: clacky skills create <name>\n", :yellow
|
|
490
|
+
exit 1
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Validate name
|
|
494
|
+
unless name.match?(/^[a-z0-9][a-z0-9-]*$/)
|
|
495
|
+
say "Error: Invalid skill name '#{name}'.\n", :red
|
|
496
|
+
say "Use lowercase letters, numbers, and hyphens only.\n", :yellow
|
|
497
|
+
exit 1
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Get description
|
|
501
|
+
description = options[:description] || ask("Skill description: ").to_s
|
|
502
|
+
|
|
503
|
+
# Get content
|
|
504
|
+
if options[:content] == "-"
|
|
505
|
+
say "Enter skill content (end with Ctrl+D):\n", :yellow
|
|
506
|
+
content = STDIN.read
|
|
507
|
+
elsif options[:content]
|
|
508
|
+
content = options[:content]
|
|
509
|
+
else
|
|
510
|
+
content = "Describe the skill here..."
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Determine location
|
|
514
|
+
location = options[:project] ? :project : :global
|
|
515
|
+
|
|
516
|
+
# Create the skill
|
|
517
|
+
loader = Clacky::SkillLoader.new(Dir.pwd)
|
|
518
|
+
skill = loader.create_skill(name, content, description, location: location)
|
|
519
|
+
|
|
520
|
+
skill_path = skill.directory
|
|
521
|
+
say "\nā
Skill created at: #{skill_path}\n", :green
|
|
522
|
+
say "\nYou can invoke it with: /#{name}\n", :cyan
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Helper method for skills command dispatcher
|
|
526
|
+
no_commands do
|
|
527
|
+
def skills_create_with_opts(name, opts = {})
|
|
528
|
+
unless name
|
|
529
|
+
say "Error: Skill name required.\n", :red
|
|
530
|
+
say "Usage: clacky skills create <name>\n", :yellow
|
|
531
|
+
exit 1
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Validate name
|
|
535
|
+
unless name.match?(/^[a-z0-9][a-z0-9-]*$/)
|
|
536
|
+
say "Error: Invalid skill name '#{name}'.\n", :red
|
|
537
|
+
say "Use lowercase letters, numbers, and hyphens only.\n", :yellow
|
|
538
|
+
exit 1
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
description = opts[:description] || ask("Skill description: ").to_s
|
|
542
|
+
content = opts[:content] || "Describe the skill here..."
|
|
543
|
+
location = opts[:project] ? :project : :global
|
|
544
|
+
|
|
545
|
+
loader = Clacky::SkillLoader.new(Dir.pwd)
|
|
546
|
+
skill = loader.create_skill(name, content, description, location: location)
|
|
547
|
+
|
|
548
|
+
skill_path = skill.directory
|
|
549
|
+
say "\nā
Skill created at: #{skill_path}\n", :green
|
|
550
|
+
say "\nYou can invoke it with: /#{name}\n", :cyan
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
desc "skills delete NAME", "Delete a skill"
|
|
555
|
+
long_desc <<-LONGDESC
|
|
556
|
+
Delete a skill by name.
|
|
557
|
+
|
|
558
|
+
NAME is the skill name (without the leading /).
|
|
559
|
+
|
|
560
|
+
Examples:
|
|
561
|
+
$ clacky skills delete explain-code
|
|
562
|
+
LONGDESC
|
|
563
|
+
def skills_delete(name = nil)
|
|
564
|
+
unless name
|
|
565
|
+
say "Error: Skill name required.\n", :red
|
|
566
|
+
say "Usage: clacky skills delete <name>\n", :yellow
|
|
567
|
+
exit 1
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
loader = Clacky::SkillLoader.new(Dir.pwd)
|
|
571
|
+
all_skills = loader.load_all
|
|
572
|
+
|
|
573
|
+
# Find the skill
|
|
574
|
+
skill = all_skills.find { |s| s.identifier == name }
|
|
575
|
+
|
|
576
|
+
unless skill
|
|
577
|
+
say "Error: Skill '#{name}' not found.\n", :red
|
|
578
|
+
exit 1
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Confirm deletion
|
|
582
|
+
prompt = TTY::Prompt.new
|
|
583
|
+
unless prompt.yes?("Delete skill '/#{name}' at #{skill.directory}?")
|
|
584
|
+
say "Cancelled.\n", :yellow
|
|
585
|
+
exit 0
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Delete the skill
|
|
589
|
+
loader.delete_skill(name)
|
|
590
|
+
say "\nā
Skill '/#{name}' deleted.\n", :green
|
|
591
|
+
end
|
|
592
|
+
|
|
254
593
|
no_commands do
|
|
255
594
|
def build_agent_config(config)
|
|
256
595
|
AgentConfig.new(
|
|
@@ -319,7 +658,8 @@ module Clacky
|
|
|
319
658
|
end
|
|
320
659
|
|
|
321
660
|
def load_session_by_number(client, agent_config, session_manager, working_dir, identifier)
|
|
322
|
-
|
|
661
|
+
# Get a larger list to search through (for ID prefix matching)
|
|
662
|
+
sessions = session_manager.list(current_dir: working_dir, limit: 100)
|
|
323
663
|
|
|
324
664
|
if sessions.empty?
|
|
325
665
|
say "No sessions found.", :yellow
|
|
@@ -329,7 +669,8 @@ module Clacky
|
|
|
329
669
|
session_data = nil
|
|
330
670
|
|
|
331
671
|
# Check if identifier is a number (index-based)
|
|
332
|
-
|
|
672
|
+
# Heuristic: If it's a small number (1-99), treat as index; otherwise treat as session ID prefix
|
|
673
|
+
if identifier.match?(/^\d+$/) && identifier.to_i <= 99
|
|
333
674
|
index = identifier.to_i - 1
|
|
334
675
|
if index < 0 || index >= sessions.size
|
|
335
676
|
say "Invalid session number. Use -l to list available sessions.", :red
|
|
@@ -399,6 +740,9 @@ module Clacky
|
|
|
399
740
|
# Inject UI into agent
|
|
400
741
|
agent.instance_variable_set(:@ui, ui_controller)
|
|
401
742
|
|
|
743
|
+
# Set skill loader for command suggestions
|
|
744
|
+
ui_controller.set_skill_loader(agent.skill_loader)
|
|
745
|
+
|
|
402
746
|
# Track agent thread state
|
|
403
747
|
agent_thread = nil
|
|
404
748
|
|
|
@@ -413,14 +757,14 @@ module Clacky
|
|
|
413
757
|
# Save final session state before exit
|
|
414
758
|
if session_manager && agent.total_tasks > 0
|
|
415
759
|
session_data = agent.to_session_data(status: :exited)
|
|
416
|
-
session_manager.save(session_data)
|
|
760
|
+
saved_path = session_manager.save(session_data)
|
|
417
761
|
|
|
418
762
|
# Show session saved message in output area (before stopping UI)
|
|
419
763
|
session_id = session_data[:session_id][0..7]
|
|
420
764
|
ui_controller.append_output("")
|
|
421
765
|
ui_controller.append_output("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
422
766
|
ui_controller.append_output("")
|
|
423
|
-
ui_controller.append_output("Session saved: #{
|
|
767
|
+
ui_controller.append_output("Session saved: #{saved_path}")
|
|
424
768
|
ui_controller.append_output("Tasks completed: #{agent.total_tasks}")
|
|
425
769
|
ui_controller.append_output("Total cost: $#{agent.total_cost.round(4)}")
|
|
426
770
|
ui_controller.append_output("")
|
|
@@ -497,6 +841,8 @@ module Clacky
|
|
|
497
841
|
if is_session_load
|
|
498
842
|
recent_user_messages = agent.get_recent_user_messages(limit: 5)
|
|
499
843
|
ui_controller.initialize_and_show_banner(recent_user_messages: recent_user_messages)
|
|
844
|
+
# Update session bar with restored agent stats
|
|
845
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
500
846
|
else
|
|
501
847
|
ui_controller.initialize_and_show_banner
|
|
502
848
|
end
|
|
@@ -529,11 +875,6 @@ module Clacky
|
|
|
529
875
|
if session_manager && agent.total_tasks > 0
|
|
530
876
|
session_manager.save(agent.to_session_data)
|
|
531
877
|
end
|
|
532
|
-
|
|
533
|
-
# Show goodbye message
|
|
534
|
-
say "\nš Goodbye! Session stats:", :green
|
|
535
|
-
say " Tasks completed: #{agent.total_tasks}", :cyan
|
|
536
|
-
say " Total cost: $#{agent.total_cost.round(4)}", :cyan
|
|
537
878
|
end
|
|
538
879
|
|
|
539
880
|
end
|