baid 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 221fba49d1c1620e0e5423517dc5b0c63f1d6d71898377b3df90671ae5732a2a
4
- data.tar.gz: 499df0b58c473f06738f1e725e5c9b023a7b1b481c3192fbdc6494e2c24e5626
3
+ metadata.gz: 25c6623df08284d0d26b7168752a7fb1d36267d06b4dd53f03d85f9eca251636
4
+ data.tar.gz: c349b2671a0c71f624e1a93e69475fc5ac74bc3b399a7b119928ad82dd4fc459
5
5
  SHA512:
6
- metadata.gz: 62e89d04b3c8cbb5ed97a4a4b20bf16b3044cddf4eb37863304fcd7222a7973b3987465fa69a18ae84f40b7e205cb2d69ad41dfc25022657629b12edd2eea243
7
- data.tar.gz: bea46903bc722717c6ca60506a13d2124c58edd277e16f0e390fce4f282c6783904557b8b9cb20d427426a91a9e54aa2838382958677b3e3c86b38c30e162f64
6
+ metadata.gz: 723af1e4232b451ba29f84be06ee506ae75fcda4c628fd55594c8a3f2dae476dad3952dd8c08aa802ae6842f04d4fafc7e204e56fc5a491f8ab62d46c87b7b2b
7
+ data.tar.gz: 1a021dd07862bcdc77347cb60c54d3bdaae50e332c5d1595396723a689479690e7efac0a66303fc9fd276fe702d1603e6720857d2025ecba1426cb75d248e3eb
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baid
4
+ class ClaudeMdWriter
5
+ AGENT_CLAUDE_MD_PATHS = {
6
+ "claude_code" => "CLAUDE.md",
7
+ "cursor" => File.join(".cursor", "rules", "BAID.md"),
8
+ "opencode" => File.join(".opencode", "AGENTS.md"),
9
+ "gemini" => "GEMINI.md",
10
+ "antigravity" => File.join(".agent", "AGENTS.md")
11
+ }.freeze
12
+
13
+ BAID_SECTION_MARKER = "<!-- baid-managed-section -->"
14
+
15
+ def self.write(instructions, agents, project_dir = ".")
16
+ agents.each do |agent|
17
+ path_template = AGENT_CLAUDE_MD_PATHS[agent[:name]]
18
+ next unless path_template
19
+
20
+ full_path = File.join(project_dir, path_template)
21
+ FileUtils.mkdir_p(File.dirname(full_path))
22
+ write_or_append(full_path, instructions)
23
+ end
24
+ end
25
+
26
+ def self.write_or_append(path, instructions)
27
+ baid_block = "#{BAID_SECTION_MARKER}\n#{instructions}\n#{BAID_SECTION_MARKER}"
28
+
29
+ if File.exist?(path)
30
+ existing = File.read(path)
31
+ if existing.include?(BAID_SECTION_MARKER)
32
+ updated = existing.gsub(
33
+ /#{Regexp.escape(BAID_SECTION_MARKER)}.*?#{Regexp.escape(BAID_SECTION_MARKER)}/m,
34
+ baid_block
35
+ )
36
+ File.write(path, updated)
37
+ else
38
+ File.open(path, "a") { |f| f.write("\n#{baid_block}\n") }
39
+ end
40
+ else
41
+ File.write(path, "#{baid_block}\n")
42
+ end
43
+ end
44
+
45
+ private_class_method :write_or_append
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baid
4
+ class ClaudeSettingsWriter
5
+ SETTINGS_PATH = File.join(".claude", "settings.json")
6
+
7
+ def self.write(hook_configs, agents, project_dir = ".")
8
+ return unless agents.any? { |a| a[:name] == "claude_code" }
9
+
10
+ claude_hooks = hook_configs["claude_code"]
11
+ return unless claude_hooks
12
+
13
+ full_path = File.join(project_dir, SETTINGS_PATH)
14
+ FileUtils.mkdir_p(File.dirname(full_path))
15
+
16
+ existing = load_settings(full_path)
17
+ existing["hooks"] ||= []
18
+
19
+ new_hooks = claude_hooks["hooks"] || []
20
+ new_hooks.each do |new_hook|
21
+ existing["hooks"].reject! { |h| h["event"] == new_hook["event"] && baid_hook?(h) }
22
+ existing["hooks"] << new_hook
23
+ end
24
+
25
+ File.write(full_path, JSON.pretty_generate(existing))
26
+ end
27
+
28
+ def self.baid_hook?(hook)
29
+ Array(hook["hooks"]).any? { |h| h["tool"]&.start_with?("baid_") }
30
+ end
31
+
32
+ def self.load_settings(path)
33
+ return {} unless File.exist?(path)
34
+
35
+ JSON.parse(File.read(path))
36
+ rescue JSON::ParserError
37
+ {}
38
+ end
39
+
40
+ private_class_method :load_settings, :baid_hook?
41
+ end
42
+ end
data/lib/baid/cli.rb CHANGED
@@ -32,22 +32,12 @@ module Baid
32
32
  Commands::Reconfigure.new.execute
