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