caruso 0.5.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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +146 -0
- data/CHANGELOG.md +213 -0
- data/CLAUDE.md +276 -0
- data/IMPROVEMENTS.md +337 -0
- data/LICENSE.txt +21 -0
- data/README.md +326 -0
- data/Rakefile +71 -0
- data/bin/caruso +6 -0
- data/caruso.gemspec +43 -0
- data/lib/caruso/adapter.rb +110 -0
- data/lib/caruso/cli.rb +532 -0
- data/lib/caruso/config_manager.rb +190 -0
- data/lib/caruso/fetcher.rb +248 -0
- data/lib/caruso/marketplace_registry.rb +102 -0
- data/lib/caruso/version.rb +5 -0
- data/lib/caruso.rb +22 -0
- data/reference/marketplace.md +433 -0
- data/reference/plugins.md +391 -0
- data/reference/plugins_reference.md +376 -0
- metadata +176 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
7
|
+
t.rspec_opts = "--tag ~live"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
RSpec::Core::RakeTask.new("spec:live") do |t|
|
|
11
|
+
ENV["RUN_LIVE_TESTS"] = "true"
|
|
12
|
+
t.rspec_opts = "--tag live"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RSpec::Core::RakeTask.new("spec:all") do |_t|
|
|
16
|
+
ENV["RUN_LIVE_TESTS"] = "true"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task default: :spec
|
|
20
|
+
|
|
21
|
+
desc "Run all tests including live tests"
|
|
22
|
+
task test_all: "spec:all"
|
|
23
|
+
|
|
24
|
+
desc "Run only live tests (requires network)"
|
|
25
|
+
task test_live: "spec:live"
|
|
26
|
+
|
|
27
|
+
namespace :bump do
|
|
28
|
+
def bump_version(type)
|
|
29
|
+
version_file = "lib/caruso/version.rb"
|
|
30
|
+
content = File.read(version_file)
|
|
31
|
+
|
|
32
|
+
unless content =~ /VERSION = "(\d+)\.(\d+)\.(\d+)"/
|
|
33
|
+
raise "Could not find version in #{version_file}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
major, minor, patch = $1.to_i, $2.to_i, $3.to_i
|
|
37
|
+
|
|
38
|
+
case type
|
|
39
|
+
when :major
|
|
40
|
+
major += 1
|
|
41
|
+
minor = 0
|
|
42
|
+
patch = 0
|
|
43
|
+
when :minor
|
|
44
|
+
minor += 1
|
|
45
|
+
patch = 0
|
|
46
|
+
when :patch
|
|
47
|
+
patch += 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
new_version = "#{major}.#{minor}.#{patch}"
|
|
51
|
+
new_content = content.sub(/VERSION = ".*"/, "VERSION = \"#{new_version}\"")
|
|
52
|
+
|
|
53
|
+
File.write(version_file, new_content)
|
|
54
|
+
puts "Bumped version to #{new_version}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "Bump patch version"
|
|
58
|
+
task :patch do
|
|
59
|
+
bump_version(:patch)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "Bump minor version"
|
|
63
|
+
task :minor do
|
|
64
|
+
bump_version(:minor)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "Bump major version"
|
|
68
|
+
task :major do
|
|
69
|
+
bump_version(:major)
|
|
70
|
+
end
|
|
71
|
+
end
|
data/bin/caruso
ADDED
data/caruso.gemspec
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/caruso/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "caruso"
|
|
7
|
+
spec.version = Caruso::VERSION
|
|
8
|
+
spec.authors = ["Philipp Comans"]
|
|
9
|
+
spec.email = ["philipp.comans@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Sync steering docs from Claude Marketplaces to other agents."
|
|
12
|
+
spec.description = "A tool to fetch Claude Code plugins and adapt them into Cursor Rules or other agent contexts."
|
|
13
|
+
spec.homepage = "https://github.com/pcomans/caruso"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/pcomans/caruso"
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(File.expand_path(f) == __FILE__) ||
|
|
25
|
+
f.start_with?(*%w[test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = "bin"
|
|
29
|
+
spec.executables = ["caruso"]
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
# Runtime dependencies
|
|
33
|
+
spec.add_dependency "faraday", "~> 2.0" # For HTTP requests
|
|
34
|
+
spec.add_dependency "git", "~> 1.19" # For cloning repos
|
|
35
|
+
spec.add_dependency "thor", "~> 1.3" # For CLI
|
|
36
|
+
|
|
37
|
+
# Development dependencies
|
|
38
|
+
spec.add_development_dependency "aruba", "~> 2.1"
|
|
39
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
40
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
41
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
42
|
+
spec.add_development_dependency "timecop", "~> 0.9"
|
|
43
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Caruso
|
|
7
|
+
class Adapter
|
|
8
|
+
attr_reader :files, :target_dir, :agent, :marketplace_name, :plugin_name
|
|
9
|
+
|
|
10
|
+
def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
|
|
11
|
+
@files = files
|
|
12
|
+
@target_dir = target_dir
|
|
13
|
+
@agent = agent
|
|
14
|
+
@marketplace_name = marketplace_name
|
|
15
|
+
@plugin_name = plugin_name
|
|
16
|
+
FileUtils.mkdir_p(@target_dir)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def adapt
|
|
20
|
+
created_files = []
|
|
21
|
+
files.each do |file_path|
|
|
22
|
+
content = File.read(file_path)
|
|
23
|
+
adapted_content = inject_metadata(content, file_path)
|
|
24
|
+
created_file = save_file(file_path, adapted_content)
|
|
25
|
+
created_files << created_file
|
|
26
|
+
end
|
|
27
|
+
created_files
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def inject_metadata(content, file_path)
|
|
33
|
+
# Check if frontmatter exists
|
|
34
|
+
if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
|
|
35
|
+
# If it exists, we might need to append to it or modify it
|
|
36
|
+
# For now, we assume existing frontmatter is "good enough" but might need 'globs' for Cursor
|
|
37
|
+
ensure_cursor_globs(content) if agent == :cursor
|
|
38
|
+
else
|
|
39
|
+
# No frontmatter, prepend it
|
|
40
|
+
create_frontmatter(file_path) + content
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ensure_cursor_globs(content)
|
|
45
|
+
# Add required Cursor metadata fields if missing
|
|
46
|
+
# globs: [] enables semantic search (Apply Intelligently)
|
|
47
|
+
# alwaysApply: false means it won't apply to every chat session
|
|
48
|
+
|
|
49
|
+
unless content.include?("globs:")
|
|
50
|
+
content.sub!(/\A---\s*\n/, "---\nglobs: []\n")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless content.include?("alwaysApply:")
|
|
54
|
+
# Add after the first line of frontmatter
|
|
55
|
+
content.sub!(/\A---\s*\n/, "---\nalwaysApply: false\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
content
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_frontmatter(file_path)
|
|
62
|
+
filename = File.basename(file_path)
|
|
63
|
+
<<~YAML
|
|
64
|
+
---
|
|
65
|
+
description: Imported rule from #{filename}
|
|
66
|
+
globs: []
|
|
67
|
+
alwaysApply: false
|
|
68
|
+
---
|
|
69
|
+
YAML
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def save_file(original_path, content)
|
|
73
|
+
filename = File.basename(original_path, ".*")
|
|
74
|
+
|
|
75
|
+
# Rename SKILL.md to the skill name (parent directory) to avoid collisions
|
|
76
|
+
if filename.casecmp("skill").zero?
|
|
77
|
+
filename = File.basename(File.dirname(original_path))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
extension = agent == :cursor ? ".mdc" : ".md"
|
|
81
|
+
output_filename = "#{filename}#{extension}"
|
|
82
|
+
|
|
83
|
+
# Extract component type from original path (commands/agents/skills)
|
|
84
|
+
component_type = extract_component_type(original_path)
|
|
85
|
+
|
|
86
|
+
# Build nested directory structure for Cursor
|
|
87
|
+
# Build nested directory structure for Cursor
|
|
88
|
+
# Structure: .cursor/rules/caruso/marketplace/plugin/component-type/file.mdc
|
|
89
|
+
subdirs = File.join("caruso", marketplace_name, plugin_name, component_type)
|
|
90
|
+
output_dir = File.join(@target_dir, subdirs)
|
|
91
|
+
FileUtils.mkdir_p(output_dir)
|
|
92
|
+
target_path = File.join(output_dir, output_filename)
|
|
93
|
+
|
|
94
|
+
File.write(target_path, content)
|
|
95
|
+
puts "Saved: #{target_path}"
|
|
96
|
+
|
|
97
|
+
# Return relative path from target_dir
|
|
98
|
+
File.join(subdirs, output_filename)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def extract_component_type(file_path)
|
|
102
|
+
# Extract component type (commands/agents/skills) from path
|
|
103
|
+
return "commands" if file_path.include?("/commands/")
|
|
104
|
+
return "agents" if file_path.include?("/agents/")
|
|
105
|
+
return "skills" if file_path.include?("/skills/")
|
|
106
|
+
|
|
107
|
+
raise Caruso::Error, "Cannot determine component type from path: #{file_path}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|