33
33
  end
34
34
 
35
- desc "install SKILL_NAME", "Install a skill into your project"
36
- def install(skill_name)
37
- Commands::Install.new.execute(skill_name)
38
- end
39
-
40
- desc "search QUERY", "Search for skills in the registry"
41
- def search(query)
42
- Commands::Search.new.execute(query)
43
- end
44
-
45
35
  desc "update", "Update all installed skills to latest versions"
46
36
  def update
47
37
  Commands::Update.new.execute
48
38
  end
49
39
 
50
- desc "skills SUBCOMMAND", "Manage workspace skills"
40
+ desc "skills SUBCOMMAND", "Manage skills"
51
41
  subcommand "skills", Class.new(Thor) {
52
42
  namespace "skills"
53
43
 
@@ -60,6 +50,21 @@ module Baid
60
50
  def show(workspace_slug, skill_id = nil)
61
51
  Commands::Skills.new.show(workspace_slug, skill_id)
62
52
  end
53
+
54
+ desc "search QUERY", "Search for skills in the registry"
55
+ def search(query)
56
+ Commands::Skills.new.search(query)
57
+ end
58
+
59
+ desc "download SKILL_NAME", "Download and install a skill into your project"
60
+ def download(skill_name)
61
+ Commands::Skills.new.download(skill_name)
62
+ end
63
+
64
+ desc "clean", "Remove all installed skills"
65
+ def clean
66
+ Commands::Skills.new.clean
67
+ end
63
68
  }
64
69
 
65
70
  desc "workspace SUBCOMMAND", "Manage workspaces"
@@ -20,15 +20,29 @@ module Baid
20
20
  puts "Configured MCP for #{agent[:name]}"
21
21
  end
22
22
 
23
- init_result = smart_init(agents)
23
+ result = smart_init(agents)
24
+
25
+ if result[:claude_md_instructions] && !result[:claude_md_instructions].empty?
26
+ ClaudeMdWriter.write(result[:claude_md_instructions], agents, @project_dir)
27
+ end
28
+
29
+ if result[:agent_configs]&.any?
30
+ SubAgentWriter.write(result[:agent_configs], agents, @project_dir)
31
+ end
32
+
33
+ if result[:hook_configs]&.any?
34
+ ClaudeSettingsWriter.write(result[:hook_configs], agents, @project_dir)
35
+ end
36
+
37
+ tech_stack = result[:tech_stack] || []
38
+ GitHookWriter.write(tech_stack, @project_dir)
24
39
 
25
40
  config_path = File.join(@project_dir, ".baid", "config.yml")
26
41
  FileUtils.mkdir_p(File.dirname(config_path))
27
42
  config = {
28
43
  "agents" => agents.map { |a| a[:name] },
29
- "installed_skills" => init_result[:skills]
44
+ "tech_stack" => tech_stack
30
45
  }
31
- config["tech_stack"] = init_result[:tech_stack] if init_result[:tech_stack]&.any?
32
46
  File.write(config_path, config.to_yaml)
33
47
 
34
48
  puts "Baid initialized with #{agents.length} agent(s)."
@@ -39,7 +53,7 @@ module Baid
39
53
  def smart_init(agents)
40
54
  token = Config.load_token
41
55
  workspace = Config.load_active_workspace
42
- return { skills: [], tech_stack: [] } unless token && workspace
56
+ return default_result unless token && workspace
43
57
 
44
58
  puts "Analyzing project..."
45
59
  scan = ProjectScanner.scan(@project_dir)
@@ -51,33 +65,37 @@ module Baid
51
65
  package_files: scan[:package_files]
52
66
  })
53
67
 
54
- return { skills: [], tech_stack: [] } unless response.code == 200
68
+ return default_result unless response.code == 200
55
69
 
56
70
  data = JSON.parse(response.body)
57
71
  tech_stack = data["tech_stack"] || []
58
72
  answers = present_questions(data["questions"] || [])
59
73
 
