aircana 2.0.0 → 3.0.0.rc1

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.
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "json"
4
4
  require_relative "generate"
5
- require_relative "../../generators/project_config_generator"
6
5
 
7
6
  module Aircana
8
7
  module CLI
@@ -10,7 +9,6 @@ module Aircana
10
9
  class << self
11
10
  def run
12
11
  generate_files
13
- ensure_project_config_exists
14
12
  install_commands_to_claude
15
13
  install_hooks_to_claude
16
14
  end
@@ -22,14 +20,6 @@ module Aircana
22
20
  Generate.run
23
21
  end
24
22
 
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
23
  def install_commands_to_claude
34
24
  claude_commands_dir = File.join(Aircana.configuration.claude_code_project_config_path, "commands")
35
25
  Aircana.create_dir_if_needed(claude_commands_dir)
@@ -26,7 +26,6 @@ module Aircana
26
26
  "File Management" => %w[files],
27
27
  "Agent Management" => %w[agents],
28
28
  "Hook Management" => %w[hooks],
29
- "Project Management" => %w[project],
30
29
  "System" => %w[generate install doctor dump-context]
31
30
  }
32
31
  end
@@ -53,7 +52,7 @@ module Aircana
53
52
  end
54
53
 
55
54
  def subcommand?(cmd_name)
56
- %w[files agents hooks project].include?(cmd_name)
55
+ %w[files agents hooks].include?(cmd_name)
57
56
  end
58
57
 
59
58
  def print_subcommand_group(subcommand_name, cmd)
@@ -19,7 +19,7 @@ module Aircana
19
19
  @global_dir = File.join(Dir.home, ".aircana")
20
20
  @project_dir = Dir.pwd
21
21
  @output_dir = File.join(@global_dir, "aircana.out")
22
- @agent_knowledge_dir = File.join(@project_dir, ".aircana", "agents")
22
+ @agent_knowledge_dir = File.join(@project_dir, ".claude", "agents")
23
23
  @hooks_dir = File.join(@project_dir, ".aircana", "hooks")
24
24
  end
25
25
 
@@ -73,14 +73,7 @@ module Aircana
73
73
  end
74
74
 
75
75
  def resolve_agent_path(agent)
76
- base_path = File.join(Aircana.configuration.agent_knowledge_dir, agent)
77
-
78
- # If this is a symlink (multi-root scenario), resolve to original
79
- if File.symlink?(base_path)
80
- File.readlink(base_path)
81
- else
82
- base_path
83
- end
76
+ File.join(Aircana.configuration.agent_knowledge_dir, agent)
84
77
  end
85
78
 
86
79
  def build_manifest_data(agent, sources)
@@ -64,7 +64,7 @@ module Aircana
64
64
  end
65
65
 
66
66
  def knowledge_path
67
- ".aircana/agents/#{agent_name}/knowledge/"
67
+ ".claude/agents/#{agent_name}/knowledge/"
68
68
  end
69
69
  end
70
70
  end
@@ -23,16 +23,6 @@ module Aircana
23
23
  "Arch" => "pacman -S git",
24
24
  "Other" => "https://git-scm.com/downloads"
25
25
  }
26
- },
27
- "jq" => {
28
- purpose: "JSON parsing for multi-root configuration",
29
- install: {
30
- "macOS" => "brew install jq",
31
- "Ubuntu/Debian" => "apt install jq",
32
- "Fedora/CentOS" => "dnf install jq",
33
- "Arch" => "pacman -S jq",
34
- "Other" => "https://jqlang.github.io/jq/download/"
35
- }
36
26
  }
37
27
  }.freeze
38
28
 
@@ -5,7 +5,7 @@ model: <%= model %>
5
5
  color: <%= color %>
6
6
  ---
7
7
 
