lib/clacky/default_skills/channel-manager/import_lark_skills.rb



# frozen_string_literal: true

require 'fileutils'
require 'pathname'

# Import lark-cli's official Skills from ~/.agents/skills/lark-* into
# ~/.clacky/skills/lark-imports/<name>/.
#
# Background:
#   lark-cli ships ~24 SKILL.md files (lark-doc, lark-sheets, lark-base, ...)
#   that teach the agent how to use `lark-cli`. They are normally installed
#   under ~/.agents/skills/lark-*, which openclacky's SkillLoader does NOT
#   scan. This importer copies them into ~/.clacky/skills/lark-imports/ so
#   they become discoverable via the standard skill description-matching
#   mechanism.
#
# This is intentionally a small, dedicated importer (not a generic external
# skills tool) — it only handles the lark-cli case for the feishu channel
# setup flow. Failures are non-fatal: the bot itself remains functional even
# if Skills cannot be exposed.
#
# Usage:
#   importer = Clacky::ChannelSetup::LarkSkillsImporter.new
#   result = importer.run
#   # result => { copied: 24, skipped: 0, errors: [] }

module Clacky
  module ChannelSetup
    class LarkSkillsImporter
      DEFAULT_SOURCE_DIR = File.join(Dir.home, '.agents', 'skills')
      DEFAULT_TARGET_DIR = File.join(Dir.home, '.clacky', 'skills', 'lark-imports')
      SKILL_PREFIX       = 'lark-'

      # @param source_dir [String] directory containing lark-cli installed skills
      # @param target_dir [String] destination under ~/.clacky/skills/
      def initialize(source_dir: DEFAULT_SOURCE_DIR, target_dir: DEFAULT_TARGET_DIR)
        @source_dir = Pathname.new(source_dir).expand_path
        @target_dir = Pathname.new(target_dir).expand_path
      end

      # Run the import. Returns a result hash; never raises on per-skill errors.
      # @return [Hash] { copied: Integer, skipped: Integer, errors: Array<String> }
      def run
        return { copied: 0, skipped: 0, errors: ["source not found: #{@source_dir}"] } unless @source_dir.directory?

        skill_dirs = discover_lark_skills
        return { copied: 0, skipped: 0, errors: [] } if skill_dirs.empty?

        FileUtils.mkdir_p(@target_dir)

        copied = 0
        errors = []
        skill_dirs.each do |src|
          begin
            copy_skill(src)
            copied += 1
          rescue StandardError => e
            errors << "#{src.basename}: #{e.message}"
          end
        end

        { copied: copied, skipped: 0, errors: errors }
      end

      # Discover candidate lark-* skill directories under @source_dir.
      # A directory qualifies when it (a) starts with "lark-" and (b) contains a SKILL.md.
      # @return [Array<Pathname>]
      private def discover_lark_skills
        @source_dir.children
                   .select { |p| p.directory? && p.basename.to_s.start_with?(SKILL_PREFIX) }
                   .select { |p| p.join('SKILL.md').exist? }
                   .sort_by { |p| p.basename.to_s }
      end

      # Copy a single skill directory into @target_dir, replacing any existing copy
      # so re-runs always reflect the latest version.
      # @param src [Pathname]
      private def copy_skill(src)
        dst = @target_dir.join(src.basename.to_s)
        FileUtils.rm_rf(dst) if dst.exist?
        FileUtils.mkdir_p(dst)
        src.children.each { |child| FileUtils.cp_r(child, dst) }
      end
    end
  end
end

# CLI entry point — invoked by SKILL.md after the user opts in to lark-cli.
# Usage:
#   ruby import_lark_skills.rb
# Prints a one-line summary; exits 0 even when nothing to copy (treat empty
# source as a soft skip — the script may run before `npx skills add`).
if $PROGRAM_NAME == __FILE__
  result = Clacky::ChannelSetup::LarkSkillsImporter.new.run
  puts "[lark-import] copied=#{result[:copied]} errors=#{result[:errors].size}"
  result[:errors].each { |e| warn "[lark-import] #{e}" }
end