74
+ puts "Configuring workspace..."
60
75
  setup_response = client.post("/init/setup", {
61
76
  workspace_slug: workspace,
62
77
  tech_stack: tech_stack,
63
78
  answers: answers,
64
- agents: agents.map { |a| a[:name] },
65
- recommended_skills: data["recommended_skills"] || []
79
+ agents: agents.map { |a| a[:name] }
66
80
  })
67
81
 
68
- return { skills: [], tech_stack: tech_stack } unless setup_response.code == 200
82
+ return default_result.merge(tech_stack: tech_stack) unless setup_response.code == 200
69
83
 
70
84
  setup_data = JSON.parse(setup_response.body)
71
- skills = setup_data["skills"] || []
72
85
 
73
- skills.each do |skill|
74
- SkillWriter.write(skill, agents, @project_dir)
75
- puts "Installed skill: #{skill["name"]}"
76
- end
86
+ {
87
+ tech_stack: tech_stack,
88
+ agent_configs: setup_data["agent_configs"] || {},
89
+ hook_configs: setup_data["hook_configs"] || {},
90
+ claude_md_instructions: setup_data["claude_md_instructions"]
91
+ }
92
+ rescue StandardError => e
93
+ puts "Warning: Smart init failed — #{e.message}"
94
+ default_result
95
+ end
77
96
 
78
- { skills: skills.map { |s| s["name"] }, tech_stack: tech_stack }
79
- rescue StandardError
80
- { skills: [], tech_stack: [] }
97
+ def default_result
98
+ { tech_stack: [], agent_configs: {}, hook_configs: {}, claude_md_instructions: nil }
81
99
  end
82
100
 
83
101
  def present_questions(questions)
@@ -3,6 +3,10 @@
3
3
  module Baid
4
4
  module Commands
5
5
  class Skills
6
+ def initialize(project_dir: ".")
7
+ @project_dir = project_dir
8
+ end
9
+
6
10
  def list(workspace_slug = nil)
7
11
  workspace_slug = resolve_workspace(workspace_slug)
8
12
 
@@ -58,6 +62,100 @@ module Baid
58
62
  puts "Agents: #{(data['agents'] || []).join(', ')}"
59
63
  end
60
64
 
65
+ def search(query)
66
+ client = ApiClient.new
67
+ response = client.get("/skills/search", q: query)
68
+ data = JSON.parse(response.body)
69
+ skills = data["skills"] || []
70
+
71
+ if skills.empty?
72
+ puts "No skills found for '#{query}'."
73
+ return
74
+ end
75
+
76
+ puts format("%-25s %-40s %-10s %s", "NAME", "DESCRIPTION", "VERSION", "WORKSPACE")
77
+ puts "-" * 90
78
+ skills.each do |skill|
79
+ puts format("%-25s %-40s %-10s %s",
80
+ skill["name"],
81
+ (skill["description"] || "")[0..38],
82
+ skill["version"],
83
+ skill["workspace"])
84
+ end
85
+ end
86
+
87
+ def download(skill_name)
88
+ client = ApiClient.new
89
+ response = client.get("/skills/resolve", name: skill_name)
90
+
91
+ if response.code != 200
92
+ puts "Skill '#{skill_name}' not found."
93
+ return
94
+ end
95
+
96
+ data = JSON.parse(response.body)
97
+ skill = data["skill"]
98
+
99
+ config = Config.load_project_config
100
+ agents = (config["agents"] || []).map do |name|
101
+ AgentDetector::AGENTS.find { |a| a[:name] == name }
102
+ end.compact
103
+
104
+ SkillWriter.write(skill, agents, @project_dir)
105
+
106
+ config["installed_skills"] ||= []
107
+ config["installed_skills"] << skill["name"] unless config["installed_skills"].include?(skill["name"])
108
+ Config.save_project_config(config)
109
+
110
+ puts "Installed #{skill['name']}@#{skill['version']}"
111
+ end
112
+
113
+ def clean
114
+ config = Config.load_project_config
115
+ installed = config["installed_skills"] || []
116
+
117
+ if installed.empty?
118
+ puts "No skills installed."
119
+ return
120
+ end
121
+
122
+ prompt = TTY::Prompt.new
123
+ puts "Installed skills: #{installed.join(', ')}"
124
+ confirmed = prompt.yes?("This will remove all installed skills. Are you sure?")
125
+
126
+ unless confirmed
127
+ puts "Aborted."
128
+ return
129
+ end
130
+
131
+ # Document removed skills
132
+ baid_dir = File.join(@project_dir, ".baid")
133
+ FileUtils.mkdir_p(baid_dir)
134
+ removed_file = File.join(baid_dir, "removed_skills.md")
135
+
136
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
137
+ content = "# Removed Skills\n\n"
138
+ content += "Removed on #{timestamp}\n\n"
139
+ installed.each { |name| content += "- #{name}\n" }
140
+
141
+ File.write(removed_file, content)
142
+
143
+ # Remove skill files from all agent directories
144
+ agents = (config["agents"] || []).map do |name|
145
+ AgentDetector::AGENTS.find { |a| a[:name] == name }
146
+ end.compact
147
+
148
+ installed.each do |skill_name|
149
+ SkillWriter.remove(skill_name, agents, @project_dir)
150
+ end
151
+
152
+ # Clear installed_skills from config
153
+ config["installed_skills"] = []
154
+ Config.save_project_config(config)
155
+
156
+ puts "Removed #{installed.length} skill(s). Details saved to .baid/removed_skills.md"
157
+ end
158
+
61
159
  private
