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
@@ -1,654 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
- require 'erb'
5
- require 'net/http'
6
- require 'json'
7
- require_relative '../base_command'
8
- require_relative '../formatters/progress_formatter'
9
- require_relative '../formatters/table_formatter'
10
- require_relative '../helpers/cluster_validator'
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 < BaseCommand
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
- handle_command_error('list tools') do
28
- tools = list_resources_or_empty('LanguageTool') do
29
- puts
30
- puts 'Tools provide MCP server capabilities for agents.'
31
- puts
32
- puts 'Install a tool with:'
33
- puts ' aictl tool install <name>'
34
- end
35
-
36
- return if tools.empty?
37
-
38
- # Get agents to count usage
39
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
40
-
41
- table_data = tools.map do |tool|
42
- name = tool.dig('metadata', 'name')
43
- type = tool.dig('spec', 'type') || 'unknown'
44
- status = tool.dig('status', 'phase') || 'Unknown'
45
-
46
- # Count agents using this tool
47
- agents_using = Helpers::ResourceDependencyChecker.tool_usage_count(agents, name)
48
-
49
- # Get health status
50
- health = tool.dig('status', 'health') || 'unknown'
51
- health_indicator = case health.downcase
52
- when 'healthy' then '✓'
53
- when 'unhealthy' then '✗'
54
- else '?'
55
- end
56
-
57
- {
58
- name: name,
59
- type: type,
60
- status: status,
61
- agents_using: agents_using,
62
- health: "#{health_indicator} #{health}"
63
- }
64
- end
65
-
66
- Formatters::TableFormatter.tools(table_data)
67
- end
68
- end
69
-
70
- desc 'inspect NAME', 'Show detailed tool information'
71
- option :cluster, type: :string, desc: 'Override current cluster context'
72
- def inspect(name)
73
- handle_command_error('inspect tool') do
74
- tool = get_resource_or_exit('LanguageTool', name)
75
-
76
- puts "Tool: #{name}"
77
- puts " Cluster: #{ctx.name}"
78
- puts " Namespace: #{ctx.namespace}"
79
- puts
80
-
81
- # Status
82
- status = tool.dig('status', 'phase') || 'Unknown'
83
- puts "Status: #{status}"
84
- puts
85
-
86
- # Spec details
87
- puts 'Configuration:'
88
- puts " Type: #{tool.dig('spec', 'type') || 'mcp'}"
89
- puts " Image: #{tool.dig('spec', 'image')}"
90
- puts " Deployment Mode: #{tool.dig('spec', 'deploymentMode') || 'sidecar'}"
91
- puts " Port: #{tool.dig('spec', 'port') || 8080}"
92
- puts " Replicas: #{tool.dig('spec', 'replicas') || 1}"
93
- puts
94
-
95
- # Resources
96
- resources = tool.dig('spec', 'resources')
97
- if resources
98
- puts 'Resources:'
99
- if resources['requests']
100
- puts ' Requests:'
101
- puts " CPU: #{resources['requests']['cpu']}"
102
- puts " Memory: #{resources['requests']['memory']}"
103
- end
104
- if resources['limits']
105
- puts ' Limits:'
106
- puts " CPU: #{resources['limits']['cpu']}"
107
- puts " Memory: #{resources['limits']['memory']}"
108
- end
109
- puts
110
- end
111
-
112
- # RBAC
113
- rbac = tool.dig('spec', 'rbac')
114
- if rbac && rbac['clusterRole']
115
- rules = rbac.dig('clusterRole', 'rules') || []
116
- puts "RBAC Permissions (#{rules.length} rules):"
117
- rules.each_with_index do |rule, idx|
118
- puts " Rule #{idx + 1}:"
119
- puts " API Groups: #{rule['apiGroups'].join(', ')}"
120
- puts " Resources: #{rule['resources'].join(', ')}"
121
- puts " Verbs: #{rule['verbs'].join(', ')}"
122
- end
123
- puts
124
- end
125
-
126
- # Egress rules
127
- egress = tool.dig('spec', 'egress') || []
128
- if egress.any?
129
- puts "Network Egress (#{egress.length} rules):"
130
- egress.each_with_index do |rule, idx|
131
- puts " Rule #{idx + 1}: #{rule['description']}"
132
- puts " DNS: #{rule['dns'].join(', ')}" if rule['dns']
133
- puts " CIDR: #{rule['cidr']}" if rule['cidr']
134
- if rule['ports']
135
- ports_str = rule['ports'].map { |p| "#{p['port']}/#{p['protocol']}" }.join(', ')
136
- puts " Ports: #{ports_str}"
137
- end
138
- end
139
- puts
140
- end
141
-
142
- # Try to fetch MCP capabilities
143
- capabilities = fetch_mcp_capabilities(name, tool, ctx.namespace)
144
- if capabilities && capabilities['tools'] && capabilities['tools'].any?
145
- puts "MCP Tools (#{capabilities['tools'].length}):"
146
- capabilities['tools'].each_with_index do |mcp_tool, idx|
147
- tool_name = mcp_tool['name']
148
-
149
- # Generate a meaningful name if empty
150
- if tool_name.nil? || tool_name.empty?
151
- # Try to derive from description (first few words)
152
- if mcp_tool['description']
153
- # Take first 3-4 words and convert to snake_case
154
- words = mcp_tool['description'].split(/\s+/).first(4)
155
- derived_name = words.join('_').downcase.gsub(/[^a-z0-9_]/, '')
156
- tool_name = "#{name}_#{derived_name}".gsub(/__+/, '_').sub(/_$/, '')
157
- else
158
- tool_name = "#{name}_tool_#{idx + 1}"
159
- end
160
- end
161
-
162
- puts " #{tool_name}"
163
- puts " Description: #{mcp_tool['description']}" if mcp_tool['description']
164
- next unless mcp_tool['inputSchema'] && mcp_tool['inputSchema']['properties']
165
-
166
- params = mcp_tool['inputSchema']['properties'].keys
167
- required = mcp_tool['inputSchema']['required'] || []
168
- param_list = params.map { |p| required.include?(p) ? "#{p}*" : p }
169
- puts " Parameters: #{param_list.join(', ')}"
170
- end
171
- puts ' (* = required)'
172
- puts
173
- end
174
-
175
- # Get agents using this tool
176
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
177
- agents_using = agents.select do |agent|
178
- tools = agent.dig('spec', 'tools') || []
179
- tools.include?(name)
180
- end
181
-
182
- if agents_using.any?
183
- puts "Agents using this tool (#{agents_using.count}):"
184
- agents_using.each do |agent|
185
- puts " - #{agent.dig('metadata', 'name')}"
186
- end
187
- else
188
- puts 'No agents using this tool'
189
- end
190
-
191
- puts
192
- puts 'Labels:'
193
- labels = tool.dig('metadata', 'labels') || {}
194
- if labels.empty?
195
- puts ' (none)'
196
- else
197
- labels.each do |key, value|
198
- puts " #{key}: #{value}"
199
- end
200
- end
201
- end
202
- end
203
-
204
- desc 'delete NAME', 'Delete a tool'
205
- option :cluster, type: :string, desc: 'Override current cluster context'
206
- option :force, type: :boolean, default: false, desc: 'Skip confirmation'
207
- def delete(name)
208
- handle_command_error('delete tool') do
209
- tool = get_resource_or_exit('LanguageTool', name)
210
-
211
- # Check dependencies and get confirmation
212
- return unless check_dependencies_and_confirm('tool', name, force: options[:force])
213
-
214
- # Confirm deletion unless --force
215
- if confirm_deletion(
216
- 'tool', name, ctx.name,
217
- details: {
218
- 'Type' => tool.dig('spec', 'type'),
219
- 'Status' => tool.dig('status', 'phase')
220
- },
221
- force: options[:force]
222
- )
223
- # Delete tool
224
- Formatters::ProgressFormatter.with_spinner("Deleting tool '#{name}'") do
225
- ctx.client.delete_resource('LanguageTool', name, ctx.namespace)
226
- end
227
-
228
- Formatters::ProgressFormatter.success("Tool '#{name}' deleted successfully")
229
- end
230
- end
231
- end
232
-
233
- desc 'install NAME', 'Install a tool from the registry'
234
- option :cluster, type: :string, desc: 'Override current cluster context'
235
- option :deployment_mode, type: :string, enum: %w[service sidecar], desc: 'Deployment mode (service or sidecar)'
236
- option :replicas, type: :numeric, desc: 'Number of replicas'
237
- option :dry_run, type: :boolean, default: false, desc: 'Preview without installing'
238
- def install(tool_name)
239
- handle_command_error('install tool') do
240
- # For dry-run mode, allow operation without a real cluster
241
- if options[:dry_run]
242
- cluster_name = options[:cluster] || 'preview'
243
- namespace = 'default'
244
- else
245
- cluster_name = ctx.name
246
- namespace = ctx.namespace
247
- end
248
-
249
- # Load tool patterns registry
250
- registry = Config::ToolRegistry.new
251
- patterns = registry.fetch
252
-
253
- # Resolve aliases
254
- tool_key = tool_name
255
- tool_key = patterns[tool_key]['alias'] while patterns[tool_key]&.key?('alias')
256
-
257
- # Look up tool in registry
258
- tool_config = patterns[tool_key]
259
- unless tool_config
260
- Formatters::ProgressFormatter.error("Tool '#{tool_name}' not found in registry")
261
- puts
262
- puts 'Available tools:'
263
- patterns.each do |key, config|
264
- next if config['alias']
265
-
266
- puts " #{key.ljust(15)} - #{config['description']}"
267
- end
268
- exit 1
269
- end
270
-
271
- # Build template variables
272
- vars = {
273
- name: tool_name,
274
- namespace: namespace,
275
- deployment_mode: options[:deployment_mode] || tool_config['deploymentMode'],
276
- replicas: options[:replicas] || 1,
277
- auth_secret: nil, # Will be set by auth command
278
- image: tool_config['image'],
279
- port: tool_config['port'],
280
- type: tool_config['type'],
281
- egress: tool_config['egress'],
282
- rbac: tool_config['rbac']
283
- }
284
-
285
- # Get template content - prefer registry manifest, fall back to generic template
286
- if tool_config['manifest']
287
- # Use manifest from registry (if provided in the future)
288
- template_content = tool_config['manifest']
289
- else
290
- # Use generic template for all tools
291
- template_path = File.join(__dir__, '..', 'templates', 'tools', 'generic.yaml')
292
- template_content = File.read(template_path)
293
- end
294
-
295
- # Render template
296
- template = ERB.new(template_content, trim_mode: '-')
297
- yaml_content = template.result_with_hash(vars)
298
-
299
- # Dry run mode
300
- if options[:dry_run]
301
- puts "Would install tool '#{tool_name}' to cluster '#{cluster_name}':"
302
- puts
303
- puts "Display Name: #{tool_config['displayName']}"
304
- puts "Description: #{tool_config['description']}"
305
- puts "Deployment Mode: #{vars[:deployment_mode]}"
306
- puts "Replicas: #{vars[:replicas]}"
307
- puts "Auth Required: #{tool_config['authRequired'] ? 'Yes' : 'No'}"
308
- puts
309
- puts 'Generated YAML:'
310
- puts '---'
311
- puts yaml_content
312
- puts
313
- puts 'To install for real, run without --dry-run'
314
- return
315
- end
316
-
317
- # Check if already exists
318
- begin
319
- ctx.client.get_resource('LanguageTool', tool_name, ctx.namespace)
320
- Formatters::ProgressFormatter.warn("Tool '#{tool_name}' already exists in cluster '#{ctx.name}'")
321
- puts
322
- return unless Helpers::UserPrompts.confirm('Do you want to update it?')
323
- rescue K8s::Error::NotFound
324
- # Tool doesn't exist, proceed with creation
325
- end
326
-
327
- # Install tool
328
- Formatters::ProgressFormatter.with_spinner("Installing tool '#{tool_name}'") do
329
- resource = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
330
- ctx.client.apply_resource(resource)
331
- end
332
-
333
- Formatters::ProgressFormatter.success("Tool '#{tool_name}' installed successfully")
334
- puts
335
- puts "Tool '#{tool_name}' is now available in cluster '#{ctx.name}'"
336
- if tool_config['authRequired']
337
- puts
338
- puts 'This tool requires authentication. Configure it with:'
339
- puts " aictl tool auth #{tool_name}"
340
- end
341
- end
342
- end
343
-
344
- desc 'auth NAME', 'Configure authentication for a tool'
345
- option :cluster, type: :string, desc: 'Override current cluster context'
346
- def auth(tool_name)
347
- handle_command_error('configure auth') do
348
- tool = get_resource_or_exit('LanguageTool', tool_name,
349
- error_message: "Tool '#{tool_name}' not found. Install it first with: aictl tool install #{tool_name}")
350
-
351
- puts "Configure authentication for tool '#{tool_name}'"
352
- puts
353
-
354
- # Determine auth type based on tool
355
- case tool_name
356
- when 'email', 'gmail'
357
- puts 'Email/Gmail Configuration'
358
- puts '-' * 40
359
- print 'SMTP Server: '
360
- smtp_server = $stdin.gets.chomp
361
- print 'SMTP Port (587): '
362
- smtp_port = $stdin.gets.chomp
363
- smtp_port = '587' if smtp_port.empty?
364
- print 'Email Address: '
365
- email = $stdin.gets.chomp
366
- print 'Password: '
367
- password = $stdin.noecho(&:gets).chomp
368
- puts
369
-
370
- secret_data = {
371
- 'SMTP_SERVER' => smtp_server,
372
- 'SMTP_PORT' => smtp_port,
373
- 'EMAIL_ADDRESS' => email,
374
- 'EMAIL_PASSWORD' => password
375
- }
376
-
377
- when 'github'
378
- puts 'GitHub Configuration'
379
- puts '-' * 40
380
- print 'GitHub Token: '
381
- token = $stdin.noecho(&:gets).chomp
382
- puts
383
-
384
- secret_data = {
385
- 'GITHUB_TOKEN' => token
386
- }
387
-
388
- when 'slack'
389
- puts 'Slack Configuration'
390
- puts '-' * 40
391
- print 'Slack Bot Token: '
392
- token = $stdin.noecho(&:gets).chomp
393
- puts
394
-
395
- secret_data = {
396
- 'SLACK_BOT_TOKEN' => token
397
- }
398
-
399
- when 'gdrive'
400
- puts 'Google Drive Configuration'
401
- puts '-' * 40
402
- puts 'Note: You need OAuth credentials from Google Cloud Console'
403
- print 'Client ID: '
404
- client_id = $stdin.gets.chomp
405
- print 'Client Secret: '
406
- client_secret = $stdin.noecho(&:gets).chomp
407
- puts
408
-
409
- secret_data = {
410
- 'GDRIVE_CLIENT_ID' => client_id,
411
- 'GDRIVE_CLIENT_SECRET' => client_secret
412
- }
413
-
414
- else
415
- puts 'Generic API Key Configuration'
416
- puts '-' * 40
417
- print 'API Key: '
418
- api_key = $stdin.noecho(&:gets).chomp
419
- puts
420
-
421
- secret_data = {
422
- 'API_KEY' => api_key
423
- }
424
- end
425
-
426
- # Create secret
427
- secret_name = "#{tool_name}-auth"
428
- secret_resource = {
429
- 'apiVersion' => 'v1',
430
- 'kind' => 'Secret',
431
- 'metadata' => {
432
- 'name' => secret_name,
433
- 'namespace' => ctx.namespace
434
- },
435
- 'type' => 'Opaque',
436
- 'stringData' => secret_data
437
- }
438
-
439
- Formatters::ProgressFormatter.with_spinner('Creating authentication secret') do
440
- ctx.client.apply_resource(secret_resource)
441
- end
442
-
443
- # Update tool to use secret
444
- tool['spec']['envFrom'] ||= []
445
- tool['spec']['envFrom'] << { 'secretRef' => { 'name' => secret_name } }
446
-
447
- Formatters::ProgressFormatter.with_spinner('Updating tool configuration') do
448
- ctx.client.apply_resource(tool)
449
- end
450
-
451
- Formatters::ProgressFormatter.success('Authentication configured successfully')
452
- puts
453
- puts "Tool '#{tool_name}' is now authenticated and ready to use"
454
- end
455
- end
456
-
457
- desc 'test NAME', 'Test tool connectivity and health'
458
- option :cluster, type: :string, desc: 'Override current cluster context'
459
- def test(tool_name)
460
- handle_command_error('test tool') do
461
- tool = get_resource_or_exit('LanguageTool', tool_name)
462
-
463
- puts "Testing tool '#{tool_name}' in cluster '#{ctx.name}'"
464
- puts
465
-
466
- # Check phase
467
- phase = tool.dig('status', 'phase') || 'Unknown'
468
- status_indicator = case phase
469
- when 'Running' then '✓'
470
- when 'Pending' then '⏳'
471
- when 'Failed' then '✗'
472
- else '?'
473
- end
474
-
475
- puts "Status: #{status_indicator} #{phase}"
476
-
477
- # Check replicas
478
- ready_replicas = tool.dig('status', 'readyReplicas') || 0
479
- desired_replicas = tool.dig('spec', 'replicas') || 1
480
- puts "Replicas: #{ready_replicas}/#{desired_replicas} ready"
481
-
482
- # Check endpoint
483
- endpoint = tool.dig('status', 'endpoint')
484
- if endpoint
485
- puts "Endpoint: #{endpoint}"
486
- else
487
- puts 'Endpoint: Not available yet'
488
- end
489
-
490
- # Get pod status
491
- puts
492
- puts 'Pod Status:'
493
-
494
- label_selector = "langop.io/tool=#{tool_name}"
495
- pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
496
-
497
- if pods.empty?
498
- puts ' No pods found'
499
- else
500
- pods.each do |pod|
501
- pod_name = pod.dig('metadata', 'name')
502
- pod_phase = pod.dig('status', 'phase') || 'Unknown'
503
- pod_indicator = case pod_phase
504
- when 'Running' then '✓'
505
- when 'Pending' then '⏳'
506
- when 'Failed' then '✗'
507
- else '?'
508
- end
509
-
510
- puts " #{pod_indicator} #{pod_name}: #{pod_phase}"
511
-
512
- # Check container status
513
- container_statuses = pod.dig('status', 'containerStatuses') || []
514
- container_statuses.each do |status|
515
- ready = status['ready'] ? '✓' : '✗'
516
- puts " #{ready} #{status['name']}: #{status['state']&.keys&.first || 'unknown'}"
517
- end
518
- end
519
- end
520
-
521
- # Test connectivity if endpoint is available
522
- if endpoint && phase == 'Running'
523
- puts
524
- puts 'Testing connectivity...'
525
- begin
526
- uri = URI(endpoint)
527
- response = Net::HTTP.get_response(uri)
528
- if response.code.to_i < 400
529
- Formatters::ProgressFormatter.success('Connectivity test passed')
530
- else
531
- Formatters::ProgressFormatter.warn("HTTP #{response.code}: #{response.message}")
532
- end
533
- rescue StandardError => e
534
- Formatters::ProgressFormatter.error("Connectivity test failed: #{e.message}")
535
- end
536
- end
537
-
538
- # Overall health
539
- puts
540
- if phase == 'Running' && ready_replicas == desired_replicas
541
- Formatters::ProgressFormatter.success("Tool '#{tool_name}' is healthy and operational")
542
- elsif phase == 'Pending'
543
- Formatters::ProgressFormatter.info("Tool '#{tool_name}' is starting up, please wait")
544
- else
545
- Formatters::ProgressFormatter.warn("Tool '#{tool_name}' has issues, check logs for details")
546
- puts
547
- puts 'View logs with:'
548
- puts " kubectl logs -n #{ctx.namespace} -l langop.io/tool=#{tool_name}"
549
- end
550
- end
551
- end
552
-
553
- desc 'search [PATTERN]', 'Search available tools in the registry'
554
- long_desc <<-DESC
555
- Search and list available tools from the registry.
556
-
557
- Without a pattern, lists all available tools.
558
- With a pattern, filters tools by name or description (case-insensitive).
559
-
560
- Examples:
561
- aictl tool search # List all tools
562
- aictl tool search web # Find tools matching "web"
563
- aictl tool search email # Find tools matching "email"
564
- DESC
565
- def search(pattern = nil)
566
- handle_command_error('search tools') do
567
- # Load tool patterns registry
568
- registry = Config::ToolRegistry.new
569
- patterns = registry.fetch
570
-
571
- # Filter out aliases and match pattern
572
- tools = patterns.select do |key, config|
573
- next false if config['alias'] # Skip aliases
574
-
575
- if pattern
576
- # Case-insensitive match on name or description
577
- key.downcase.include?(pattern.downcase) ||
578
- config['description']&.downcase&.include?(pattern.downcase)
579
- else
580
- true
581
- end
582
- end
583
-
584
- if tools.empty?
585
- if pattern
586
- Formatters::ProgressFormatter.info("No tools found matching '#{pattern}'")
587
- else
588
- Formatters::ProgressFormatter.info('No tools found in registry')
589
- end
590
- return
591
- end
592
-
593
- # Display tools in a nice format
594
- tools.each do |name, config|
595
- description = config['description'] || 'No description'
596
-
597
- # Bold the tool name (ANSI escape codes)
598
- bold_name = "\e[1m#{name}\e[0m"
599
- puts "#{bold_name} - #{description}"
600
- end
601
- end
602
- end
603
-
604
- private
605
-
606
- # Fetch MCP capabilities from a running tool server
607
- #
608
- # @param name [String] Tool name
609
- # @param tool [Hash] Tool resource
610
- # @param namespace [String] Kubernetes namespace
611
- # @return [Hash, nil] MCP capabilities or nil if unavailable
612
- def fetch_mcp_capabilities(name, tool, namespace)
613
- return nil unless tool.dig('status', 'phase') == 'Running'
614
-
615
- # Get the service endpoint
616
- port = tool.dig('spec', 'port') || 80
617
-
618
- # Try to query the MCP server using kubectl port-forward
619
- # This is a fallback approach since we can't directly connect from CLI
620
- begin
621
- # Try to find a pod for this tool
622
- label_selector = "app.kubernetes.io/name=#{name}"
623
- pods = ctx.client.list_resources('Pod', namespace: namespace, label_selector: label_selector)
624
-
625
- return nil if pods.empty?
626
-
627
- pod_name = pods.first.dig('metadata', 'name')
628
-
629
- # Query the MCP server using JSON-RPC protocol
630
- # MCP uses the tools/list method to list available tools
631
- json_rpc_request = {
632
- jsonrpc: '2.0',
633
- id: 1,
634
- method: 'tools/list',
635
- params: {}
636
- }.to_json
637
-
638
- result = `kubectl exec -n #{namespace} #{pod_name} -- curl -s -X POST \
639
- http://localhost:#{port}/mcp/tools/list -H "Content-Type: application/json" \
640
- -d '#{json_rpc_request}' 2>/dev/null`
641
-
642
- return nil if result.empty?
643
-
644
- response = JSON.parse(result)
645
- response['result']
646
- rescue StandardError
647
- # Silently fail - capabilities are optional information
648
- nil
649
- end
650
- end
651
- end
652
- end
653
- end
654
- end