8
- <%= helpers.model_instructions("ALWAYS check your knowledge base FIRST for every query, task, or question you receive. Use `ls .aircana/agents/#{agent_name}/knowledge/*.md` to list files from your knowledge base.
8
+ <%= helpers.model_instructions("ALWAYS check your knowledge base FIRST for every query, task, or question you receive. Use `ls .claude/agents/#{agent_name}/knowledge/*.md` to list files from your knowledge base.
9
9
 
10
10
  MANDATORY WORKFLOW:
11
11
  1. BEFORE responding to ANY request - search and read relevant files in your knowledge base
@@ -18,7 +18,7 @@ Your knowledge base contains domain-specific information that takes priority ove
18
18
 
19
19
  ## Knowledge Base Integration
20
20
 
21
- Your specialized knowledge is in the current project directory. Use `ls .aircana/agents/<%= agent_name %>/knowledge/*.md` to list available files, then read them with the Read tool.
21
+ Your specialized knowledge is in the current project directory. Use `ls .claude/agents/<%= agent_name %>/knowledge/*.md` to list available files, then read them with the Read tool.
22
22
 
23
23
  This knowledge base contains:
24
24
  - Domain-specific documentation from Confluence
@@ -1,127 +1,12 @@
1
1
  #!/bin/bash
2
- # Multi-root project support hook for Aircana
2
+ # Session start hook for Aircana
3
3
  # This hook runs when a new Claude Code session starts
4
4
 
5
- PROJECT_JSON=".aircana/project.json"
6
- CLAUDE_AGENTS_DIR=".claude/agents"
7
- AIRCANA_AGENTS_DIR=".aircana/agents"
8
-
9
5
  # Create log directory if it doesn't exist
10
6
  mkdir -p ~/.aircana
11
7
 
12
8
  # Log session start
13
9
  echo "$(date): New Claude Code session started in $(pwd)" >> ~/.aircana/hooks.log
14
10
 
15
- # Check if jq is available
16
- if ! command -v jq &> /dev/null; then
17
- echo "$(date): Warning - jq not found. Multi-root support disabled." >> ~/.aircana/hooks.log
18
- echo "{}"
19
- exit 0
20
- fi
21
-
22
- # Check if project.json exists
23
- if [ ! -f "$PROJECT_JSON" ]; then
24
- echo "$(date): No project.json found, skipping multi-root setup" >> ~/.aircana/hooks.log
25
- echo "{}"
26
- exit 0
27
- fi
28
-
29
- echo "$(date): Processing multi-root configuration from $PROJECT_JSON" >> ~/.aircana/hooks.log
30
-
31
- # Ensure directories exist
32
- mkdir -p "$CLAUDE_AGENTS_DIR" 2>/dev/null
33
- mkdir -p "$AIRCANA_AGENTS_DIR" 2>/dev/null
34
-
35
- # Clean up existing symlinks (only remove symlinks, not real files)
36
- find "$CLAUDE_AGENTS_DIR" -type l -delete 2>/dev/null
37
- find "$AIRCANA_AGENTS_DIR" -type l -delete 2>/dev/null
38
-
39
- # Parse folders from project.json
40
- FOLDERS=$(jq -r '.folders[]?.path // empty' "$PROJECT_JSON" 2>/dev/null)
41
-
42
- if [ -z "$FOLDERS" ]; then
43
- echo "$(date): No folders configured in project.json" >> ~/.aircana/hooks.log
44
- echo "{}"
45
- exit 0
46
- fi
47
-
48
- # Track what we've linked for reporting
49
- LINKED_AGENTS=0
50
- LINKED_KNOWLEDGE=0
51
-
52
- # Create symlinks for each configured folder
53
- for folder in $FOLDERS; do
54
- # Validate folder exists
55
- if [ ! -d "$folder" ]; then
56
- echo "$(date): Warning - folder '$folder' not found, skipping" >> ~/.aircana/hooks.log
57
- continue
58
- fi
59
-
60
- echo "$(date): Processing folder: $folder" >> ~/.aircana/hooks.log
61
-
62
- # Get folder name for prefix (replace slashes with underscores for nested paths)
63
- PREFIX=$(echo "$folder" | tr '/' '_')
64
-
65
- # Link agents from sub-folder .claude/agents
66
- if [ -d "$folder/.claude/agents" ]; then
67
- for agent_file in "$folder/.claude/agents"/*.md; do
68
- if [ ! -f "$agent_file" ]; then
69
- continue
70
- fi
71
-
72
- AGENT_NAME=$(basename "$agent_file" .md)
73
- LINK_NAME="${PREFIX}_${AGENT_NAME}.md"
74
- TARGET_PATH="$CLAUDE_AGENTS_DIR/$LINK_NAME"
75
-
76
- # Create relative path from .claude/agents to the agent file
77
- RELATIVE_PATH=$(realpath --relative-to="$CLAUDE_AGENTS_DIR" "$agent_file" 2>/dev/null)
78
-
79
- if [ -n "$RELATIVE_PATH" ]; then
80
- ln -sf "$RELATIVE_PATH" "$TARGET_PATH"
81
- echo "$(date): Linked agent: $LINK_NAME -> $RELATIVE_PATH" >> ~/.aircana/hooks.log
82
- ((LINKED_AGENTS++))
83
- fi
84
- done
85
- fi
86
-
87
- # Link knowledge from sub-folder .aircana/agents
88
- if [ -d "$folder/.aircana/agents" ]; then
89
- for agent_dir in "$folder/.aircana/agents"/*; do
90
- if [ ! -d "$agent_dir" ]; then
91
- continue
92
- fi
93
-
94
- AGENT_NAME=$(basename "$agent_dir")
95
- LINK_NAME="${PREFIX}_${AGENT_NAME}"
96
- TARGET_PATH="$AIRCANA_AGENTS_DIR/$LINK_NAME"
97
-
98
- # Create relative path from .aircana/agents to the knowledge directory
99
- RELATIVE_PATH=$(realpath --relative-to="$AIRCANA_AGENTS_DIR" "$agent_dir" 2>/dev/null)
100
-
101
- if [ -n "$RELATIVE_PATH" ]; then
102
- ln -sf "$RELATIVE_PATH" "$TARGET_PATH"
103
- echo "$(date): Linked knowledge: $LINK_NAME -> $RELATIVE_PATH" >> ~/.aircana/hooks.log
104
- ((LINKED_KNOWLEDGE++))
105
- fi
106
- done
107
- fi
108
- done
109
-
110
- # Report results
111
- echo "$(date): Multi-root setup complete - linked $LINKED_AGENTS agents and $LINKED_KNOWLEDGE knowledge bases" >> ~/.aircana/hooks.log
112
-
113
- # Return success with optional context
114
- if [ $LINKED_AGENTS -gt 0 ] || [ $LINKED_KNOWLEDGE -gt 0 ]; then
115
- CONTEXT="Multi-root: Linked $LINKED_AGENTS agents and $LINKED_KNOWLEDGE knowledge bases from configured folders."
116
- ESCAPED_CONTEXT=$(echo -n "$CONTEXT" | sed 's/"/\\"/g')
117
- cat << EOF
118
- {
119
- "hookSpecificOutput": {
120
- "hookEventName": "SessionStart",
121
- "additionalContext": "$ESCAPED_CONTEXT"
122
- }
123
- }
124
- EOF
125
- else
126
- echo "{}"
127
- fi
11
+ # Return success
12
+ echo "{}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aircana
4
- VERSION = "2.0.0"
4
+ VERSION = "3.0.0.rc1"
5
5
  end
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.rc1
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
@@ -126,7 +126,6 @@ files:
126
126
  - lib/aircana/cli/commands/generate.rb
127
127
  - lib/aircana/cli/commands/hooks.rb
128
128
  - lib/aircana/cli/commands/install.rb
129
- - lib/aircana/cli/commands/project.rb
130
129
  - lib/aircana/cli/help_formatter.rb
131
130
  - lib/aircana/cli/shell_command.rb
132
131
  - lib/aircana/cli/subcommand.rb
@@ -149,14 +148,12 @@ files:
149
148
  - lib/aircana/generators/helpers.rb
150
149
  - lib/aircana/generators/hooks_generator.rb
151
150
  - lib/aircana/generators/plan_command_generator.rb
152
- - lib/aircana/generators/project_config_generator.rb
153
151
  - lib/aircana/generators/record_command_generator.rb
154
152
  - lib/aircana/generators/review_command_generator.rb
155
153
  - lib/aircana/human_logger.rb
156
154
  - lib/aircana/initializers.rb
157
155
  - lib/aircana/llm/claude_client.rb
158
156
  - lib/aircana/progress_tracker.rb
159
- - lib/aircana/symlink_manager.rb
160
157
  - lib/aircana/system_checker.rb
161
158
  - lib/aircana/templates/agents/base_agent.erb
162
159
  - lib/aircana/templates/agents/defaults/apply_feedback.erb
@@ -204,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
201
  - !ruby/object:Gem::Version
205
202
  version: '0'
206
203
  requirements: []
207
- rubygems_version: 3.6.2
204
+ rubygems_version: 3.6.9
208
205
  specification_version: 4
209
206
  summary: Humble workflow and context utilities for engineering with agents
210
207
  test_files: []
@@ -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