62
160
 
63
161
  def resolve_workspace(workspace_slug)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baid
4
+ class GitHookWriter
5
+ BAID_MARKER = "# baid-managed"
6
+
7
+ DEFAULT_COMMANDS = {
8
+ "ruby" => "bundle exec rubocop --autocorrect-all -f quiet && bundle exec rspec --fail-fast",
9
+ "node" => "npm run lint && npm test",
10
+ "python" => "ruff check . && pytest"
11
+ }.freeze
12
+
13
+ BAID_END_MARKER = "# end baid-managed"
14
+
15
+ def self.write(tech_stack, project_dir = ".")
16
+ hooks_dir = File.join(project_dir, ".git", "hooks")
17
+ return unless Dir.exist?(hooks_dir)
18
+
19
+ commands = build_commands(tech_stack)
20
+ return if commands.empty?
21
+
22
+ hook_path = File.join(hooks_dir, "pre-commit")
23
+ baid_section = "#{BAID_MARKER}: pre-commit hook\n#{commands.join("\n")}\n#{BAID_END_MARKER}"
24
+
25
+ if File.exist?(hook_path)
26
+ existing = File.read(hook_path)
27
+ if existing.include?(BAID_MARKER)
28
+ content = existing.sub(/#{Regexp.escape(BAID_MARKER)}.*?#{Regexp.escape(BAID_END_MARKER)}/m, baid_section)
29
+ else
30
+ content = existing.rstrip + "\n\n#{baid_section}\n"
31
+ end
32
+ else
33
+ content = "#!/bin/sh\n#{baid_section}\n"
34
+ end
35
+
36
+ File.write(hook_path, content)
37
+ FileUtils.chmod(0o755, hook_path)
38
+ end
39
+
40
+ def self.build_commands(tech_stack)
41
+ DEFAULT_COMMANDS.filter_map do |stack_key, command|
42
+ command if tech_stack.any? { |t| t.include?(stack_key) }
43
+ end
44
+ end
45
+
46
+ private_class_method :build_commands
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baid
4
+ class SubAgentWriter
5
+ AGENT_SUB_AGENT_DIRS = {
6
+ "claude_code" => File.join(".claude", "agents"),
7
+ "cursor" => File.join(".cursor", "agents"),
8
+ "opencode" => File.join(".opencode", "agents")
9
+ }.freeze
10
+
11
+ def self.write(agent_configs, agents, project_dir = ".")
12
+ agents.each do |agent|
13
+ dir_template = AGENT_SUB_AGENT_DIRS[agent[:name]]
14
+ next unless dir_template
15
+
16
+ config = agent_configs[agent[:name]]
17
+ next unless config&.dig("sub_agents")
18
+
19
+ full_dir = File.join(project_dir, dir_template)
20
+ FileUtils.mkdir_p(full_dir)
21
+
22
+ config["sub_agents"].each do |sub_agent|
23
+ filename = File.basename(sub_agent["filename"].to_s)
24
+ next if filename.empty?
25
+
26
+ file_path = File.join(full_dir, filename)
27
+ File.write(file_path, sub_agent["content"])
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/baid.rb CHANGED
@@ -8,7 +8,7 @@ require "fileutils"
8
8
  require "tty-prompt"
9
9
 
10
10
  module Baid
11
- VERSION = "0.1.0"
11
+ VERSION = "0.3.0"
12
12
  end
13
13
 
14
14
  require_relative "baid/config"
@@ -20,10 +20,13 @@ require_relative "baid/commands/login"
20
20
  require_relative "baid/commands/logout"
21
21
  require_relative "baid/commands/whoami"
22
22
  require_relative "baid/skill_writer"
23
+ require_relative "baid/claude_md_writer"
24
+ require_relative "baid/sub_agent_writer"
25
+ require_relative "baid/git_hook_writer"
26
+ require_relative "baid/claude_settings_writer"
23
27
  require_relative "baid/commands/init"
24
28
  require_relative "baid/commands/reconfigure"
25
- require_relative "baid/commands/install"
26
- require_relative "baid/commands/search"
29
+
27
30
  require_relative "baid/commands/update"
28
31
  require_relative "baid/skills_sh_scraper"
29
32
  require_relative "baid/commands/skills"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: baid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Baid Team
@@ -93,6 +93,7 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '3.18'
96
+ description: Manage AI agent skills, sub-agents, and hooks across workspaces
96
97
  executables:
97
98
  - baid
98
99
  extensions: []
@@ -102,22 +103,24 @@ files:
102
103
  - lib/baid.rb
103
104
  - lib/baid/agent_detector.rb
104
105
  - lib/baid/api_client.rb
106
+ - lib/baid/claude_md_writer.rb
107
+ - lib/baid/claude_settings_writer.rb
105
108
  - lib/baid/cli.rb
106
109
  - lib/baid/commands/init.rb
107
- - lib/baid/commands/install.rb
108
110
  - lib/baid/commands/login.rb
109
111
  - lib/baid/commands/logout.rb
110
112
  - lib/baid/commands/reconfigure.rb
111
- - lib/baid/commands/search.rb
112
113
  - lib/baid/commands/skills.rb
113
114
  - lib/baid/commands/update.rb
114
115
  - lib/baid/commands/whoami.rb
115
116
  - lib/baid/commands/workspace.rb
116
117
  - lib/baid/config.rb
118
+ - lib/baid/git_hook_writer.rb
117
119
  - lib/baid/mcp_configurator.rb
118
120
  - lib/baid/project_scanner.rb
119
121
  - lib/baid/skill_writer.rb
120
122
  - lib/baid/skills_sh_scraper.rb
123
+ - lib/baid/sub_agent_writer.rb
121
124
  homepage: https://baid.dev
122
125
  licenses:
123
126
  - MIT
@@ -138,5 +141,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
141
  requirements: []
139
142
  rubygems_version: 3.6.9
140
143
  specification_version: 4
141
- summary: CLI for BaidSkills — AI agent skill management
144
+ summary: CLI for Baid — AI agent skill management for development teams
142
145
  test_files: []
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Baid
4
- module Commands
5
- class Install
6
- def initialize(project_dir: ".")
7
- @project_dir = project_dir
8
- end
9
-
10
- def execute(skill_name)
11
- client = ApiClient.new
12
- response = client.get("/skills/resolve", name: skill_name)
13
-
14
- if response.code != 200
15
- puts "Skill '#{skill_name}' not found."
16
- return
17
- end
18
-
19
- data = JSON.parse(response.body)
20
- skill = data["skill"]
21
-
22
- config = Config.load_project_config
23
- agents = (config["agents"] || []).map do |name|
24
- AgentDetector::AGENTS.find { |a| a[:name] == name }
25
- end.compact
26
-
27
- SkillWriter.write(skill, agents, @project_dir)
28
-
29
- # Update installed_skills in config
30
- config["installed_skills"] ||= []
31
- config["installed_skills"] << skill["name"] unless config["installed_skills"].include?(skill["name"])
32
- Config.save_project_config(config)
33
-
34
- puts "Installed #{skill['name']}@#{skill['version']}"
35
- end
36
- end
37
- end
38
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Baid
4
- module Commands
5
- class Search
6
- def execute(query)
7
- client = ApiClient.new
8
- response = client.get("/skills/search", q: query)
9
- data = JSON.parse(response.body)
10
- skills = data["skills"] || []
11
-
12
- if skills.empty?
13
- puts "No skills found for '#{query}'."
14
- return
15
- end
16
-
17
- puts format("%-25s %-40s %-10s %s", "NAME", "DESCRIPTION", "VERSION", "WORKSPACE")
18
- puts "-" * 90
19
- skills.each do |skill|
20
- puts format("%-25s %-40s %-10s %s",
21
- skill["name"],
22
- (skill["description"] || "")[0..38],
23
- skill["version"],
24
- skill["workspace"])
25
- end
26
- end
27
- end
28
- end
29
- end