aircana 2.0.0 → 3.0.0.rc2

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.claude-plugin/plugin.json +7 -0
  3. data/.rspec_status +184 -187
  4. data/.rubocop.yml +12 -0
  5. data/CHANGELOG.md +38 -0
  6. data/CLAUDE.md +51 -20
  7. data/README.md +132 -63
  8. data/agents/apply_feedback.md +92 -0
  9. data/agents/executor.md +85 -0
  10. data/agents/jira.md +46 -0
  11. data/agents/planner.md +64 -0
  12. data/agents/reviewer.md +95 -0
  13. data/agents/sub-agent-coordinator.md +91 -0
  14. data/agents/test-agent/manifest.json +15 -0
  15. data/commands/air-apply-feedback.md +15 -0
  16. data/commands/air-ask-expert.md +42 -0
  17. data/commands/air-execute.md +13 -0
  18. data/commands/air-plan.md +33 -0
  19. data/commands/air-record.md +17 -0
  20. data/commands/air-review.md +12 -0
  21. data/commands/sample-command.md +1 -0
  22. data/hooks/hooks.json +31 -0
  23. data/lib/aircana/cli/app.rb +27 -30
  24. data/lib/aircana/cli/commands/agents.rb +41 -9
  25. data/lib/aircana/cli/commands/doctor_checks.rb +2 -3
  26. data/lib/aircana/cli/commands/generate.rb +0 -11
  27. data/lib/aircana/cli/commands/hooks.rb +4 -4
  28. data/lib/aircana/cli/commands/init.rb +266 -0
  29. data/lib/aircana/cli/commands/plugin.rb +157 -0
  30. data/lib/aircana/cli/help_formatter.rb +2 -3
  31. data/lib/aircana/configuration.rb +29 -3
  32. data/lib/aircana/contexts/manifest.rb +1 -8
  33. data/lib/aircana/generators/agents_generator.rb +3 -2
  34. data/lib/aircana/hooks_manifest.rb +189 -0
  35. data/lib/aircana/plugin_manifest.rb +146 -0
  36. data/lib/aircana/system_checker.rb +0 -11
  37. data/lib/aircana/templates/agents/base_agent.erb +2 -2
  38. data/lib/aircana/templates/hooks/session_start.erb +3 -118
  39. data/lib/aircana/templates/hooks/user_prompt_submit.erb +0 -6
  40. data/lib/aircana/version.rb +1 -1
  41. data/spec_target_1760205040_181/agents/test-agent/manifest.json +15 -0
  42. data/spec_target_1760205220_486/agents/test-agent/manifest.json +15 -0
  43. data/spec_target_1760205379_250/agents/test-agent/manifest.json +15 -0
  44. data/spec_target_1760205601_652/agents/test-agent/manifest.json +15 -0
  45. data/spec_target_1760205608_135/agents/test-agent/manifest.json +15 -0
  46. data/spec_target_1760205654_952/agents/test-agent/manifest.json +15 -0
  47. metadata +29 -7
  48. data/lib/aircana/cli/commands/install.rb +0 -179
  49. data/lib/aircana/cli/commands/project.rb +0 -156
  50. data/lib/aircana/generators/project_config_generator.rb +0 -54
  51. data/lib/aircana/symlink_manager.rb +0 -158
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aircana
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weston Dransfield
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-10-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: httparty
@@ -102,6 +102,7 @@ executables:
102
102
  extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
+ - ".claude-plugin/plugin.json"
105
106
  - ".devcontainer/devcontainer.json"
106
107
  - ".dockerignore"
107
108
  - ".rspec_status"
@@ -113,8 +114,23 @@ files:
113
114
  - README.md
114
115
  - Rakefile
115
116
  - SECURITY.md
117
+ - agents/apply_feedback.md
118
+ - agents/executor.md
119
+ - agents/jira.md
120
+ - agents/planner.md
121
+ - agents/reviewer.md
122
+ - agents/sub-agent-coordinator.md
123
+ - agents/test-agent/manifest.json
124
+ - commands/air-apply-feedback.md
125
+ - commands/air-ask-expert.md
126
+ - commands/air-execute.md
127
+ - commands/air-plan.md
128
+ - commands/air-record.md
129
+ - commands/air-review.md
130
+ - commands/sample-command.md
116
131
  - compose.yml
117
132
  - exe/aircana
