sxn 0.3.0 → 0.4.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.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sxn
4
+ module MCP
5
+ module Tools
6
+ # Base module for MCP tools with shared error handling
7
+ module BaseTool
8
+ # Maps sxn errors to user-friendly messages
9
+ module ErrorMapping
10
+ SXN_ERROR_MESSAGES = {
11
+ # Session errors
12
+ Sxn::SessionNotFoundError => "Session not found",
13
+ Sxn::SessionAlreadyExistsError => "Session already exists",
14
+ Sxn::SessionExistsError => "Session already exists",
15
+ Sxn::SessionHasChangesError => "Session has uncommitted changes",
16
+ Sxn::InvalidSessionNameError => "Invalid session name",
17
+ Sxn::NoActiveSessionError => "No active session",
18
+ # Project errors
19
+ Sxn::ProjectNotFoundError => "Project not found",
20
+ Sxn::ProjectAlreadyExistsError => "Project already exists",
21
+ Sxn::ProjectExistsError => "Project already exists",
22
+ Sxn::ProjectInUseError => "Project is in use",
23
+ Sxn::InvalidProjectNameError => "Invalid project name",
24
+ Sxn::InvalidProjectPathError => "Invalid project path",
25
+ # Worktree errors
26
+ Sxn::WorktreeError => "Worktree error",
27
+ Sxn::WorktreeExistsError => "Worktree already exists",
28
+ Sxn::WorktreeNotFoundError => "Worktree not found",
29
+ Sxn::WorktreeCreationError => "Failed to create worktree",
30
+ Sxn::WorktreeRemovalError => "Failed to remove worktree",
31
+ # Template errors
32
+ Sxn::SessionTemplateNotFoundError => "Template not found",
33
+ Sxn::SessionTemplateValidationError => "Template validation failed",
34
+ # Rule errors
35
+ Sxn::RuleError => "Rule error",
36
+ Sxn::RuleNotFoundError => "Rule not found",
37
+ Sxn::InvalidRuleTypeError => "Invalid rule type",
38
+ Sxn::InvalidRuleConfigError => "Invalid rule configuration",
39
+ # Configuration errors
40
+ Sxn::ConfigurationError => "Configuration error",
41
+ # MCP errors
42
+ Sxn::MCPError => "MCP error",
43
+ Sxn::MCPServerError => "MCP server error",
44
+ Sxn::MCPValidationError => "MCP validation error"
45
+ }.freeze
46
+
47
+ # Wrap a block with error handling that converts sxn errors to error responses
48
+ def self.wrap
49
+ yield
50
+ rescue Sxn::ConfigurationError => e
51
+ error_response("sxn not initialized: #{e.message}. Run 'sxn init' first.")
52
+ rescue Sxn::Error => e
53
+ error_type = SXN_ERROR_MESSAGES[e.class] || "Error"
54
+ error_response("#{error_type}: #{e.message}")
55
+ rescue StandardError => e
56
+ error_response("Unexpected error: #{e.message}")
57
+ end
58
+
59
+ def self.error_response(message)
60
+ ::MCP::Tool::Response.new([{ type: "text", text: message }], error: true)
61
+ end
62
+ end
63
+
64
+ # Helper to check if sxn is initialized
65
+ def self.ensure_initialized!(server_context)
66
+ return true if server_context[:config_manager]
67
+
68
+ false
69
+ end
70
+
71
+ # Helper to return error response for uninitialized sxn
72
+ def self.not_initialized_response
73
+ ErrorMapping.error_response("sxn not initialized in this workspace. Run 'sxn init' first.")
74
+ end
75
+
76
+ # Helper to build a successful text response
77
+ def self.text_response(text)
78
+ ::MCP::Tool::Response.new([{ type: "text", text: text }])
79
+ end
80
+
81
+ # Helper to build a JSON response
82
+ def self.json_response(data, summary: nil)
83
+ content = []
84
+ content << { type: "text", text: summary } if summary
85
+ content << { type: "text", text: JSON.pretty_generate(data) }
86
+ ::MCP::Tool::Response.new(content)
87
+ end
88
+
89
+ # Helper to build an error response
90
+ def self.error_response(message)
91
+ ErrorMapping.error_response(message)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sxn
4
+ module MCP
5
+ module Tools
6
+ module Projects
7
+ # List registered projects
8
+ class ListProjects < ::MCP::Tool
9
+ description "List all registered projects in the sxn workspace"
10
+
11
+ input_schema(
12
+ type: "object",
13
+ properties: {},
14
+ required: []
15
+ )
16
+
17
+ class << self
18
+ def call(server_context:)
19
+ BaseTool.ensure_initialized!(server_context)
20
+
21
+ BaseTool::ErrorMapping.wrap do
22
+ project_manager = server_context[:project_manager]
23
+ projects = project_manager.list_projects
24
+
25
+ if projects.empty?
26
+ BaseTool.text_response("No projects registered. Use sxn_projects_add to register a project.")
27
+ else
28
+ formatted = projects.map do |p|
29
+ "- #{p[:name]} (#{p[:type]})\n #{p[:path]}\n Default branch: #{p[:default_branch]}"
30
+ end.join("\n")
31
+
32
+ BaseTool.text_response("Registered projects (#{projects.length}):\n#{formatted}")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Register a new project
40
+ class AddProject < ::MCP::Tool
41
+ description "Register a new git repository as a project. " \
42
+ "Automatically detects project type and default branch."
43
+
44
+ input_schema(
45
+ type: "object",
46
+ properties: {
47
+ name: {
48
+ type: "string",
49
+ pattern: "^[a-zA-Z0-9_-]+$",
50
+ description: "Project name (alphanumeric, hyphens, underscores only)"
51
+ },
52
+ path: {
53
+ type: "string",
54
+ description: "Path to the git repository (absolute or relative)"
55
+ },
56
+ type: {
57
+ type: "string",
58
+ description: "Project type (auto-detected if not specified). " \
59
+ "Options: rails, ruby, javascript, typescript, react, nextjs, etc."
60
+ },
61
+ default_branch: {
62
+ type: "string",
63
+ description: "Default branch for worktrees (auto-detected if not specified)"
64
+ }
65
+ },
66
+ required: %w[name path]
67
+ )
68
+
69
+ class << self
70
+ def call(name:, path:, server_context:, type: nil, default_branch: nil)
71
+ BaseTool.ensure_initialized!(server_context)
72
+
73
+ BaseTool::ErrorMapping.wrap do
74
+ project_manager = server_context[:project_manager]
75
+
76
+ result = project_manager.add_project(
77
+ name,
78
+ path,
79
+ type: type,
80
+ default_branch: default_branch
81
+ )
82
+
83
+ BaseTool.text_response(
84
+ "Project '#{result[:name]}' registered successfully.\n" \
85
+ "Type: #{result[:type]}\n" \
86
+ "Path: #{result[:path]}\n" \
87
+ "Default branch: #{result[:default_branch]}"
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # Get project details
95
+ class GetProject < ::MCP::Tool
96
+ description "Get detailed information about a registered project"
97
+
98
+ input_schema(
99
+ type: "object",
100
+ properties: {
101
+ name: {
102
+ type: "string",
103
+ description: "Project name to retrieve"
104
+ }
105
+ },
106
+ required: ["name"]
107
+ )
108
+
109
+ class << self
110
+ def call(name:, server_context:)
111
+ BaseTool.ensure_initialized!(server_context)
112
+
113
+ BaseTool::ErrorMapping.wrap do
114
+ project_manager = server_context[:project_manager]
115
+ project = project_manager.get_project(name)
116
+
117
+ # Validate the project
118
+ validation = project_manager.validate_project(name)
119
+
120
+ # Get project rules
121
+ rules = project_manager.get_project_rules(name)
122
+ rules_summary = rules.map { |type, configs| "#{type}: #{Array(configs).length}" }.join(", ")
123
+
124
+ output = <<~INFO
125
+ Project: #{project[:name]}
126
+ Type: #{project[:type]}
127
+ Path: #{project[:path]}
128
+ Default branch: #{project[:default_branch]}
129
+
130
+ Validation: #{validation[:valid] ? "Valid" : "Invalid"}
131
+ #{validation[:issues].map { |i| " - #{i}" }.join("\n") unless validation[:valid]}
132
+
133
+ Rules: #{rules_summary.empty? ? "(none)" : rules_summary}
134
+ INFO
135
+
136
+ BaseTool.text_response(output.strip)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sxn
4
+ module MCP
5
+ module Tools
6
+ module Rules
7
+ # List rules for a project
8
+ class ListRules < ::MCP::Tool
9
+ description "List project rules (copy_files, setup_commands, templates)"
10
+
11
+ input_schema(
12
+ type: "object",
13
+ properties: {
14
+ project_name: {
15
+ type: "string",
16
+ description: "Project name (lists all rules if not specified)"
17
+ }
18
+ },
19
+ required: []
20
+ )
21
+
22
+ class << self
23
+ def call(server_context:, project_name: nil)
24
+ BaseTool.ensure_initialized!(server_context)
25
+
26
+ BaseTool::ErrorMapping.wrap do
27
+ rules_manager = server_context[:rules_manager]
28
+ rules = rules_manager.list_rules(project_name)
29
+
30
+ if rules.empty?
31
+ BaseTool.text_response("No rules defined.")
32
+ else
33
+ # Group by project
34
+ by_project = rules.group_by { |r| r[:project] }
35
+
36
+ output = by_project.map do |proj, proj_rules|
37
+ proj_output = "#{proj}:\n"
38
+ proj_rules.each do |rule|
39
+ config_preview = case rule[:type]
40
+ when "copy_files"
41
+ rule[:config]["source"]
42
+ when "setup_commands"
43
+ rule[:config]["command"]&.join(" ")
44
+ when "template"
45
+ "#{rule[:config]["source"]} -> #{rule[:config]["destination"]}"
46
+ else
47
+ rule[:config].to_s
48
+ end
49
+ proj_output += " - [#{rule[:type]}] #{config_preview}\n"
50
+ end
51
+ proj_output
52
+ end.join("\n")
53
+
54
+ BaseTool.text_response("Project rules:\n\n#{output}")
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Apply rules to a worktree
62
+ class ApplyRules < ::MCP::Tool
63
+ description "Apply project-specific rules to a worktree (copy files, run setup commands)"
64
+
65
+ input_schema(
66
+ type: "object",
67
+ properties: {
68
+ project_name: {
69
+ type: "string",
70
+ description: "Project name"
71
+ },
72
+ session_name: {
73
+ type: "string",
74
+ description: "Session name (defaults to current session)"
75
+ }
76
+ },
77
+ required: ["project_name"]
78
+ )
79
+
80
+ class << self
81
+ def call(project_name:, server_context:, session_name: nil)
82
+ BaseTool.ensure_initialized!(server_context)
83
+
84
+ BaseTool::ErrorMapping.wrap do
85
+ rules_manager = server_context[:rules_manager]
86
+
87
+ result = rules_manager.apply_rules(project_name, session_name)
88
+
89
+ if result[:success]
90
+ BaseTool.text_response(
91
+ "Rules applied successfully to '#{project_name}'.\n" \
92
+ "Applied: #{result[:applied_count]} rule(s)"
93
+ )
94
+ else
95
+ BaseTool.text_response(
96
+ "Some rules failed for '#{project_name}'.\n" \
97
+ "Applied: #{result[:applied_count]} rule(s)\n" \
98
+ "Errors:\n#{result[:errors].map { |e| " - #{e}" }.join("\n")}"
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end