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