133
+ - hooks/hooks.json
118
134
  - lib/aircana.rb
119
135
  - lib/aircana/cli.rb
120
136
  - lib/aircana/cli/app.rb
@@ -125,8 +141,8 @@ files:
125
141
  - lib/aircana/cli/commands/dump_context.rb
126
142
  - lib/aircana/cli/commands/generate.rb
127
143
  - lib/aircana/cli/commands/hooks.rb
128
- - lib/aircana/cli/commands/install.rb
129
- - lib/aircana/cli/commands/project.rb
144
+ - lib/aircana/cli/commands/init.rb
145
+ - lib/aircana/cli/commands/plugin.rb
130
146
  - lib/aircana/cli/help_formatter.rb
131
147
  - lib/aircana/cli/shell_command.rb
132
148
  - lib/aircana/cli/subcommand.rb
@@ -149,14 +165,14 @@ files:
149
165
  - lib/aircana/generators/helpers.rb
150
166
  - lib/aircana/generators/hooks_generator.rb
151
167
  - lib/aircana/generators/plan_command_generator.rb
152
- - lib/aircana/generators/project_config_generator.rb
153
168
  - lib/aircana/generators/record_command_generator.rb
154
169
  - lib/aircana/generators/review_command_generator.rb
170
+ - lib/aircana/hooks_manifest.rb
155
171
  - lib/aircana/human_logger.rb
156
172
  - lib/aircana/initializers.rb
157
173
  - lib/aircana/llm/claude_client.rb
174
+ - lib/aircana/plugin_manifest.rb
158
175
  - lib/aircana/progress_tracker.rb
159
- - lib/aircana/symlink_manager.rb
160
176
  - lib/aircana/system_checker.rb
161
177
  - lib/aircana/templates/agents/base_agent.erb
162
178
  - lib/aircana/templates/agents/defaults/apply_feedback.erb
@@ -181,6 +197,12 @@ files:
181
197
  - lib/aircana/templates/hooks/user_prompt_submit.erb
182
198
  - lib/aircana/version.rb
183
199
  - sig/aircana.rbs
200
+ - spec_target_1760205040_181/agents/test-agent/manifest.json
201
+ - spec_target_1760205220_486/agents/test-agent/manifest.json
202
+ - spec_target_1760205379_250/agents/test-agent/manifest.json
203
+ - spec_target_1760205601_652/agents/test-agent/manifest.json
204
+ - spec_target_1760205608_135/agents/test-agent/manifest.json
205
+ - spec_target_1760205654_952/agents/test-agent/manifest.json
184
206
  homepage: https://github.com/westonkd/aircana
185
207
  licenses:
186
208
  - MIT
@@ -204,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
226
  - !ruby/object:Gem::Version
205
227
  version: '0'
206
228
  requirements: []
207
- rubygems_version: 3.6.2
229
+ rubygems_version: 3.6.9
208
230
  specification_version: 4
209
231
  summary: Humble workflow and context utilities for engineering with agents
210
232
  test_files: []
