openclacky 0.6.1 → 0.6.3

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
@@ -32,6 +32,10 @@ module Clacky
32
32
  confirm_edits - Auto-approve read-only tools, confirm edits
33
33
  plan_only - Generate plan without executing
34
34
 
35
+ UI themes:
36
+ hacker - Matrix/hacker-style with bracket symbols (default)
37
+ minimal - Clean, simple symbols
38
+
35
39
  Session management:
36
40
  -c, --continue - Continue the most recent session for this directory
37
41
  -l, --list - List recent sessions
@@ -42,6 +46,8 @@ module Clacky
42
46
  LONGDESC
43
47
  option :mode, type: :string, default: "confirm_safes",
44
48
  desc: "Permission mode: auto_approve, confirm_safes, confirm_edits, plan_only"
49
+ option :theme, type: :string, default: "hacker",
50
+ desc: "UI theme: hacker, minimal (default: hacker)"
45
51
  option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Show detailed output"
46
52
  option :path, type: :string, desc: "Project directory path (defaults to current directory)"
47
53
  option :continue, type: :boolean, aliases: "-c", desc: "Continue most recent session"
@@ -127,6 +133,80 @@ module Clacky
127
133
  end
128
134
  end
129
135
 
136
+ desc "new PROJECT_NAME", "Create a new Rails project from the official template"
137
+ long_desc <<-LONGDESC
138
+ Create a new Rails project from the official template.
139
+
140
+ This command will:
141
+ 1. Clone the template from git@github.com:clacky-ai/rails-template-7x-starter.git
142
+ 2. Change into the project directory
143
+ 3. Run bin/setup to install dependencies and configure the project
144
+
145
+ Example:
146
+ $ clacky new my_rails_app
147
+ LONGDESC
148
+ def new(project_name = nil)
149
+ unless project_name
150
+ say "Error: Project name is required.", :red
151
+ say "Usage: clacky new <project_name>", :yellow
152
+ exit 1
153
+ end
154
+
155
+ # Validate project name
156
+ unless project_name.match?(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
157
+ say "Error: Invalid project name. Use only letters, numbers, underscores, and hyphens.", :red
158
+ exit 1
159
+ end
160
+
161
+ template_repo = "git@github.com:clacky-ai/rails-template-7x-starter.git"
162
+ current_dir = Dir.pwd
163
+ target_dir = File.join(current_dir, project_name)
164
+
165
+ # Check if target directory already exists
166
+ if Dir.exist?(target_dir)
167
+ say "Error: Directory '#{project_name}' already exists.", :red
168
+ exit 1
169
+ end
170
+
171
+ say "Creating new Rails project: #{project_name}", :green
172
+
173
+ # Clone the template repository
174
+ say "\nšŸ“¦ Cloning template repository...", :cyan
175
+ clone_command = "git clone #{template_repo} #{project_name}"
176
+
177
+ clone_result = system(clone_command)
178
+
179
+ unless clone_result
180
+ say "\nāŒ Failed to clone repository. Please check your git configuration and network connection.", :red
181
+ exit 1
182
+ end
183
+
184
+ say "āœ“ Repository cloned successfully", :green
185
+
186
+ # Run bin/setup
187
+ say "\nāš™ļø Running bin/setup...", :cyan
188
+
189
+ Dir.chdir(target_dir)
190
+
191
+ setup_command = "./bin/setup"
192
+
193
+ setup_result = system(setup_command)
194
+
195
+ Dir.chdir(current_dir)
196
+
197
+ unless setup_result
198
+ say "\nāŒ Failed to run bin/setup. Please check the setup script for errors.", :red
199
+ say "You can try running it manually:", :yellow
200
+ say " cd #{project_name} && ./bin/setup", :cyan
201
+ exit 1
202
+ end
203
+
204
+ say "\nāœ… Project '#{project_name}' created successfully!", :green
205
+ say "\nNext steps:", :green
206
+ say " cd #{project_name}", :cyan
207
+ say " clacky agent", :cyan
208
+ end
209
+
130
210
  desc "price", "Show pricing information for AI models"
131
211
  def price
132
212
  say "\nšŸ’° Model Pricing Information\n\n", :green
@@ -171,6 +251,338 @@ module Clacky
171
251
  say "https://www.anthropic.com/pricing\n\n", :blue
172
252
  end
173
253
 
254
+ desc "skills", "Manage and list skills"
255
+ long_desc <<-LONGDESC
256
+ Manage and list skills that extend Claude's capabilities.
257
+
258
+ Skills are reusable prompts with YAML frontmatter that define
259
+ when and how Claude should use them. Skills can be invoked
260
+ directly with /skill-name or loaded automatically based on context.
261
+
262
+ Skill locations (in priority order):
263
+ - .clacky/skills/ (project, highest priority)
264
+ - ~/.clacky/skills/ (user global)
265
+ - .claude/skills/ (project, compatibility)
266
+ - ~/.claude/skills/ (user global, compatibility)
267
+
268
+ Subcommands:
269
+ list - List all available skills
270
+ show <name> - Show details of a specific skill
271
+
272
+ Examples:
273
+ $ clacky skills list
274
+ $ clacky skills show explain-code
275
+ LONGDESC
276
+ subcommand_option_names = []
277
+
278
+ # Main skills command - delegates to subcommands or shows help
279
+ def skills(*args)
280
+ if args.empty?
281
+ invoke :help, ["skills"]
282
+ else
283
+ subcommand = args.shift
284
+ case subcommand
285
+ when "list"
286
+ skills_list
287
+ when "show"
288
+ skills_show(args.first)
289
+ when "create"
290
+ # Parse options for create
291
+ name = args.first
292
+ opts = {}
293
+ i = 1
294
+ while i < args.length
295
+ if args[i] == "--description"
296
+ opts[:description] = args[i + 1]
297
+ i += 2
298
+ elsif args[i] == "--content"
299
+ opts[:content] = args[i + 1]
300
+ i += 2
301
+ elsif args[i] == "--project"
302
+ opts[:project] = true
303
+ i += 1
304
+ else
305
+ i += 1
306
+ end
307
+ end
308
+ skills_create_with_opts(name, opts)
309
+ when "delete"
310
+ skills_delete(args.first)
311
+ else
312
+ say "Unknown skill subcommand: #{subcommand}", :red
313
+ invoke :help, ["skills"]
314
+ end
315
+ end
316
+ end
317
+
318
+ desc "skills list", "List all available skills"
319
+ long_desc <<-LONGDESC
320
+ List all available skills from all configured locations:
321
+ - Project skills (.clacky/skills/)
322
+ - Global skills (~/.clacky/skills/)
323
+ - Compatible skills (.claude/skills/, ~/.claude/skills/)
324
+
325
+ Each skill shows:
326
+ - Name and slash command
327
+ - Description
328
+ - Whether it can be auto-invoked by Claude
329
+ - Whether it supports user invocation
330
+ LONGDESC
331
+ def skills_list
332
+ loader = Clacky::SkillLoader.new(Dir.pwd)
333
+ all_skills = loader.load_all
334
+
335
+ if all_skills.empty?
336
+ say "\nšŸ“š No skills found.\n", :yellow
337
+ say "\nCreate your first skill:", :cyan
338
+ say " ~/.clacky/skills/<skill-name>/SKILL.md", :white
339
+ say " or .clacky/skills/<skill-name>/SKILL.md\n", :white
340
+ return
341
+ end
342
+
343
+ say "\nšŸ“š Available Skills (#{all_skills.size})\n\n", :green
344
+
345
+ all_skills.each do |skill|
346
+ # Build status indicators
347
+ indicators = []
348
+ indicators << "šŸ¤–" if skill.model_invocation_allowed?
349
+ indicators << "šŸ‘¤" if skill.user_invocable?
350
+ indicators << "šŸ”€" if skill.forked_context?
351
+
352
+ say " /#{skill.identifier}", :cyan
353
+ say " #{indicators.join(' ')}" unless indicators.empty?
354
+ say "\n"
355
+
356
+ # Show description (truncated if too long)
357
+ desc = skill.context_description
358
+ if desc.length > 60
359
+ desc = desc[0..57] + "..."
360
+ end
361
+ say " #{desc}\n", :white
362
+
363
+ # Show location with priority indicator
364
+ location = case loader.loaded_from[skill.identifier]
365
+ when :default
366
+ "built-in"
367
+ when :project_clacky
368
+ "project .clacky"
369
+ when :project_claude
370
+ "project .claude (compat)"
371
+ when :global_clacky
372
+ "global .clacky"
373
+ when :global_claude
374
+ "global .claude (compat)"
375
+ else
376
+ "unknown"
377
+ end
378
+ say " [#{location}]\n", :yellow
379
+
380
+ say "\n"
381
+ end
382
+
383
+ # Show errors if any
384
+ if loader.errors.any?
385
+ say "\nāš ļø Warnings:\n", :yellow
386
+ loader.errors.each do |error|
387
+ say " - #{error}\n", :red
388
+ end
389
+ end
390
+ end
391
+
392
+ desc "skills show NAME", "Show details of a specific skill"
393
+ long_desc <<-LONGDESC
394
+ Show the full content and metadata of a specific skill.
395
+
396
+ NAME is the skill name (without the leading /).
397
+
398
+ Examples:
399
+ $ clacky skills show explain-code
400
+ LONGDESC
401
+ def skills_show(name = nil)
402
+ unless name
403
+ say "Error: Skill name required.\n", :red
404
+ say "Usage: clacky skills show <name>\n", :yellow
405
+ exit 1
406
+ end
407
+
408
+ loader = Clacky::SkillLoader.new(Dir.pwd)
409
+ all_skills = loader.load_all
410
+
411
+ # Try to find the skill
412
+ skill = all_skills.find { |s| s.identifier == name }
413
+
414
+ unless skill
415
+ # Try prefix matching
416
+ matching = all_skills.select { |s| s.identifier.start_with?(name) }
417
+ if matching.size == 1
418
+ skill = matching.first
419
+ else
420
+ say "\nāŒ Skill '#{name}' not found.\n", :red
421
+ say "\nAvailable skills:\n", :yellow
422
+ all_skills.each { |s| say " /#{s.identifier}\n", :cyan }
423
+ exit 1
424
+ end
425
+ end
426
+
427
+ # Display skill details
428
+ say "\nšŸ“– Skill: /#{skill.identifier}\n\n", :green
429
+
430
+ say "Description:\n", :yellow
431
+ say " #{skill.context_description}\n\n", :white
432
+
433
+ say "Status:\n", :yellow
434
+ say " Auto-invokable: #{skill.model_invocation_allowed? ? 'Yes' : 'No'}\n", :white
435
+ say " User-invokable: #{skill.user_invocable? ? 'Yes' : 'No'}\n", :white
436
+ say " Forked context: #{skill.forked_context? ? 'Yes' : 'No'}\n", :white
437
+
438
+ if skill.allowed_tools
439
+ say " Allowed tools: #{skill.allowed_tools.join(', ')}\n", :white
440
+ end
441
+
442
+ say "\nLocation: #{skill.source_path}\n\n", :yellow
443
+
444
+ say "Content:\n", :yellow
445
+ say "-" * 60 + "\n", :white
446
+ say skill.content, :white
447
+ say "\n" + "-" * 60 + "\n", :white
448
+
449
+ # Show supporting files if any
450
+ if skill.has_supporting_files?
451
+ say "\nSupporting files:\n", :yellow
452
+ skill.supporting_files.each do |file|
453
+ say " - #{file.relative_path_from(Pathname.new(Dir.pwd))}\n", :cyan
454
+ end
455
+ end
456
+ end
457
+
458
+ desc "skills create NAME", "Create a new skill"
459
+ long_desc <<-LONGDESC
460
+ Create a new skill in the global skills directory.
461
+
462
+ NAME is the skill name (lowercase letters, numbers, and hyphens only).
463
+
464
+ This creates a new directory at ~/.clacky/skills/NAME/SKILL.md
465
+ with a template skill file.
466
+
467
+ Options:
468
+ --description Set the skill description
469
+ --content Set the skill content (use - for stdin)
470
+ --project Create in project .clacky/skills/ instead
471
+
472
+ Examples:
473
+ $ clacky skills create explain-code --description "Explain code with diagrams"
474
+ $ clacky skills create deploy --description "Deploy application" --project
475
+ LONGDESC
476
+ option :description, type: :string, desc: "Skill description"
477
+ option :content, type: :string, desc: "Skill content (use - for stdin)"
478
+ option :project, type: :boolean, desc: "Create in project directory"
479
+ def skills_create(name = nil)
480
+ unless name
481
+ say "Error: Skill name required.\n", :red
482
+ say "Usage: clacky skills create <name>\n", :yellow
483
+ exit 1
484
+ end
485
+
486
+ # Validate name
487
+ unless name.match?(/^[a-z0-9][a-z0-9-]*$/)
488
+ say "Error: Invalid skill name '#{name}'.\n", :red
489
+ say "Use lowercase letters, numbers, and hyphens only.\n", :yellow
490
+ exit 1
491
+ end
492
+
493
+ # Get description
494
+ description = options[:description] || ask("Skill description: ").to_s
495
+
496
+ # Get content
497
+ if options[:content] == "-"
498
+ say "Enter skill content (end with Ctrl+D):\n", :yellow
499
+ content = STDIN.read
500
+ elsif options[:content]
501
+ content = options[:content]
502
+ else
503
+ content = "Describe the skill here..."
504
+ end
505
+
506
+ # Determine location
507
+ location = options[:project] ? :project : :global
508
+
509
+ # Create the skill
510
+ loader = Clacky::SkillLoader.new(Dir.pwd)
511
+ skill = loader.create_skill(name, content, description, location: location)
512
+
513
+ skill_path = skill.directory
514
+ say "\nāœ… Skill created at: #{skill_path}\n", :green
515
+ say "\nYou can invoke it with: /#{name}\n", :cyan
516
+ end
517
+
518
+ # Helper method for skills command dispatcher
519
+ no_commands do
520
+ def skills_create_with_opts(name, opts = {})
521
+ unless name
522
+ say "Error: Skill name required.\n", :red
523
+ say "Usage: clacky skills create <name>\n", :yellow
524
+ exit 1
525
+ end
526
+
527
+ # Validate name
528
+ unless name.match?(/^[a-z0-9][a-z0-9-]*$/)
529
+ say "Error: Invalid skill name '#{name}'.\n", :red
530
+ say "Use lowercase letters, numbers, and hyphens only.\n", :yellow
531
+ exit 1
532
+ end
533
+
534
+ description = opts[:description] || ask("Skill description: ").to_s
535
+ content = opts[:content] || "Describe the skill here..."
536
+ location = opts[:project] ? :project : :global
537
+
538
+ loader = Clacky::SkillLoader.new(Dir.pwd)
539
+ skill = loader.create_skill(name, content, description, location: location)
540
+
541
+ skill_path = skill.directory
542
+ say "\nāœ… Skill created at: #{skill_path}\n", :green
543
+ say "\nYou can invoke it with: /#{name}\n", :cyan
544
+ end
545
+ end
546
+
547
+ desc "skills delete NAME", "Delete a skill"
548
+ long_desc <<-LONGDESC
549
+ Delete a skill by name.
550
+
551
+ NAME is the skill name (without the leading /).
552
+
553
+ Examples:
554
+ $ clacky skills delete explain-code
555
+ LONGDESC
556
+ def skills_delete(name = nil)
557
+ unless name
558
+ say "Error: Skill name required.\n", :red
559
+ say "Usage: clacky skills delete <name>\n", :yellow
560
+ exit 1
561
+ end
562
+
563
+ loader = Clacky::SkillLoader.new(Dir.pwd)
564
+ all_skills = loader.load_all
565
+
566
+ # Find the skill
567
+ skill = all_skills.find { |s| s.identifier == name }
568
+
569
+ unless skill
570
+ say "Error: Skill '#{name}' not found.\n", :red
571
+ exit 1
572
+ end
573
+
574
+ # Confirm deletion
575
+ prompt = TTY::Prompt.new
576
+ unless prompt.yes?("Delete skill '/#{name}' at #{skill.directory}?")
577
+ say "Cancelled.\n", :yellow
578
+ exit 0
579
+ end
580
+
581
+ # Delete the skill
582
+ loader.delete_skill(name)
583
+ say "\nāœ… Skill '/#{name}' deleted.\n", :green
584
+ end
585
+
174
586
  no_commands do
175
587
  def build_agent_config(config)
176
588
  AgentConfig.new(
@@ -239,7 +651,8 @@ module Clacky
239
651
  end
240
652
 
241
653
  def load_session_by_number(client, agent_config, session_manager, working_dir, identifier)
242
- sessions = session_manager.list(current_dir: working_dir, limit: 10)
654
+ # Get a larger list to search through (for ID prefix matching)
655
+ sessions = session_manager.list(current_dir: working_dir, limit: 100)
243
656
 
244
657
  if sessions.empty?
245
658
  say "No sessions found.", :yellow
@@ -249,7 +662,8 @@ module Clacky
249
662
  session_data = nil
250
663
 
251
664
  # Check if identifier is a number (index-based)
252
- if identifier.match?(/^\d+$/)
665
+ # Heuristic: If it's a small number (1-99), treat as index; otherwise treat as session ID prefix
666
+ if identifier.match?(/^\d+$/) && identifier.to_i <= 99
253
667
  index = identifier.to_i - 1
254
668
  if index < 0 || index >= sessions.size
255
669
  say "Invalid session number. Use -l to list available sessions.", :red
@@ -300,16 +714,28 @@ module Clacky
300
714
 
301
715
  # Run agent with UI2 split-screen interface
302
716
  def run_agent_with_ui2(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil, is_session_load: false)
717
+ # Validate theme
718
+ theme_name = options[:theme] || "hacker"
719
+ available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
720
+ unless available_themes.include?(theme_name)
721
+ say "Error: Unknown theme '#{theme_name}'. Available themes: #{available_themes.join(', ')}", :red
722
+ exit 1
723
+ end
724
+
303
725
  # Create UI2 controller with configuration
304
726
  ui_controller = UI2::UIController.new(
305
727
  working_dir: working_dir,
306
728
  mode: agent_config.permission_mode.to_s,
307
- model: agent_config.model
729
+ model: agent_config.model,
730
+ theme: theme_name
308
731
  )
309
732
 
310
733
  # Inject UI into agent
311
734
  agent.instance_variable_set(:@ui, ui_controller)
312
735
 
736
+ # Set skill loader for command suggestions
737
+ ui_controller.set_skill_loader(agent.skill_loader)
738
+
313
739
  # Track agent thread state
314
740
  agent_thread = nil
315
741
 
@@ -408,6 +834,8 @@ module Clacky
408
834
  if is_session_load
409
835
  recent_user_messages = agent.get_recent_user_messages(limit: 5)
410
836
  ui_controller.initialize_and_show_banner(recent_user_messages: recent_user_messages)
837
+ # Update session bar with restored agent stats
838
+ ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
411
839
  else
412
840
  ui_controller.initialize_and_show_banner
413
841
  end
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: skill-add
3
+ description: Guide for creating new SKILL.md files
4
+ disable-model-invocation: false
5
+ user-invocable: true
6
+ ---
7
+
8
+ # Skill Creation Guide
9
+
10
+ ## SKILL.md Structure
11
+
12
+ ### 1. Front Matter (Required)
13
+ ```yaml
14
+ ---
15
+ name: skill-name
16
+ description: Brief one-line description
17
+ disable-model-invocation: false
18
+ user-invocable: true
19
+ ---
20
+ ```
21
+
22
+ ### 2. Main Content
23
+ ```markdown
24
+ # Skill Title
25
+
26
+ ## Usage
27
+ How to invoke: "command description" or `/skill-name`
28
+
29
+ ## Process Steps
30
+
31
+ ### 1. First Step
32
+ What to do
33
+
34
+ ### 2. Next Step
35
+ Continue the task
36
+
37
+ ## Commands Used
38
+ ```bash
39
+ # Key commands
40
+ ```
41
+
42
+ ## Notes
43
+ - Important points
44
+ ```
45
+
46
+ ## File Location
47
+ `.clacky/skills/{skill-name}/SKILL.md`
48
+
49
+ ## Minimal Example
50
+ ```markdown
51
+ ---
52
+ name: hello
53
+ description: Simple greeting
54
+ disable-model-invocation: false
55
+ user-invocable: true
56
+ ---
57
+
58
+ # Hello Skill
59
+
60
+ ## Usage
61
+ Say "hello" or `/hello`
62
+
63
+ ## Process Steps
64
+ ### 1. Greet user
65
+ ### 2. Offer help
66
+ ```