language-operator 0.1.31 → 0.1.35

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