@@ -1,179 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require_relative "generate"
5
- require_relative "../../generators/project_config_generator"
6
-
7
- module Aircana
8
- module CLI
9
- module Install
10
- class << self
11
- def run
12
- generate_files
13
- ensure_project_config_exists
14
- install_commands_to_claude
15
- install_hooks_to_claude
16
- end
17
-
18
- private
19
-
20
- def generate_files
21
- Aircana.human_logger.info("Generating files before installation...")
22
- Generate.run
23
- end
24
-
25
- def ensure_project_config_exists
26
- project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
27
- return if File.exist?(project_json_path)
28
-
29
- Aircana.human_logger.info("Creating project.json for multi-root support...")
30
- Aircana::Generators::ProjectConfigGenerator.new.generate
31
- end
32
-
33
- def install_commands_to_claude
34
- claude_commands_dir = File.join(Aircana.configuration.claude_code_project_config_path, "commands")
35
- Aircana.create_dir_if_needed(claude_commands_dir)
36
-
37
- copy_command_files(claude_commands_dir)
38
- install_agents_to_claude
39
- end
40
-
41
- def copy_command_files(destination_dir)
42
- Dir.glob("#{Aircana.configuration.output_dir}/commands/*").each do |file|
43
- Aircana.human_logger.success("Installing #{file} to #{destination_dir}")
44
- FileUtils.cp(file, destination_dir)
45
- end
46
- end
47
-
48
- def install_agents_to_claude
49
- claude_agents_dir = File.join(Aircana.configuration.claude_code_project_config_path, "agents")
50
- Aircana.create_dir_if_needed(claude_agents_dir)
51
-
52
- copy_agent_files(claude_agents_dir)
53
- end
54
-
55
- def copy_agent_files(destination_dir)
56
- agent_files_pattern = File.join(Aircana.configuration.output_dir, "agents", "*.md")
57
- Dir.glob(agent_files_pattern).each do |file|
58
- agent_name = File.basename(file, ".md")
59
- next unless default_agent?(agent_name)
60
-
61
- destination_file = File.join(destination_dir, File.basename(file))
62
- # Skip copying if source and destination are the same
63
- next if File.expand_path(file) == File.expand_path(destination_file)
64
-
65
- Aircana.human_logger.success("Installing default agent #{file} to #{destination_dir}")
66
- FileUtils.cp(file, destination_dir)
67
- end
68
- end
69
-
70
- def default_agent?(agent_name)
71
- require_relative "../../generators/agents_generator"
72
- Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
73
- end
74
-
75
- def install_hooks_to_claude
76
- return unless Dir.exist?(Aircana.configuration.hooks_dir)
77
-
78
- settings_file = File.join(Aircana.configuration.claude_code_project_config_path, "settings.local.json")
79
- install_hooks_to_settings(settings_file)
80
- end
81
-
82
- def install_hooks_to_settings(settings_file)
83
- settings = load_settings(settings_file)
84
- hook_configs = build_hook_configs
85
-
86
- return if hook_configs.empty?
87
-
88
- settings["hooks"] = hook_configs
89
- save_settings(settings_file, settings)
90
-
91
- Aircana.human_logger.success("Installed hooks to #{settings_file}")
92
- end
93
-
94
- def load_settings(settings_file)
95
- if File.exist?(settings_file)
96
- JSON.parse(File.read(settings_file))
97
- else
98
- Aircana.create_dir_if_needed(File.dirname(settings_file))
99
- {}
100
- end
101
- rescue JSON::ParserError
102
- Aircana.human_logger.warn("Invalid JSON in #{settings_file}, creating new settings")
103
- {}
104
- end
105
-
106
- def save_settings(settings_file, settings)
107
- File.write(settings_file, JSON.pretty_generate(settings))
108
- end
109
-
110
- def build_hook_configs
111
- hooks = {}
112
-
113
- # Map hook files to Claude Code hook events and their properties
114
- hook_mappings = {
115
- "pre_tool_use" => { event: "PreToolUse", matcher: nil },
116
- "post_tool_use" => { event: "PostToolUse", matcher: nil },
117
- "user_prompt_submit" => { event: "UserPromptSubmit", matcher: nil },
118
- "session_start" => { event: "SessionStart", matcher: nil },
119
- "notification_sqs" => { event: "Notification", matcher: nil },
120
- "rubocop_pre_commit" => { event: "PreToolUse", matcher: "Bash" },
121
- "rspec_test" => { event: "PostToolUse", matcher: "Bash" },
122
- "bundle_install" => { event: "PostToolUse", matcher: "Bash" }
123
- }
124
-
125
- Dir.glob("#{Aircana.configuration.hooks_dir}/*.sh").each do |hook_file|
126
- hook_name = File.basename(hook_file, ".sh")
127
-
128
- # Determine mapping for this hook
129
- mapping = if hook_mappings.key?(hook_name)
130
- hook_mappings[hook_name]
131
- else
132
- # For custom hooks, try to infer the event type from the filename
133
- infer_hook_mapping(hook_name)
134
- end
135
-
136
- next unless mapping
137
-
138
- event_key = mapping[:event]
139
-
140
- # Create relative path from project root
141
- relative_path = File.join(".aircana", "hooks", "#{hook_name}.sh")
142
-
143
- hook_entry = {
144
- "hooks" => [
145
- {
146
- "type" => "command",
147
- "command" => relative_path
148
- }
149
- ]
150
- }
151
-
152
- # Add matcher if specified
153
- hook_entry["matcher"] = mapping[:matcher] if mapping[:matcher]
154
-
155
- hooks[event_key] ||= []
156
- hooks[event_key] << hook_entry
157
- end
158
-
159
- hooks
160
- end
161
-
162
- def infer_hook_mapping(hook_name)
163
- # Try to infer the event type from common patterns in the hook name
164
- case hook_name
165
- when /pre_tool_use|pre_tool|before_tool/i
166
- { event: "PreToolUse", matcher: nil }
167
- when /user_prompt|prompt_submit|before_prompt/i
168
- { event: "UserPromptSubmit", matcher: nil }
169
- when /session_start|session_init|startup/i
170
- { event: "SessionStart", matcher: nil }
171
- else
172
- # Default to PostToolUse for unknown custom hooks and post_tool patterns
173
- { event: "PostToolUse", matcher: nil }
174
- end
175
- end
176
- end
177
- end
178
- end
179
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "tty-prompt"
5
- require_relative "../../symlink_manager"
6
- require_relative "../../generators/project_config_generator"
7
-
8
- module Aircana
9
- module CLI
10
- module Project
11
- class << self
12
- def init
13
- generator = Aircana::Generators::ProjectConfigGenerator.new
14
- config_path = generator.generate
15
-
16
- Aircana.human_logger.success "Initialized project.json at #{config_path}"
17
- Aircana.human_logger.info "Add folders using: aircana project add <path>"
18
- end
19
-
20
- def add(folder_path)
21
- project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
22
-
23
- # Create project.json if it doesn't exist
24
- init unless File.exist?(project_json_path)
25
-
26
- # Validate folder exists
27
- full_path = File.join(Aircana.configuration.project_dir, folder_path)
28
- unless Dir.exist?(full_path)
29
- Aircana.human_logger.error "Folder not found: #{folder_path}"
30
- return
31
- end
32
-
33
- # Load existing config
34
- config = JSON.parse(File.read(project_json_path))
35
- config["folders"] ||= []
36
-
37
- # Check if folder already exists
38
- if config["folders"].any? { |f| f["path"] == folder_path }
39
- Aircana.human_logger.warn "Folder already configured: #{folder_path}"
40
- return
41
- end
42
-
43
- # Add the folder
44
- config["folders"] << { "path" => folder_path }
45
-
46
- # Save updated config
47
- File.write(project_json_path, JSON.pretty_generate(config))
48
-
49
- Aircana.human_logger.success "Added folder: #{folder_path}"
50
-
51
- # Check what agents/knowledge would be available
52
- check_folder_contents(folder_path)
53
-
54
- # Offer to sync
55
- prompt = TTY::Prompt.new
56
- sync if prompt.yes?("Would you like to sync symlinks now?")
57
- end
58
-
59
- def remove(folder_path)
60
- project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
61
-
62
- unless File.exist?(project_json_path)
63
- Aircana.human_logger.error "No project.json found. Run 'aircana project init' first."
64
- return
65
- end
66
-
67
- # Load existing config
68
- config = JSON.parse(File.read(project_json_path))
69
- config["folders"] ||= []
70
-
71
- # Remove the folder
72
- original_count = config["folders"].size
73
- config["folders"].reject! { |f| f["path"] == folder_path }
74
-
75
- if config["folders"].size == original_count
76
- Aircana.human_logger.warn "Folder not found in configuration: #{folder_path}"
77
- return
78
- end
79
-
80
- # Save updated config
81
- File.write(project_json_path, JSON.pretty_generate(config))
82
-
83
- Aircana.human_logger.success "Removed folder: #{folder_path}"
84
-
85
- # Clean up symlinks
86
- Aircana::SymlinkManager.cleanup_broken_symlinks
87
- end
88
-
89
- def list
90
- project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
91
-
92
- unless File.exist?(project_json_path)
93
- Aircana.human_logger.info "No project.json found. Run 'aircana project init' to create one."
94
- return
95
- end
96
-
97
- config = JSON.parse(File.read(project_json_path))
98
- folders = config["folders"] || []
99
-
100
- if folders.empty?
101
- Aircana.human_logger.info "No folders configured."
102
- Aircana.human_logger.info "Add folders using: aircana project add <path>"
103
- return
104
- end
105
-
106
- Aircana.human_logger.info "Configured folders:"
107
- folders.each do |folder|
108
- folder_path = folder["path"]
109
- status = Dir.exist?(File.join(Aircana.configuration.project_dir, folder_path)) ? "✓" : "✗"
110
- Aircana.human_logger.info " #{status} #{folder_path}"
111
-
112
- # Show available agents if folder exists
113
- check_folder_contents(folder_path, indent: " ") if status == "✓"
114
- end
115
- end
116
-
117
- def sync
118
- Aircana.human_logger.info "Syncing multi-root project symlinks..."
119
-
120
- stats = Aircana::SymlinkManager.sync_multi_root_agents
121
-
122
- if stats[:agents].zero? && stats[:knowledge].zero?
123
- Aircana.human_logger.info "No agents or knowledge bases to link."
124
- else
125
- Aircana.human_logger.success "Sync complete: #{stats[:agents]} agents, #{stats[:knowledge]} knowledge bases"
126
- end
127
- end
128
-
129
- private
130
-
131
- def check_folder_contents(folder_path, indent: " ")
132
- agents_dir = File.join(folder_path, ".claude", "agents")
133
- knowledge_dir = File.join(folder_path, ".aircana", "agents")
134
-
135
- agents = []
136
- knowledge = []
137
-
138
- agents = Dir.glob("#{agents_dir}/*.md").map { |f| File.basename(f, ".md") } if Dir.exist?(agents_dir)
139
-
140
- if Dir.exist?(knowledge_dir)
141
- knowledge = Dir.glob("#{knowledge_dir}/*").select { |d| File.directory?(d) }
142
- .map { |d| File.basename(d) }
143
- end
144
-
145
- Aircana.human_logger.info "#{indent}Agents: #{agents.join(", ")}" if agents.any?
146
-
147
- Aircana.human_logger.info "#{indent}Knowledge: #{knowledge.join(", ")}" if knowledge.any?
148
-
149
- return unless agents.empty? && knowledge.empty?
150
-
151
- Aircana.human_logger.info "#{indent}No agents or knowledge found"
152
- end
153
- end
154
- end
155
- end
156
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base_generator"
4
- require "json"
5
-
6
- module Aircana
7
- module Generators
8
- class ProjectConfigGenerator < BaseGenerator
9
- def initialize(file_in: nil, file_out: nil)
10
- super(
11
- file_in: file_in || default_template_path,
12
- file_out: file_out || default_output_path
13
- )
14
- end
15
-
16
- def generate
17
- # Create the project.json with default content
18
- project_config = default_project_config
19
-
20
- Aircana.create_dir_if_needed(File.dirname(file_out))
21
- File.write(file_out, JSON.pretty_generate(project_config))
22
-
23
- Aircana.human_logger.success "Generated project.json at #{file_out}"
24
- file_out
25
- end
26
-
27
- private
28
-
29
- def default_template_path
30
- # We don't use a template for this, generate directly
31
- nil
32
- end
33
-
34
- def default_output_path
35
- File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
36
- end
37
-
38
- def default_project_config
39
- {
40
- "folders" => [],
41
- "_comment" => [
42
- "Add folders to include agents from sub-projects",
43
- "Example:",
44
- " 'folders': [",
45
- " { 'path': 'frontend' },",
46
- " { 'path': 'backend' },",
47
- " { 'path': 'shared/utils' }",
48
- " ]"
49
- ]
50
- }
51
- end
52
- end
53
- end
54
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fileutils"
5
-
6
- module Aircana
7
- class SymlinkManager
8
- class << self
9
- def sync_multi_root_agents
10
- project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
11
-
12
- unless File.exist?(project_json_path)
13
- Aircana.human_logger.info "No project.json found, skipping multi-root sync"
14
- return { agents: 0, knowledge: 0 }
15
- end
16
-
17
- begin
18
- config = JSON.parse(File.read(project_json_path))
19
- folders = config["folders"] || []
20
-
21
- if folders.empty?
22
- Aircana.human_logger.info "No folders configured in project.json"
23
- return { agents: 0, knowledge: 0 }
24
- end
25
-
26
- cleanup_broken_symlinks
27
- create_symlinks_for_folders(folders)
28
- rescue JSON::ParserError => e
29
- Aircana.human_logger.error "Invalid JSON in project.json: #{e.message}"
30
- { agents: 0, knowledge: 0 }
31
- end
32
- end
33
-
34
- def cleanup_broken_symlinks
35
- claude_agents_dir = File.join(Aircana.configuration.project_dir, ".claude", "agents")
36
- aircana_agents_dir = File.join(Aircana.configuration.project_dir, ".aircana", "agents")
37
-
38
- [claude_agents_dir, aircana_agents_dir].each do |dir|
39
- next unless Dir.exist?(dir)
40
-
41
- Dir.glob("#{dir}/*").each do |path|
42
- if File.symlink?(path) && !File.exist?(path)
43
- File.delete(path)
44
- Aircana.human_logger.info "Removed broken symlink: #{path}"
45
- end
46
- end
47
- end
48
- end
49
-
50
- def create_symlinks_for_folders(folders)
51
- stats = { agents: 0, knowledge: 0 }
52
-
53
- folders.each do |folder_config|
54
- folder_path = folder_config["path"]
55
- next unless folder_path_valid?(folder_path)
56
-
57
- prefix = folder_path.tr("/", "_")
58
-
59
- stats[:agents] += link_agents(folder_path, prefix)
60
- stats[:knowledge] += link_knowledge(folder_path, prefix)
61
- end
62
-
63
- Aircana.human_logger.success "Linked #{stats[:agents]} agents and #{stats[:knowledge]} knowledge bases"
64
- stats
65
- end
66
-
67
- def link_agents(folder_path, prefix)
68
- source_dir = File.join(folder_path, ".claude", "agents")
69
- target_dir = File.join(Aircana.configuration.project_dir, ".claude", "agents")
70
-
71
- return 0 unless Dir.exist?(source_dir)
72
-
73
- FileUtils.mkdir_p(target_dir)
74
- linked = 0
75
-
76
- Dir.glob("#{source_dir}/*.md").each do |agent_file|
77
- agent_name = File.basename(agent_file, ".md")
78
- link_name = "#{prefix}_#{agent_name}.md"
79
- target_path = File.join(target_dir, link_name)
80
-
81
- # Use relative paths for symlinks
82
- relative_path = calculate_relative_path(target_dir, agent_file)
83
-
84
- File.symlink(relative_path, target_path) unless File.exist?(target_path)
85
- linked += 1
86
- Aircana.human_logger.info "Linked agent: #{link_name}"
87
- end
88
-
89
- linked
90
- end
91
-
92
- def link_knowledge(folder_path, prefix)
93
- source_dir = File.join(folder_path, ".aircana", "agents")
94
- target_dir = File.join(Aircana.configuration.project_dir, ".aircana", "agents")
95
-
96
- return 0 unless Dir.exist?(source_dir)
97
-
98
- FileUtils.mkdir_p(target_dir)
99
- linked = 0
100
-
101
- Dir.glob("#{source_dir}/*").each do |agent_dir|
102
- next unless File.directory?(agent_dir)
103
-
104
- agent_name = File.basename(agent_dir)
105
- link_name = "#{prefix}_#{agent_name}"
106
- target_path = File.join(target_dir, link_name)
107
-
108
- # Use relative paths for symlinks
109
- relative_path = calculate_relative_path(target_dir, agent_dir)
110
-
111
- File.symlink(relative_path, target_path) unless File.exist?(target_path)
112
- linked += 1
113
- Aircana.human_logger.info "Linked knowledge: #{link_name}"
114
- end
115
-
116
- linked
117
- end
118
-
119
- def folder_path_valid?(folder_path)
120
- full_path = File.join(Aircana.configuration.project_dir, folder_path)
121
-
122
- unless Dir.exist?(full_path)
123
- Aircana.human_logger.warn "Folder not found: #{folder_path}"
124
- return false
125
- end
126
-
127
- true
128
- end
129
-
130
- def calculate_relative_path(from_dir, to_path)
131
- # Calculate the relative path from one directory to another
132
- from = Pathname.new(File.expand_path(from_dir))
133
- to = Pathname.new(File.expand_path(to_path))
134
- to.relative_path_from(from).to_s
135
- end
136
-
137
- # Helper methods for resolving symlinked agent paths
138
- def resolve_agent_path(agent_name)
139
- agent_path = File.join(Aircana.configuration.agent_knowledge_dir, agent_name)
140
-
141
- if File.symlink?(agent_path)
142
- File.readlink(agent_path)
143
- else
144
- agent_path
145
- end
146
- end
147
-
148
- def agent_is_symlinked?(agent_name)
149
- agent_path = File.join(Aircana.configuration.agent_knowledge_dir, agent_name)
150
- File.symlink?(agent_path)
151
- end
152
-
153
- def resolve_symlinked_path(path)
154
- File.symlink?(path) ? File.readlink(path) : path
155
- end
156
- end
157
- end
158
- end