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.
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
- sessions = session_manager.list(current_dir: working_dir, limit: 10)
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
- if identifier.match?(/^\d+$/)
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: #{session_id}")
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