language-operator 0.0.1 → 0.1.30

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +53 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +77 -0
  8. data/README.md +3 -11
  9. data/Rakefile +34 -0
  10. data/bin/aictl +7 -0
  11. data/completions/_aictl +232 -0
  12. data/completions/aictl.bash +121 -0
  13. data/completions/aictl.fish +114 -0
  14. data/docs/architecture/agent-runtime.md +585 -0
  15. data/docs/dsl/agent-reference.md +591 -0
  16. data/docs/dsl/best-practices.md +1078 -0
  17. data/docs/dsl/chat-endpoints.md +895 -0
  18. data/docs/dsl/constraints.md +671 -0
  19. data/docs/dsl/mcp-integration.md +1177 -0
  20. data/docs/dsl/webhooks.md +932 -0
  21. data/docs/dsl/workflows.md +744 -0
  22. data/examples/README.md +569 -0
  23. data/examples/agent_example.rb +86 -0
  24. data/examples/chat_endpoint_agent.rb +118 -0
  25. data/examples/github_webhook_agent.rb +171 -0
  26. data/examples/mcp_agent.rb +158 -0
  27. data/examples/oauth_callback_agent.rb +296 -0
  28. data/examples/stripe_webhook_agent.rb +219 -0
  29. data/examples/webhook_agent.rb +80 -0
  30. data/lib/language_operator/agent/base.rb +110 -0
  31. data/lib/language_operator/agent/executor.rb +440 -0
  32. data/lib/language_operator/agent/instrumentation.rb +54 -0
  33. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  34. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  35. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  36. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  37. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  38. data/lib/language_operator/agent/safety/manager.rb +207 -0
  39. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  40. data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
  41. data/lib/language_operator/agent/scheduler.rb +183 -0
  42. data/lib/language_operator/agent/telemetry.rb +116 -0
  43. data/lib/language_operator/agent/web_server.rb +610 -0
  44. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  45. data/lib/language_operator/agent.rb +149 -0
  46. data/lib/language_operator/cli/commands/agent.rb +1252 -0
  47. data/lib/language_operator/cli/commands/cluster.rb +335 -0
  48. data/lib/language_operator/cli/commands/install.rb +404 -0
  49. data/lib/language_operator/cli/commands/model.rb +266 -0
  50. data/lib/language_operator/cli/commands/persona.rb +396 -0
  51. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  52. data/lib/language_operator/cli/commands/status.rb +156 -0
  53. data/lib/language_operator/cli/commands/tool.rb +537 -0
  54. data/lib/language_operator/cli/commands/use.rb +47 -0
  55. data/lib/language_operator/cli/errors/handler.rb +180 -0
  56. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  57. data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
  58. data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
  59. data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
  60. data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
  61. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  62. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  63. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  64. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  65. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  66. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  67. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  68. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  69. data/lib/language_operator/cli/main.rb +232 -0
  70. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  71. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  72. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  73. data/lib/language_operator/client/base.rb +214 -0
  74. data/lib/language_operator/client/config.rb +136 -0
  75. data/lib/language_operator/client/cost_calculator.rb +37 -0
  76. data/lib/language_operator/client/mcp_connector.rb +123 -0
  77. data/lib/language_operator/client.rb +19 -0
  78. data/lib/language_operator/config/cluster_config.rb +101 -0
  79. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  80. data/lib/language_operator/config/tool_registry.rb +96 -0
  81. data/lib/language_operator/config.rb +138 -0
  82. data/lib/language_operator/dsl/adapter.rb +124 -0
  83. data/lib/language_operator/dsl/agent_context.rb +90 -0
  84. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  85. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  86. data/lib/language_operator/dsl/config.rb +119 -0
  87. data/lib/language_operator/dsl/context.rb +50 -0
  88. data/lib/language_operator/dsl/execution_context.rb +47 -0
  89. data/lib/language_operator/dsl/helpers.rb +109 -0
  90. data/lib/language_operator/dsl/http.rb +184 -0
  91. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  92. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  93. data/lib/language_operator/dsl/registry.rb +36 -0
  94. data/lib/language_operator/dsl/shell.rb +125 -0
  95. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  96. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  97. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  98. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  99. data/lib/language_operator/dsl.rb +160 -0
  100. data/lib/language_operator/errors.rb +60 -0
  101. data/lib/language_operator/kubernetes/client.rb +279 -0
  102. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  103. data/lib/language_operator/loggable.rb +47 -0
  104. data/lib/language_operator/logger.rb +141 -0
  105. data/lib/language_operator/retry.rb +123 -0
  106. data/lib/language_operator/retryable.rb +132 -0
  107. data/lib/language_operator/tool_loader.rb +242 -0
  108. data/lib/language_operator/validators.rb +170 -0
  109. data/lib/language_operator/version.rb +1 -1
  110. data/lib/language_operator.rb +65 -3
  111. data/requirements/tasks/challenge.md +9 -0
  112. data/requirements/tasks/iterate.md +36 -0
  113. data/requirements/tasks/optimize.md +21 -0
  114. data/requirements/tasks/tag.md +5 -0
  115. data/test_agent_dsl.rb +108 -0
  116. metadata +503 -20
