language-operator 0.1.61 → 0.1.62

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require_relative '../../command_loader'
6
+ require_relative '../../helpers/label_utils'
7
+
8
+ # Include all tool subcommand modules
9
+ require_relative 'install'
10
+ require_relative 'test'
11
+ require_relative 'search'
12
+
13
+ module LanguageOperator
14
+ module CLI
15
+ module Commands
16
+ module Tool
17
+ # Base tool command class
18
+ class Base < BaseCommand
19
+ include Constants
20
+ include CLI::Helpers::ClusterValidator
21
+
22
+ # Include all subcommand modules
23
+ include Install
24
+ include Test
25
+ include Search
26
+
27
+ desc 'list', 'List all tools in current cluster'
28
+ option :cluster, type: :string, desc: 'Override current cluster context'
29
+ def list
30
+ handle_command_error('list tools') do
31
+ tools = list_resources_or_empty(RESOURCE_TOOL, resource_name: 'tools') do
32
+ puts
33
+ puts 'Install a tool with:'
34
+ puts ' aictl tool install <name>'
35
+ end
36
+
37
+ return if tools.empty?
38
+
39
+ # Get agents to count usage
40
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
41
+
42
+ table_data = tools.map do |tool|
43
+ name = tool.dig('metadata', 'name')
44
+ tool.dig('spec', 'type') || 'unknown'
45
+ status = tool.dig('status', 'phase') || 'Unknown'
46
+
47
+ # Count agents using this tool
48
+ Helpers::ResourceDependencyChecker.tool_usage_count(agents, name)
49
+
50
+ # Get health status
51
+ health = tool.dig('status', 'health') || 'unknown'
52
+ case health.downcase
53
+ when 'healthy' then '✓'
54
+ when 'unhealthy' then '✗'
55
+ else '?'
56
+ end
57
+
58
+ {
59
+ name: name,
60
+ namespace: tool.dig('metadata', 'namespace') || ctx.namespace,
61
+ status: status
62
+ }
63
+ end
64
+
65
+ Formatters::TableFormatter.tools(table_data)
66
+ end
67
+ end
68
+
69
+ desc 'inspect NAME', 'Show detailed tool information'
70
+ option :cluster, type: :string, desc: 'Override current cluster context'
71
+ def inspect(name)
72
+ handle_command_error('inspect tool') do
73
+ tool = get_resource_or_exit(RESOURCE_TOOL, name)
74
+
75
+ # Main tool information
76
+ puts
77
+ highlighted_box(
78
+ title: RESOURCE_TOOL,
79
+ rows: {
80
+ 'Name' => pastel.white.bold(name),
81
+ 'Namespace' => ctx.namespace,
82
+ 'Cluster' => ctx.name,
83
+ 'Status' => tool.dig('status', 'phase') || 'Unknown',
84
+ 'Type' => tool.dig('spec', 'type') || 'mcp',
85
+ 'Image' => tool.dig('spec', 'image'),
86
+ 'Deployment Mode' => tool.dig('spec', 'deploymentMode') || 'sidecar',
87
+ 'Port' => tool.dig('spec', 'port') || 8080,
88
+ 'Replicas' => tool.dig('spec', 'replicas') || 1
89
+ }
90
+ )
91
+ puts
92
+
93
+ # Resources
94
+ resources = tool.dig('spec', 'resources')
95
+ if resources
96
+ resource_rows = {}
97
+ requests = resources['requests'] || {}
98
+ limits = resources['limits'] || {}
99
+
100
+ # CPU
101
+ cpu_request = requests['cpu']
102
+ cpu_limit = limits['cpu']
103
+ resource_rows['CPU'] = [cpu_request, cpu_limit].compact.join(' / ') if cpu_request || cpu_limit
104
+
105
+ # Memory
106
+ memory_request = requests['memory']
107
+ memory_limit = limits['memory']
108
+ resource_rows['Memory'] = [memory_request, memory_limit].compact.join(' / ') if memory_request || memory_limit
109
+
110
+ highlighted_box(title: 'Resources (Request/Limit)', rows: resource_rows, color: :cyan) unless resource_rows.empty?
111
+ puts
112
+ end
113
+
114
+ # RBAC
115
+ rbac = tool.dig('spec', 'rbac')
116
+ if rbac && rbac['clusterRole']
117
+ rules = rbac.dig('clusterRole', 'rules') || []
118
+ puts "RBAC Permissions (#{rules.length} rules):"
119
+ rules.each_with_index do |rule, idx|
120
+ puts " Rule #{idx + 1}:"
121
+ puts " API Groups: #{rule['apiGroups'].join(', ')}"
122
+ puts " Resources: #{rule['resources'].join(', ')}"
123
+ puts " Verbs: #{rule['verbs'].join(', ')}"
124
+ end
125
+ puts
126
+ end
127
+
128
+ # Egress rules
129
+ egress = tool.dig('spec', 'egress') || []
130
+ if egress.any?
131
+ puts "Network Egress (#{egress.length} rules):"
132
+ egress.each_with_index do |rule, idx|
133
+ puts " Rule #{idx + 1}: #{rule['description']}"
134
+ puts " DNS: #{rule['dns'].join(', ')}" if rule['dns']
135
+ puts " CIDR: #{rule['cidr']}" if rule['cidr']
136
+ if rule['ports']
137
+ ports_str = rule['ports'].map { |p| "#{p['port']}/#{p['protocol']}" }.join(', ')
138
+ puts " Ports: #{ports_str}"
139
+ end
140
+ end
141
+ puts
142
+ end
143
+
144
+ # Try to fetch MCP capabilities
145
+ capabilities = fetch_mcp_capabilities(name, tool, ctx.namespace)
146
+ if capabilities && capabilities['tools'] && capabilities['tools'].any?
147
+ puts "MCP Tools (#{capabilities['tools'].length}):"
148
+ capabilities['tools'].each_with_index do |mcp_tool, idx|
149
+ tool_name = mcp_tool['name']
150
+
151
+ # Generate a meaningful name if empty
152
+ if tool_name.nil? || tool_name.empty?
153
+ # Try to derive from description (first few words)
154
+ if mcp_tool['description']
155
+ # Take first 3-4 words and convert to snake_case
156
+ words = mcp_tool['description'].split(/\s+/).first(4)
157
+ derived_name = words.join('_').downcase.gsub(/[^a-z0-9_]/, '')
158
+ tool_name = "#{name}_#{derived_name}".gsub(/__+/, '_').sub(/_$/, '')
159
+ else
160
+ tool_name = "#{name}_tool_#{idx + 1}"
161
+ end
162
+ end
163
+
164
+ puts " #{tool_name}"
165
+ puts " Description: #{mcp_tool['description']}" if mcp_tool['description']
166
+ next unless mcp_tool['inputSchema'] && mcp_tool['inputSchema']['properties']
167
+
168
+ params = mcp_tool['inputSchema']['properties'].keys
169
+ required = mcp_tool['inputSchema']['required'] || []
170
+ param_list = params.map { |p| required.include?(p) ? "#{p}*" : p }
171
+ puts " Parameters: #{param_list.join(', ')}"
172
+ end
173
+ puts ' (* = required)'
174
+ puts
175
+ end
176
+
177
+ # Get agents using this tool
178
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
179
+ agents_using = agents.select do |agent|
180
+ tools = agent.dig('spec', 'tools') || []
181
+ tools.include?(name)
182
+ end
183
+ agent_names = agents_using.map { |agent| agent.dig('metadata', 'name') }
184
+
185
+ list_box(
186
+ title: 'Agents using this tool',
187
+ items: agent_names
188
+ )
189
+
190
+ puts
191
+ labels = tool.dig('metadata', 'labels') || {}
192
+ list_box(
193
+ title: 'Labels',
194
+ items: labels,
195
+ style: :key_value
196
+ )
197
+ end
198
+ end
199
+
200
+ desc 'delete NAME', 'Delete a tool'
201
+ option :cluster, type: :string, desc: 'Override current cluster context'
202
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
203
+ def delete(name)
204
+ handle_command_error('delete tool') do
205
+ get_resource_or_exit(RESOURCE_TOOL, name)
206
+
207
+ # Check dependencies and get confirmation
208
+ return unless check_dependencies_and_confirm('tool', name, force: options[:force])
209
+
210
+ # Confirm deletion unless --force
211
+ return unless confirm_deletion_with_force('tool', name, ctx.name, force: options[:force])
212
+
213
+ # Delete tool
214
+ Formatters::ProgressFormatter.with_spinner("Deleting tool '#{name}'") do
215
+ ctx.client.delete_resource(RESOURCE_TOOL, name, ctx.namespace)
216
+ end
217
+ end
218
+ end
219
+
220
+ private
221
+
222
+ # Fetch MCP capabilities from a running tool server
223
+ #
224
+ # @param name [String] Tool name
225
+ # @param tool [Hash] Tool resource
226
+ # @param namespace [String] Kubernetes namespace
227
+ # @return [Hash, nil] MCP capabilities or nil if unavailable
228
+ def fetch_mcp_capabilities(name, tool, namespace)
229
+ return nil unless tool.dig('status', 'phase') == 'Running'
230
+
231
+ # Get the service endpoint
232
+ port = tool.dig('spec', 'port') || 80
233
+
234
+ # Try to query the MCP server using kubectl port-forward
235
+ # This is a fallback approach since we can't directly connect from CLI
236
+ begin
237
+ # Try to find a pod for this tool
238
+ label_selector = CLI::Helpers::LabelUtils.agent_pod_selector(name)
239
+ pods = ctx.client.list_resources('Pod', namespace: namespace, label_selector: label_selector)
240
+
241
+ return nil if pods.empty?
242
+
243
+ pod_name = pods.first.dig('metadata', 'name')
244
+
245
+ # Query the MCP server using JSON-RPC protocol
246
+ # MCP uses the tools/list method to list available tools
247
+ json_rpc_request = {
248
+ jsonrpc: '2.0',
249
+ id: 1,
250
+ method: 'tools/list',
251
+ params: {}
252
+ }.to_json
253
+
254
+ result = `kubectl exec -n #{namespace} #{pod_name} -- curl -s -X POST \
255
+ http://localhost:#{port}/mcp/tools/list -H "Content-Type: application/json" \
256
+ -d '#{json_rpc_request}' 2>/dev/null`
257
+
258
+ return nil if result.empty?
259
+
260
+ response = JSON.parse(result)
261
+ response['result']
262
+ rescue StandardError
263
+ # Silently fail - capabilities are optional information
264
+ nil
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ module LanguageOperator
7
+ module CLI
8
+ module Commands
9
+ module Tool
10
+ # Tool installation and authentication commands
11
+ module Install
12
+ def self.included(base)
13
+ base.class_eval do
14
+ desc 'install NAME', 'Install a tool from the registry'
15
+ option :cluster, type: :string, desc: 'Override current cluster context'
16
+ option :deployment_mode, type: :string, enum: %w[service sidecar], desc: 'Deployment mode (service or sidecar)'
17
+ option :replicas, type: :numeric, desc: 'Number of replicas'
18
+ option :dry_run, type: :boolean, default: false, desc: 'Preview without installing'
19
+ def install(tool_name)
20
+ handle_command_error('install tool') do
21
+ # For dry-run mode, allow operation without a real cluster
22
+ if options[:dry_run]
23
+ cluster_name = options[:cluster] || 'preview'
24
+ namespace = 'default'
25
+ else
26
+ cluster_name = ctx.name
27
+ namespace = ctx.namespace
28
+ end
29
+
30
+ # Load tool patterns registry
31
+ registry = Config::ToolRegistry.new
32
+ patterns = registry.fetch
33
+
34
+ # Resolve aliases
35
+ tool_key = tool_name
36
+ tool_key = patterns[tool_key]['alias'] while patterns[tool_key]&.key?('alias')
37
+
38
+ # Look up tool in registry
39
+ tool_config = patterns[tool_key]
40
+ unless tool_config
41
+ Formatters::ProgressFormatter.error("Tool '#{tool_name}' not found in registry")
42
+ puts
43
+ puts 'Available tools:'
44
+ patterns.each do |key, config|
45
+ next if config['alias']
46
+
47
+ puts " #{key.ljust(15)} - #{config['description']}"
48
+ end
49
+ exit 1
50
+ end
51
+
52
+ # Build template variables
53
+ vars = {
54
+ name: tool_name,
55
+ namespace: namespace,
56
+ cluster_ref: cluster_name,
57
+ deployment_mode: options[:deployment_mode] || tool_config['deploymentMode'],
58
+ replicas: options[:replicas] || 1,
59
+ auth_secret: nil, # Will be set by auth command
60
+ image: tool_config['image'],
61
+ port: tool_config['port'],
62
+ type: tool_config['type'],
63
+ egress: tool_config['egress'],
64
+ rbac: tool_config['rbac']
65
+ }
66
+
67
+ # Get template content - prefer registry manifest, fall back to generic template
68
+ if tool_config['manifest']
69
+ # Use manifest from registry (if provided in the future)
70
+ template_content = tool_config['manifest']
71
+ else
72
+ # Use generic template for all tools
73
+ template_path = File.join(__dir__, '..', '..', 'templates', 'tools', 'generic.yaml')
74
+ template_content = File.read(template_path)
75
+ end
76
+
77
+ # Render template
78
+ template = ERB.new(template_content, trim_mode: '-')
79
+ yaml_content = template.result_with_hash(vars)
80
+
81
+ # Dry run mode
82
+ if options[:dry_run]
83
+ puts "Would install tool '#{tool_name}' to cluster '#{cluster_name}':"
84
+ puts
85
+ puts "Display Name: #{tool_config['displayName']}"
86
+ puts "Description: #{tool_config['description']}"
87
+ puts "Deployment Mode: #{vars[:deployment_mode]}"
88
+ puts "Replicas: #{vars[:replicas]}"
89
+ puts "Auth Required: #{tool_config['authRequired'] ? 'Yes' : 'No'}"
90
+ puts
91
+ puts 'Generated YAML:'
92
+ puts '---'
93
+ puts yaml_content
94
+ puts
95
+ puts 'To install for real, run without --dry-run'
96
+ return
97
+ end
98
+
99
+ # Check if already exists
100
+ begin
101
+ ctx.client.get_resource(LanguageOperator::Constants::RESOURCE_TOOL, tool_name, ctx.namespace)
102
+ Formatters::ProgressFormatter.warn("Tool '#{tool_name}' already exists in cluster '#{ctx.name}'")
103
+ puts
104
+ return unless CLI::Helpers::UserPrompts.confirm('Do you want to update it?')
105
+ rescue K8s::Error::NotFound
106
+ # Tool doesn't exist, proceed with creation
107
+ end
108
+
109
+ # Install tool
110
+ Formatters::ProgressFormatter.with_spinner("Installing tool '#{tool_name}'") do
111
+ resource = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
112
+ ctx.client.apply_resource(resource)
113
+ end
114
+
115
+ puts
116
+
117
+ # Show tool details
118
+ format_tool_details(
119
+ name: tool_name,
120
+ namespace: ctx.namespace,
121
+ cluster: ctx.name,
122
+ status: 'Ready',
123
+ image: tool_config['image'],
124
+ created: Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
125
+ )
126
+
127
+ puts
128
+ if tool_config['authRequired']
129
+ puts 'This tool requires authentication. Configure it with:'
130
+ puts pastel.dim(" aictl tool auth #{tool_name}")
131
+ else
132
+ puts "Tool '#{tool_name}' is now available for agents to use"
133
+ end
134
+ end
135
+ end
136
+
137
+ desc 'auth NAME', 'Configure authentication for a tool'
138
+ option :cluster, type: :string, desc: 'Override current cluster context'
139
+ def auth(tool_name)
140
+ handle_command_error('configure auth') do
141
+ tool = get_resource_or_exit(LanguageOperator::Constants::RESOURCE_TOOL, tool_name,
142
+ error_message: "Tool '#{tool_name}' not found. Install it first with: aictl tool install #{tool_name}")
143
+
144
+ puts "Configure authentication for tool '#{tool_name}'"
145
+ puts
146
+
147
+ # Determine auth type based on tool
148
+ case tool_name
149
+ when 'email', 'gmail'
150
+ puts 'Email/Gmail Configuration'
151
+ puts '-' * 40
152
+ print 'SMTP Server: '
153
+ smtp_server = $stdin.gets.chomp
154
+ print 'SMTP Port (587): '
155
+ smtp_port = $stdin.gets.chomp
156
+ smtp_port = '587' if smtp_port.empty?
157
+ print 'Email Address: '
158
+ email = $stdin.gets.chomp
159
+ print 'Password: '
160
+ password = $stdin.noecho(&:gets).chomp
161
+ puts
162
+
163
+ secret_data = {
164
+ 'SMTP_SERVER' => smtp_server,
165
+ 'SMTP_PORT' => smtp_port,
166
+ 'EMAIL_ADDRESS' => email,
167
+ 'EMAIL_PASSWORD' => password
168
+ }
169
+
170
+ when 'github'
171
+ puts 'GitHub Configuration'
172
+ puts '-' * 40
173
+ print 'GitHub Token: '
174
+ token = $stdin.noecho(&:gets).chomp
175
+ puts
176
+
177
+ secret_data = {
178
+ 'GITHUB_TOKEN' => token
179
+ }
180
+
181
+ when 'slack'
182
+ puts 'Slack Configuration'
183
+ puts '-' * 40
184
+ print 'Slack Bot Token: '
185
+ token = $stdin.noecho(&:gets).chomp
186
+ puts
187
+
188
+ secret_data = {
189
+ 'SLACK_BOT_TOKEN' => token
190
+ }
191
+
192
+ when 'gdrive'
193
+ puts 'Google Drive Configuration'
194
+ puts '-' * 40
195
+ puts 'Note: You need OAuth credentials from Google Cloud Console'
196
+ print 'Client ID: '
197
+ client_id = $stdin.gets.chomp
198
+ print 'Client Secret: '
199
+ client_secret = $stdin.noecho(&:gets).chomp
200
+ puts
201
+
202
+ secret_data = {
203
+ 'GDRIVE_CLIENT_ID' => client_id,
204
+ 'GDRIVE_CLIENT_SECRET' => client_secret
205
+ }
206
+
207
+ else
208
+ puts 'Generic API Key Configuration'
209
+ puts '-' * 40
210
+ print 'API Key: '
211
+ api_key = $stdin.noecho(&:gets).chomp
212
+ puts
213
+
214
+ secret_data = {
215
+ 'API_KEY' => api_key
216
+ }
217
+ end
218
+
219
+ # Create secret
220
+ secret_name = "#{tool_name}-auth"
221
+ secret_resource = {
222
+ 'apiVersion' => 'v1',
223
+ 'kind' => 'Secret',
224
+ 'metadata' => {
225
+ 'name' => secret_name,
226
+ 'namespace' => ctx.namespace
227
+ },
228
+ 'type' => 'Opaque',
229
+ 'stringData' => secret_data
230
+ }
231
+
232
+ Formatters::ProgressFormatter.with_spinner('Creating authentication secret') do
233
+ ctx.client.apply_resource(secret_resource)
234
+ end
235
+
236
+ # Update tool to use secret
237
+ tool['spec']['envFrom'] ||= []
238
+ tool['spec']['envFrom'] << { 'secretRef' => { 'name' => secret_name } }
239
+
240
+ Formatters::ProgressFormatter.with_spinner('Updating tool configuration') do
241
+ ctx.client.apply_resource(tool)
242
+ end
243
+
244
+ Formatters::ProgressFormatter.success('Authentication configured successfully')
245
+ puts
246
+ puts "Tool '#{tool_name}' is now authenticated and ready to use"
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../config/tool_registry'
4
+
5
+ module LanguageOperator
6
+ module CLI
7
+ module Commands
8
+ module Tool
9
+ # Tool search commands
10
+ module Search
11
+ def self.included(base)
12
+ base.class_eval do
13
+ desc 'search [PATTERN]', 'Search available tools in the registry'
14
+ long_desc <<-DESC
15
+ Search and list available tools from the registry.
16
+
17
+ Without a pattern, lists all available tools.
18
+ With a pattern, filters tools by name or description (case-insensitive).
19
+
20
+ Examples:
21
+ aictl tool search # List all tools
22
+ aictl tool search web # Find tools matching "web"
23
+ aictl tool search email # Find tools matching "email"
24
+ DESC
25
+ def search(pattern = nil)
26
+ handle_command_error('search tools') do
27
+ # Load tool patterns registry
28
+ registry = Config::ToolRegistry.new
29
+ patterns = registry.fetch
30
+
31
+ # Filter out aliases and match pattern
32
+ tools = patterns.select do |key, config|
33
+ next false if config['alias'] # Skip aliases
34
+
35
+ if pattern
36
+ # Case-insensitive match on name or description
37
+ key.downcase.include?(pattern.downcase) ||
38
+ config['description']&.downcase&.include?(pattern.downcase)
39
+ else
40
+ true
41
+ end
42
+ end
43
+
44
+ if tools.empty?
45
+ if pattern
46
+ Formatters::ProgressFormatter.info("No tools found matching '#{pattern}'")
47
+ else
48
+ Formatters::ProgressFormatter.info('No tools found in registry')
49
+ end
50
+ return
51
+ end
52
+
53
+ # Display tools in a nice format
54
+ tools.each do |name, config|
55
+ description = config['description'] || 'No description'
56
+
57
+ # Bold the tool name (ANSI escape codes)
58
+ bold_name = "\e[1m#{name}\e[0m"
59
+ puts "#{bold_name} - #{description}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end