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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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
|