@@ -0,0 +1,537 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+ require 'erb'
6
+ require 'net/http'
7
+ require_relative '../formatters/progress_formatter'
8
+ require_relative '../formatters/table_formatter'
9
+ require_relative '../helpers/cluster_validator'
10
+ require_relative '../helpers/cluster_context'
11
+ require_relative '../helpers/user_prompts'
12
+ require_relative '../helpers/resource_dependency_checker'
13
+ require_relative '../../config/cluster_config'
14
+ require_relative '../../config/tool_registry'
15
+ require_relative '../../kubernetes/client'
16
+
17
+ module LanguageOperator
18
+ module CLI
19
+ module Commands
20
+ # Tool management commands
21
+ class Tool < Thor
22
+ include Helpers::ClusterValidator
23
+
24
+ desc 'list', 'List all tools in current cluster'
25
+ option :cluster, type: :string, desc: 'Override current cluster context'
26
+ def list
27
+ ctx = Helpers::ClusterContext.from_options(options)
28
+
29
+ tools = ctx.client.list_resources('LanguageTool', namespace: ctx.namespace)
30
+
31
+ if tools.empty?
32
+ Formatters::ProgressFormatter.info("No tools found in cluster '#{ctx.name}'")
33
+ puts
34
+ puts 'Tools provide MCP server capabilities for agents.'
35
+ puts
36
+ puts 'Install a tool with:'
37
+ puts ' aictl tool install <name>'
38
+ return
39
+ end
40
+
41
+ # Get agents to count usage
42
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
43
+
44
+ table_data = tools.map do |tool|
45
+ name = tool.dig('metadata', 'name')
46
+ type = tool.dig('spec', 'type') || 'unknown'
47
+ status = tool.dig('status', 'phase') || 'Unknown'
48
+
49
+ # Count agents using this tool
50
+ agents_using = Helpers::ResourceDependencyChecker.tool_usage_count(agents, name)
51
+
52
+ # Get health status
53
+ health = tool.dig('status', 'health') || 'unknown'
54
+ health_indicator = case health.downcase
55
+ when 'healthy' then '✓'
56
+ when 'unhealthy' then '✗'
57
+ else '?'
58
+ end
59
+
60
+ {
61
+ name: name,
62
+ type: type,
63
+ status: status,
64
+ agents_using: agents_using,
65
+ health: "#{health_indicator} #{health}"
66
+ }
67
+ end
68
+
69
+ Formatters::TableFormatter.tools(table_data)
70
+ rescue StandardError => e
71
+ Formatters::ProgressFormatter.error("Failed to list tools: #{e.message}")
72
+ raise if ENV['DEBUG']
73
+
74
+ exit 1
75
+ end
76
+
77
+ desc 'delete NAME', 'Delete a tool'
78
+ option :cluster, type: :string, desc: 'Override current cluster context'
79
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
80
+ def delete(name)
81
+ ctx = Helpers::ClusterContext.from_options(options)
82
+
83
+ # Get tool
84
+ begin
85
+ tool = ctx.client.get_resource('LanguageTool', name, ctx.namespace)
86
+ rescue K8s::Error::NotFound
87
+ Formatters::ProgressFormatter.error("Tool '#{name}' not found in cluster '#{ctx.name}'")
88
+ exit 1
89
+ end
90
+
91
+ # Check for agents using this tool
92
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
93
+ agents_using = Helpers::ResourceDependencyChecker.agents_using_tool(agents, name)
94
+
95
+ if agents_using.any? && !options[:force]
96
+ Formatters::ProgressFormatter.warn("Tool '#{name}' is in use by #{agents_using.count} agent(s)")
97
+ puts
98
+ puts 'Agents using this tool:'
99
+ agents_using.each do |agent|
100
+ puts " - #{agent.dig('metadata', 'name')}"
101
+ end
102
+ puts
103
+ puts 'Delete these agents first, or use --force to delete anyway.'
104
+ puts
105
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
106
+ end
107
+
108
+ # Confirm deletion using UserPrompts helper
109
+ unless options[:force] || agents_using.any?
110
+ puts "This will delete tool '#{name}' from cluster '#{ctx.name}':"
111
+ puts " Type: #{tool.dig('spec', 'type')}"
112
+ puts " Status: #{tool.dig('status', 'phase')}"
113
+ puts
114
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
115
+ end
116
+
117
+ # Delete tool
118
+ Formatters::ProgressFormatter.with_spinner("Deleting tool '#{name}'") do
119
+ ctx.client.delete_resource('LanguageTool', name, ctx.namespace)
120
+ end
121
+
122
+ Formatters::ProgressFormatter.success("Tool '#{name}' deleted successfully")
123
+ rescue StandardError => e
124
+ Formatters::ProgressFormatter.error("Failed to delete tool: #{e.message}")
125
+ raise if ENV['DEBUG']
126
+
127
+ exit 1
128
+ end
129
+
130
+ desc 'install NAME', 'Install a tool from the registry'
131
+ option :cluster, type: :string, desc: 'Override current cluster context'
132
+ option :deployment_mode, type: :string, enum: %w[service sidecar], desc: 'Deployment mode (service or sidecar)'
133
+ option :replicas, type: :numeric, desc: 'Number of replicas'
134
+ option :dry_run, type: :boolean, default: false, desc: 'Preview without installing'
135
+ def install(tool_name)
136
+ # For dry-run mode, allow operation without a real cluster
137
+ if options[:dry_run]
138
+ cluster_name = options[:cluster] || 'preview'
139
+ namespace = 'default'
140
+ else
141
+ ctx = Helpers::ClusterContext.from_options(options)
142
+ cluster_name = ctx.name
143
+ namespace = ctx.namespace
144
+ end
145
+
146
+ # Load tool patterns registry
147
+ registry = Config::ToolRegistry.new
148
+ patterns = registry.fetch
149
+
150
+ # Resolve aliases
151
+ tool_key = tool_name
152
+ tool_key = patterns[tool_key]['alias'] while patterns[tool_key]&.key?('alias')
153
+
154
+ # Look up tool in registry
155
+ tool_config = patterns[tool_key]
156
+ unless tool_config
157
+ Formatters::ProgressFormatter.error("Tool '#{tool_name}' not found in registry")
158
+ puts
159
+ puts 'Available tools:'
160
+ patterns.each do |key, config|
161
+ next if config['alias']
162
+
163
+ puts " #{key.ljust(15)} - #{config['description']}"
164
+ end
165
+ exit 1
166
+ end
167
+
168
+ # Build template variables
169
+ vars = {
170
+ name: tool_name,
171
+ namespace: namespace,
172
+ deployment_mode: options[:deployment_mode] || tool_config['deploymentMode'],
173
+ replicas: options[:replicas] || 1,
174
+ auth_secret: nil, # Will be set by auth command
175
+ image: tool_config['image'],
176
+ port: tool_config['port'],
177
+ type: tool_config['type'],
178
+ egress: tool_config['egress'],
179
+ rbac: tool_config['rbac']
180
+ }
181
+
182
+ # Get template content - prefer registry manifest, fall back to generic template
183
+ if tool_config['manifest']
184
+ # Use manifest from registry (if provided in the future)
185
+ template_content = tool_config['manifest']
186
+ else
187
+ # Use generic template for all tools
188
+ template_path = File.join(__dir__, '..', 'templates', 'tools', 'generic.yaml')
189
+ template_content = File.read(template_path)
190
+ end
191
+
192
+ # Render template
193
+ template = ERB.new(template_content)
194
+ yaml_content = template.result_with_hash(vars)
195
+
196
+ # Dry run mode
197
+ if options[:dry_run]
198
+ puts "Would install tool '#{tool_name}' to cluster '#{cluster_name}':"
199
+ puts
200
+ puts "Display Name: #{tool_config['displayName']}"
201
+ puts "Description: #{tool_config['description']}"
202
+ puts "Deployment Mode: #{vars[:deployment_mode]}"
203
+ puts "Replicas: #{vars[:replicas]}"
204
+ puts "Auth Required: #{tool_config['authRequired'] ? 'Yes' : 'No'}"
205
+ puts
206
+ puts 'Generated YAML:'
207
+ puts '---'
208
+ puts yaml_content
209
+ puts
210
+ puts 'To install for real, run without --dry-run'
211
+ return
212
+ end
213
+
214
+ # Connect to cluster
215
+ ctx = Helpers::ClusterContext.from_options(options)
216
+
217
+ # Check if already exists
218
+ begin
219
+ ctx.client.get_resource('LanguageTool', tool_name, ctx.namespace)
220
+ Formatters::ProgressFormatter.warn("Tool '#{tool_name}' already exists in cluster '#{ctx.name}'")
221
+ puts
222
+ return unless Helpers::UserPrompts.confirm('Do you want to update it?')
223
+ rescue K8s::Error::NotFound
224
+ # Tool doesn't exist, proceed with creation
225
+ end
226
+
227
+ # Install tool
228
+ Formatters::ProgressFormatter.with_spinner("Installing tool '#{tool_name}'") do
229
+ resource = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
230
+ ctx.client.apply_resource(resource)
231
+ end
232
+
233
+ Formatters::ProgressFormatter.success("Tool '#{tool_name}' installed successfully")
234
+ puts
235
+ puts "Tool '#{tool_name}' is now available in cluster '#{ctx.name}'"
236
+ if tool_config['authRequired']
237
+ puts
238
+ puts 'This tool requires authentication. Configure it with:'
239
+ puts " aictl tool auth #{tool_name}"
240
+ end
241
+ rescue StandardError => e
242
+ Formatters::ProgressFormatter.error("Failed to install tool: #{e.message}")
243
+ raise if ENV['DEBUG']
244
+
245
+ exit 1
246
+ end
247
+
248
+ desc 'auth NAME', 'Configure authentication for a tool'
249
+ option :cluster, type: :string, desc: 'Override current cluster context'
250
+ def auth(tool_name)
251
+ ctx = Helpers::ClusterContext.from_options(options)
252
+
253
+ # Check if tool exists
254
+ begin
255
+ tool = ctx.client.get_resource('LanguageTool', tool_name, ctx.namespace)
256
+ rescue K8s::Error::NotFound
257
+ Formatters::ProgressFormatter.error("Tool '#{tool_name}' not found in cluster '#{ctx.name}'")
258
+ puts
259
+ puts 'Install the tool first with:'
260
+ puts " aictl tool install #{tool_name}"
261
+ exit 1
262
+ end
263
+
264
+ puts "Configure authentication for tool '#{tool_name}'"
265
+ puts
266
+
267
+ # Determine auth type based on tool
268
+ case tool_name
269
+ when 'email', 'gmail'
270
+ puts 'Email/Gmail Configuration'
271
+ puts '-' * 40
272
+ print 'SMTP Server: '
273
+ smtp_server = $stdin.gets.chomp
274
+ print 'SMTP Port (587): '
275
+ smtp_port = $stdin.gets.chomp
276
+ smtp_port = '587' if smtp_port.empty?
277
+ print 'Email Address: '
278
+ email = $stdin.gets.chomp
279
+ print 'Password: '
280
+ password = $stdin.noecho(&:gets).chomp
281
+ puts
282
+
283
+ secret_data = {
284
+ 'SMTP_SERVER' => smtp_server,
285
+ 'SMTP_PORT' => smtp_port,
286
+ 'EMAIL_ADDRESS' => email,
287
+ 'EMAIL_PASSWORD' => password
288
+ }
289
+
290
+ when 'github'
291
+ puts 'GitHub Configuration'
292
+ puts '-' * 40
293
+ print 'GitHub Token: '
294
+ token = $stdin.noecho(&:gets).chomp
295
+ puts
296
+
297
+ secret_data = {
298
+ 'GITHUB_TOKEN' => token
299
+ }
300
+
301
+ when 'slack'
302
+ puts 'Slack Configuration'
303
+ puts '-' * 40
304
+ print 'Slack Bot Token: '
305
+ token = $stdin.noecho(&:gets).chomp
306
+ puts
307
+
308
+ secret_data = {
309
+ 'SLACK_BOT_TOKEN' => token
310
+ }
311
+
312
+ when 'gdrive'
313
+ puts 'Google Drive Configuration'
314
+ puts '-' * 40
315
+ puts 'Note: You need OAuth credentials from Google Cloud Console'
316
+ print 'Client ID: '
317
+ client_id = $stdin.gets.chomp
318
+ print 'Client Secret: '
319
+ client_secret = $stdin.noecho(&:gets).chomp
320
+ puts
321
+
322
+ secret_data = {
323
+ 'GDRIVE_CLIENT_ID' => client_id,
324
+ 'GDRIVE_CLIENT_SECRET' => client_secret
325
+ }
326
+
327
+ else
328
+ puts 'Generic API Key Configuration'
329
+ puts '-' * 40
330
+ print 'API Key: '
331
+ api_key = $stdin.noecho(&:gets).chomp
332
+ puts
333
+
334
+ secret_data = {
335
+ 'API_KEY' => api_key
336
+ }
337
+ end
338
+
339
+ # Create secret
340
+ secret_name = "#{tool_name}-auth"
341
+ secret_resource = {
342
+ 'apiVersion' => 'v1',
343
+ 'kind' => 'Secret',
344
+ 'metadata' => {
345
+ 'name' => secret_name,
346
+ 'namespace' => ctx.namespace
347
+ },
348
+ 'type' => 'Opaque',
349
+ 'stringData' => secret_data
350
+ }
351
+
352
+ Formatters::ProgressFormatter.with_spinner('Creating authentication secret') do
353
+ ctx.client.apply_resource(secret_resource)
354
+ end
355
+
356
+ # Update tool to use secret
357
+ tool['spec']['envFrom'] ||= []
358
+ tool['spec']['envFrom'] << { 'secretRef' => { 'name' => secret_name } }
359
+
360
+ Formatters::ProgressFormatter.with_spinner('Updating tool configuration') do
361
+ ctx.client.apply_resource(tool)
362
+ end
363
+
364
+ Formatters::ProgressFormatter.success('Authentication configured successfully')
365
+ puts
366
+ puts "Tool '#{tool_name}' is now authenticated and ready to use"
367
+ rescue StandardError => e
368
+ Formatters::ProgressFormatter.error("Failed to configure auth: #{e.message}")
369
+ raise if ENV['DEBUG']
370
+
371
+ exit 1
372
+ end
373
+
374
+ desc 'test NAME', 'Test tool connectivity and health'
375
+ option :cluster, type: :string, desc: 'Override current cluster context'
376
+ def test(tool_name)
377
+ ctx = Helpers::ClusterContext.from_options(options)
378
+
379
+ # Get tool
380
+ begin
381
+ tool = ctx.client.get_resource('LanguageTool', tool_name, ctx.namespace)
382
+ rescue K8s::Error::NotFound
383
+ Formatters::ProgressFormatter.error("Tool '#{tool_name}' not found in cluster '#{ctx.name}'")
384
+ exit 1
385
+ end
386
+
387
+ puts "Testing tool '#{tool_name}' in cluster '#{ctx.name}'"
388
+ puts
389
+
390
+ # Check phase
391
+ phase = tool.dig('status', 'phase') || 'Unknown'
392
+ status_indicator = case phase
393
+ when 'Running' then '✓'
394
+ when 'Pending' then '⏳'
395
+ when 'Failed' then '✗'
396
+ else '?'
397
+ end
398
+
399
+ puts "Status: #{status_indicator} #{phase}"
400
+
401
+ # Check replicas
402
+ ready_replicas = tool.dig('status', 'readyReplicas') || 0
403
+ desired_replicas = tool.dig('spec', 'replicas') || 1
404
+ puts "Replicas: #{ready_replicas}/#{desired_replicas} ready"
405
+
406
+ # Check endpoint
407
+ endpoint = tool.dig('status', 'endpoint')
408
+ if endpoint
409
+ puts "Endpoint: #{endpoint}"
410
+ else
411
+ puts 'Endpoint: Not available yet'
412
+ end
413
+
414
+ # Get pod status
415
+ puts
416
+ puts 'Pod Status:'
417
+
418
+ label_selector = "langop.io/tool=#{tool_name}"
419
+ pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
420
+
421
+ if pods.empty?
422
+ puts ' No pods found'
423
+ else
424
+ pods.each do |pod|
425
+ pod_name = pod.dig('metadata', 'name')
426
+ pod_phase = pod.dig('status', 'phase') || 'Unknown'
427
+ pod_indicator = case pod_phase
428
+ when 'Running' then '✓'
429
+ when 'Pending' then '⏳'
430
+ when 'Failed' then '✗'
431
+ else '?'
432
+ end
433
+
434
+ puts " #{pod_indicator} #{pod_name}: #{pod_phase}"
435
+
436
+ # Check container status
437
+ container_statuses = pod.dig('status', 'containerStatuses') || []
438
+ container_statuses.each do |status|
439
+ ready = status['ready'] ? '✓' : '✗'
440
+ puts " #{ready} #{status['name']}: #{status['state']&.keys&.first || 'unknown'}"
441
+ end
442
+ end
443
+ end
444
+
445
+ # Test connectivity if endpoint is available
446
+ if endpoint && phase == 'Running'
447
+ puts
448
+ puts 'Testing connectivity...'
449
+ begin
450
+ uri = URI(endpoint)
451
+ response = Net::HTTP.get_response(uri)
452
+ if response.code.to_i < 400
453
+ Formatters::ProgressFormatter.success('Connectivity test passed')
454
+ else
455
+ Formatters::ProgressFormatter.warn("HTTP #{response.code}: #{response.message}")
456
+ end
457
+ rescue StandardError => e
458
+ Formatters::ProgressFormatter.error("Connectivity test failed: #{e.message}")
459
+ end
460
+ end
461
+
462
+ # Overall health
463
+ puts
464
+ if phase == 'Running' && ready_replicas == desired_replicas
465
+ Formatters::ProgressFormatter.success("Tool '#{tool_name}' is healthy and operational")
466
+ elsif phase == 'Pending'
467
+ Formatters::ProgressFormatter.info("Tool '#{tool_name}' is starting up, please wait")
468
+ else
469
+ Formatters::ProgressFormatter.warn("Tool '#{tool_name}' has issues, check logs for details")
470
+ puts
471
+ puts 'View logs with:'
472
+ puts " kubectl logs -n #{ctx.namespace} -l langop.io/tool=#{tool_name}"
473
+ end
474
+ rescue StandardError => e
475
+ Formatters::ProgressFormatter.error("Failed to test tool: #{e.message}")
476
+ raise if ENV['DEBUG']
477
+
478
+ exit 1
479
+ end
480
+
481
+ desc 'search [PATTERN]', 'Search available tools in the registry'
482
+ long_desc <<-DESC
483
+ Search and list available tools from the registry.
484
+
485
+ Without a pattern, lists all available tools.
486
+ With a pattern, filters tools by name or description (case-insensitive).
487
+
488
+ Examples:
489
+ aictl tool search # List all tools
490
+ aictl tool search web # Find tools matching "web"
491
+ aictl tool search email # Find tools matching "email"
492
+ DESC
493
+ def search(pattern = nil)
494
+ # Load tool patterns registry
495
+ registry = Config::ToolRegistry.new
496
+ patterns = registry.fetch
497
+
498
+ # Filter out aliases and match pattern
499
+ tools = patterns.select do |key, config|
500
+ next false if config['alias'] # Skip aliases
501
+
502
+ if pattern
503
+ # Case-insensitive match on name or description
504
+ key.downcase.include?(pattern.downcase) ||
505
+ config['description']&.downcase&.include?(pattern.downcase)
506
+ else
507
+ true
508
+ end
509
+ end
510
+
511
+ if tools.empty?
512
+ if pattern
513
+ Formatters::ProgressFormatter.info("No tools found matching '#{pattern}'")
514
+ else
515
+ Formatters::ProgressFormatter.info('No tools found in registry')
516
+ end
517
+ return
518
+ end
519
+
520
+ # Display tools in a nice format
521
+ tools.each do |name, config|
522
+ description = config['description'] || 'No description'
523
+
524
+ # Bold the tool name (ANSI escape codes)
525
+ bold_name = "\e[1m#{name}\e[0m"
526
+ puts "#{bold_name} - #{description}"
527
+ end
528
+ rescue StandardError => e
529
+ Formatters::ProgressFormatter.error("Failed to search tools: #{e.message}")
530
+ raise if ENV['DEBUG']
531
+
532
+ exit 1
533
+ end
534
+ end
535
+ end
536
+ end
537
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'pastel'
5
+ require_relative '../formatters/progress_formatter'
6
+ require_relative '../../config/cluster_config'
7
+
8
+ module LanguageOperator
9
+ module CLI
10
+ module Commands
11
+ # Switch cluster context command
12
+ class Use < Thor
13
+ desc 'use CLUSTER', 'Switch to a different cluster context'
14
+ def self.exit_on_failure?
15
+ true
16
+ end
17
+
18
+ def switch(cluster_name)
19
+ unless Config::ClusterConfig.cluster_exists?(cluster_name)
20
+ Formatters::ProgressFormatter.error("Cluster '#{cluster_name}' not found")
21
+ puts "\nAvailable clusters:"
22
+ Config::ClusterConfig.list_clusters.each do |cluster|
23
+ puts " - #{cluster[:name]}"
24
+ end
25
+ exit 1
26
+ end
27
+
28
+ Config::ClusterConfig.set_current_cluster(cluster_name)
29
+ cluster = Config::ClusterConfig.get_cluster(cluster_name)
30
+
31
+ Formatters::ProgressFormatter.success("Switched to cluster '#{cluster_name}'")
32
+
33
+ pastel = Pastel.new
34
+ puts "\nCluster Details"
35
+ puts '----------------'
36
+ puts "Name: #{pastel.bold.white(cluster[:name])}"
37
+ puts "Namespace: #{pastel.bold.white(cluster[:namespace])}"
38
+ rescue StandardError => e
39
+ Formatters::ProgressFormatter.error("Failed to switch cluster: #{e.message}")
40
+ raise if ENV['DEBUG']
41
+
42
+ exit 1
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end