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