language-operator 0.1.61 → 0.1.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -2,19 +2,22 @@
2
2
 
3
3
  require 'thor'
4
4
  require 'fileutils'
5
+ require_relative '../utils/secure_path'
5
6
  require_relative 'commands/cluster'
6
7
  require_relative 'commands/use'
7
- require_relative 'commands/agent'
8
+ require_relative 'commands/agent/base'
8
9
  require_relative 'commands/status'
9
10
  require_relative 'commands/persona'
10
- require_relative 'commands/tool'
11
- require_relative 'commands/model'
11
+ require_relative 'commands/tool/base'
12
+ require_relative 'commands/model/base'
12
13
  require_relative 'commands/quickstart'
13
14
  require_relative 'commands/install'
14
- require_relative 'commands/system'
15
+ require_relative 'commands/system/base'
15
16
  require_relative 'formatters/progress_formatter'
17
+ require_relative 'helpers/ux_helper'
16
18
  require_relative '../config/cluster_config'
17
19
  require_relative '../kubernetes/client'
20
+ require_relative 'errors/thor_errors'
18
21
 
19
22
  module LanguageOperator
20
23
  module CLI
@@ -22,6 +25,8 @@ module LanguageOperator
22
25
  #
23
26
  # Provides commands for creating, running, and managing language-operator resources.
24
27
  class Main < Thor
28
+ include Helpers::UxHelper
29
+
25
30
  def self.exit_on_failure?
26
31
  true
27
32
  end
@@ -33,11 +38,11 @@ module LanguageOperator
33
38
 
34
39
  desc 'version', 'Show aictl and operator version'
35
40
  def version
36
- puts "aictl v#{LanguageOperator::VERSION}"
37
- puts
38
-
39
- # Try to get operator version from current cluster
41
+ # Check operator installation status first
40
42
  current_cluster = Config::ClusterConfig.current_cluster
43
+ operator_version = nil
44
+ operator_installed = false
45
+
41
46
  if current_cluster
42
47
  cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
43
48
  begin
@@ -47,52 +52,46 @@ module LanguageOperator
47
52
  )
48
53
 
49
54
  if k8s.operator_installed?
55
+ operator_installed = true
50
56
  operator_version = k8s.operator_version || 'unknown'
51
- puts "Operator: v#{operator_version}"
52
- puts "Cluster: #{current_cluster}"
53
-
54
- # Check compatibility (simple version check)
55
- # In the future, this could be more sophisticated
56
- puts
57
- Formatters::ProgressFormatter.success('Versions are compatible')
58
- else
59
- Formatters::ProgressFormatter.warn("Operator not installed in cluster '#{current_cluster}'")
60
57
  end
61
- rescue StandardError => e
62
- Formatters::ProgressFormatter.error("Could not connect to cluster: #{e.message}")
58
+ rescue StandardError
59
+ # Silently handle connection errors - we'll show appropriate message below
63
60
  end
61
+ end
62
+
63
+ # Show sparkly logo with appropriate subtitle
64
+ if operator_installed
65
+ logo(sparkle: true, title: "kubernetes language-operator detected (v#{operator_version})")
64
66
  else
65
- puts 'No cluster selected'
66
- puts
67
- puts 'Select a cluster to check operator version:'
68
- puts ' aictl use <cluster>'
67
+ logo(sparkle: true, title: 'kubernetes language-operator not found')
69
68
  end
70
69
  end
71
70
 
72
- desc 'cluster SUBCOMMAND ...ARGS', 'Manage language clusters'
71
+ desc 'cluster SUBCOMMAND ...ARGS', 'Manage clusters'
73
72
  subcommand 'cluster', Commands::Cluster
74
73
 
75
- desc 'use CLUSTER', 'Switch to a different cluster context'
74
+ desc 'use CLUSTER', 'Switch to a different cluster'
76
75
  def use(cluster_name)
77
76
  Commands::Use.new.switch(cluster_name)
78
77
  end
79
78
 
80
- desc 'agent SUBCOMMAND ...ARGS', 'Manage autonomous agents'
81
- subcommand 'agent', Commands::Agent
79
+ desc 'agent SUBCOMMAND ...ARGS', 'Manage agents'
80
+ subcommand 'agent', Commands::Agent::Base
82
81
 
83
- desc 'persona SUBCOMMAND ...ARGS', 'Manage agent personas'
82
+ desc 'persona SUBCOMMAND ...ARGS', 'Manage personas'
84
83
  subcommand 'persona', Commands::Persona
85
84
 
86
- desc 'tool SUBCOMMAND ...ARGS', 'Manage MCP tools'
87
- subcommand 'tool', Commands::Tool
85
+ desc 'tool SUBCOMMAND ...ARGS', 'Manage tools'
86
+ subcommand 'tool', Commands::Tool::Base
88
87
 
89
- desc 'model SUBCOMMAND ...ARGS', 'Manage language models'
90
- subcommand 'model', Commands::Model
88
+ desc 'model SUBCOMMAND ...ARGS', 'Manage models'
89
+ subcommand 'model', Commands::Model::Base
91
90
 
92
- desc 'system SUBCOMMAND ...ARGS', 'System commands for schema and metadata'
93
- subcommand 'system', Commands::System
91
+ desc 'system SUBCOMMAND ...ARGS', 'System utilities'
92
+ subcommand 'system', Commands::System::Base
94
93
 
95
- desc 'quickstart', 'Interactive setup wizard for first-time users'
94
+ desc 'quickstart', 'Wizard for first-time users'
96
95
  def quickstart
97
96
  Commands::Quickstart.new.invoke(:start)
98
97
  end
@@ -157,13 +156,24 @@ module LanguageOperator
157
156
  when 'fish'
158
157
  install_fish_completion
159
158
  else
160
- Formatters::ProgressFormatter.error("Unsupported shell: #{shell}")
159
+ message = "Unsupported shell: #{shell}"
160
+ Formatters::ProgressFormatter.error(message)
161
161
  puts
162
162
  puts 'Supported shells: bash, zsh, fish'
163
- exit 1
163
+ raise Errors::ValidationError, message
164
164
  end
165
165
  end
166
166
 
167
+ def help(command = nil, subcommand = false)
168
+ if command.nil? && !subcommand
169
+ # Show logo when displaying general help (no specific command)
170
+ logo
171
+ end
172
+
173
+ # Delegate to Thor's original help method
174
+ super
175
+ end
176
+
167
177
  private
168
178
 
169
179
  def install_bash_completion
@@ -174,7 +184,7 @@ module LanguageOperator
174
184
  return
175
185
  end
176
186
 
177
- target = File.expand_path('~/.bash_completion.d/aictl')
187
+ target = LanguageOperator::Utils::SecurePath.expand_home_path('.bash_completion.d/aictl')
178
188
  FileUtils.mkdir_p(File.dirname(target))
179
189
  FileUtils.cp(completion_file, target)
180
190
 
@@ -196,7 +206,7 @@ module LanguageOperator
196
206
  end
197
207
 
198
208
  # Check if user has a custom fpath directory
199
- fpath_dir = File.expand_path('~/.zsh/completions')
209
+ fpath_dir = LanguageOperator::Utils::SecurePath.expand_home_path('.zsh/completions')
200
210
  FileUtils.mkdir_p(fpath_dir)
201
211
 
202
212
  target = File.join(fpath_dir, '_aictl')
@@ -220,7 +230,7 @@ module LanguageOperator
220
230
  return
221
231
  end
222
232
 
223
- target = File.expand_path('~/.config/fish/completions/aictl.fish')
233
+ target = LanguageOperator::Utils::SecurePath.expand_home_path('.config/fish/completions/aictl.fish')
224
234
  FileUtils.mkdir_p(File.dirname(target))
225
235
  FileUtils.cp(completion_file, target)
226
236
 
@@ -4,6 +4,9 @@ metadata:
4
4
  name: <%= name %>
5
5
  namespace: <%= namespace %>
6
6
  spec:
7
+ <% if cluster_ref -%>
8
+ clusterRef: <%= cluster_ref %>
9
+ <% end -%>
7
10
  type: <%= type || 'mcp' %>
8
11
  image: <%= image || "changeme/#{name}-tool:latest" %>
9
12
  imagePullPolicy: Always
@@ -1,19 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tty-prompt'
4
- require 'pastel'
5
3
  require_relative '../helpers/schedule_builder'
4
+ require_relative '../helpers/ux_helper'
6
5
 
7
6
  module LanguageOperator
8
7
  module CLI
9
8
  module Wizards
10
9
  # Interactive wizard for creating agents
11
10
  class AgentWizard
12
- attr_reader :prompt, :pastel
11
+ include Helpers::UxHelper
13
12
 
14
13
  def initialize
15
- @prompt = TTY::Prompt.new
16
- @pastel = Pastel.new
14
+ # UxHelper provides pastel and prompt methods
17
15
  end
18
16
 
19
17
  # Run the wizard and return the generated description
@@ -42,11 +40,7 @@ module LanguageOperator
42
40
  private
43
41
 
44
42
  def show_welcome
45
- puts
46
- puts pastel.cyan('╭────────────────────────────────────────╮')
47
- puts pastel.cyan('│ Let\'s create your agent! 🤖 │')
48
- puts pastel.cyan('╰────────────────────────────────────────╯')
49
- puts
43
+ logo(title: 'create an agent')
50
44
  end
51
45
 
52
46
  def ask_task_description
@@ -216,23 +210,21 @@ module LanguageOperator
216
210
 
217
211
  def show_preview(description, schedule_info, tools_config)
218
212
  puts
219
- puts pastel.cyan('╭─ Preview ──────────────────────────────╮')
220
- puts '│'
221
- puts "#{pastel.bold('Task:')} #{description}"
213
+
214
+ content = []
215
+ content << "#{pastel.bold('Task:')} #{description}"
222
216
 
223
217
  if schedule_info[:type] == :manual
224
- puts "#{pastel.bold('Mode:')} Manual trigger"
218
+ content << "#{pastel.bold('Mode:')} Manual trigger"
225
219
  else
226
220
  schedule_text = schedule_info[:description] || 'on demand'
227
- puts "#{pastel.bold('Schedule:')} #{schedule_text}"
221
+ content << "#{pastel.bold('Schedule:')} #{schedule_text}"
228
222
  end
229
223
 
230
- puts "#{pastel.bold('Cron:')} #{pastel.dim(schedule_info[:cron])}" if schedule_info[:cron]
231
-
232
- puts "│ #{pastel.bold('Tools:')} #{tools_config[:tools].join(', ')}" if tools_config[:tools]&.any?
224
+ content << "#{pastel.bold('Cron:')} #{pastel.dim(schedule_info[:cron])}" if schedule_info[:cron]
225
+ content << "#{pastel.bold('Tools:')} #{tools_config[:tools].join(', ')}" if tools_config[:tools]&.any?
233
226
 
234
- puts ''
235
- puts pastel.cyan('╰────────────────────────────────────────╯')
227
+ puts box(content.join("\n"), title: 'Preview')
236
228
  puts
237
229
  end
238
230
 
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/ux_helper'
4
+ require_relative '../helpers/validation_helper'
5
+ require_relative '../helpers/provider_helper'
6
+ require_relative '../formatters/progress_formatter'
7
+ require_relative '../../kubernetes/client'
8
+ require_relative '../../kubernetes/resource_builder'
9
+
10
+ module LanguageOperator
11
+ module CLI
12
+ module Wizards
13
+ # Interactive wizard for creating language models
14
+ #
15
+ # Guides users through provider selection, credential input,
16
+ # model selection, and resource creation.
17
+ #
18
+ # @example
19
+ # wizard = Wizards::ModelWizard.new(ctx)
20
+ # wizard.run
21
+ class ModelWizard
22
+ include Helpers::UxHelper
23
+ include Helpers::ValidationHelper
24
+ include Helpers::ProviderHelper
25
+
26
+ attr_reader :ctx
27
+
28
+ def initialize(ctx)
29
+ @ctx = ctx
30
+ end
31
+
32
+ # Run the wizard and create the model
33
+ #
34
+ # @return [Boolean] true if model was created successfully
35
+ def run
36
+ show_welcome
37
+
38
+ # Step 1: Provider selection
39
+ provider_info = select_provider
40
+ return false unless provider_info
41
+
42
+ # Step 2: Get credentials
43
+ credentials = get_credentials(provider_info)
44
+ return false unless credentials
45
+
46
+ # Step 3: Test connection
47
+ test_result = test_connection(provider_info, credentials)
48
+ return false unless test_result[:success]
49
+
50
+ # Step 4: Select model
51
+ model_id = select_model(provider_info, credentials)
52
+ return false unless model_id
53
+
54
+ # Step 5: Get display name
55
+ model_name = get_model_name(model_id)
56
+ return false unless model_name
57
+
58
+ # Create resources
59
+ success = create_model_resource(model_name, provider_info, credentials, model_id)
60
+ return false unless success
61
+
62
+ # Show success
63
+ show_success(model_name, model_id, provider_info)
64
+
65
+ true
66
+ end
67
+
68
+ private
69
+
70
+ def show_welcome
71
+ logo(title: "add a model to cluster '#{ctx.name}'")
72
+ end
73
+
74
+ def select_provider
75
+ provider = prompt.select('Select a provider:') do |menu|
76
+ menu.choice 'Anthropic', :anthropic
77
+ menu.choice 'OpenAI', :openai
78
+ menu.choice 'Other (OpenAI-compatible)', :openai_compatible
79
+ end
80
+
81
+ case provider
82
+ when :anthropic
83
+ { provider: :anthropic, provider_key: 'anthropic', display_name: 'Anthropic' }
84
+ when :openai
85
+ { provider: :openai, provider_key: 'openai', display_name: 'OpenAI' }
86
+ when :openai_compatible
87
+ endpoint = ask_url('API endpoint URL (e.g., http://localhost:11434):')
88
+ return nil unless endpoint
89
+
90
+ { provider: :openai_compatible, provider_key: 'openai-compatible',
91
+ display_name: 'OpenAI-Compatible', endpoint: endpoint }
92
+ end
93
+ rescue TTY::Reader::InputInterrupt
94
+ Formatters::ProgressFormatter.error('Cancelled')
95
+ nil
96
+ end
97
+
98
+ def get_credentials(provider_info)
99
+ case provider_info[:provider]
100
+ when :anthropic
101
+ show_credential_help('Anthropic', 'https://console.anthropic.com')
102
+ api_key = ask_secret('Enter your Anthropic API key:')
103
+ return nil unless api_key
104
+
105
+ { api_key: api_key }
106
+ when :openai
107
+ show_credential_help('OpenAI', 'https://platform.openai.com/api-keys')
108
+ api_key = ask_secret('Enter your OpenAI API key:')
109
+ return nil unless api_key
110
+
111
+ { api_key: api_key }
112
+ when :openai_compatible
113
+ needs_auth = ask_yes_no('Does this endpoint require authentication?', default: false)
114
+ return nil if needs_auth.nil?
115
+
116
+ api_key = needs_auth ? ask_secret('Enter API key:') : nil
117
+ { api_key: api_key, endpoint: provider_info[:endpoint] }
118
+ end
119
+ end
120
+
121
+ def show_credential_help(_provider_name, url)
122
+ puts "If you need to, get your API key at #{pastel.cyan(url)}."
123
+ puts
124
+ end
125
+
126
+ def test_connection(provider_info, credentials)
127
+ puts
128
+ test_result = Formatters::ProgressFormatter.with_spinner('Testing connection') do
129
+ test_provider_connection(
130
+ provider_info[:provider],
131
+ api_key: credentials[:api_key],
132
+ endpoint: provider_info[:endpoint]
133
+ )
134
+ end
135
+
136
+ unless test_result[:success]
137
+ Formatters::ProgressFormatter.error("Connection failed: #{test_result[:error]}")
138
+ puts
139
+ retry_choice = ask_yes_no('Try again with different credentials?', default: false)
140
+ if retry_choice
141
+ new_credentials = get_credentials(provider_info)
142
+ return { success: false } unless new_credentials
143
+
144
+ return test_connection(provider_info, new_credentials)
145
+ end
146
+ return { success: false }
147
+ end
148
+
149
+ puts
150
+ { success: true }
151
+ end
152
+
153
+ def select_model(provider_info, credentials)
154
+ available_models = fetch_provider_models(
155
+ provider_info[:provider],
156
+ api_key: credentials[:api_key],
157
+ endpoint: provider_info[:endpoint]
158
+ )
159
+
160
+ if available_models.nil? || available_models.empty?
161
+ Formatters::ProgressFormatter.warn('Could not fetch available models')
162
+ puts
163
+ model_id = prompt.ask('Enter model identifier manually:') do |q|
164
+ q.required true
165
+ end
166
+ return model_id
167
+ end
168
+
169
+ ask_select('Select a model:', available_models, per_page: 10)
170
+ rescue TTY::Reader::InputInterrupt
171
+ Formatters::ProgressFormatter.error('Cancelled')
172
+ nil
173
+ end
174
+
175
+ def get_model_name(model_id)
176
+ # Generate smart default from model_id
177
+ # Examples: "gpt-4-turbo" -> "gpt-4-turbo", "claude-3-opus-20240229" -> "claude-3-opus-20240229"
178
+ # "mistralai/magistral-small-2509" -> "magistral-small-2509"
179
+ default_name = model_id.split('/').last.downcase.gsub(/[^0-9a-z]/i, '-').gsub(/-+/, '-')
180
+ default_name = default_name[0..62] if default_name.length > 63 # K8s limit is 63 chars
181
+
182
+ ask_k8s_name('Your name for this model:', default: default_name)
183
+ rescue TTY::Reader::InputInterrupt
184
+ Formatters::ProgressFormatter.error('Cancelled')
185
+ nil
186
+ end
187
+
188
+ def create_model_resource(model_name, provider_info, credentials, model_id)
189
+ # Check if model already exists
190
+ begin
191
+ ctx.client.get_resource('LanguageModel', model_name, ctx.namespace)
192
+ Formatters::ProgressFormatter.error("Model '#{model_name}' already exists in cluster '#{ctx.name}'")
193
+ puts
194
+ puts "Use 'aictl model inspect #{model_name}' to view details"
195
+ puts "Use 'aictl model edit #{model_name}' to modify it"
196
+ return false
197
+ rescue K8s::Error::NotFound
198
+ # Expected - model doesn't exist yet
199
+ end
200
+
201
+ puts
202
+ Formatters::ProgressFormatter.with_spinner("Creating model '#{model_name}'") do
203
+ # Create API key secret if provided
204
+ if credentials[:api_key]
205
+ secret_name = "#{model_name}-api-key"
206
+ secret = {
207
+ 'apiVersion' => 'v1',
208
+ 'kind' => 'Secret',
209
+ 'metadata' => {
210
+ 'name' => secret_name,
211
+ 'namespace' => ctx.namespace
212
+ },
213
+ 'type' => 'Opaque',
214
+ 'stringData' => {
215
+ 'api-key' => credentials[:api_key]
216
+ }
217
+ }
218
+ ctx.client.apply_resource(secret)
219
+ end
220
+
221
+ # Create LanguageModel resource
222
+ resource = Kubernetes::ResourceBuilder.language_model(
223
+ model_name,
224
+ provider: provider_info[:provider_key],
225
+ model: model_id,
226
+ endpoint: provider_info[:endpoint],
227
+ cluster: ctx.namespace
228
+ )
229
+
230
+ # Add API key reference if secret was created
231
+ if credentials[:api_key]
232
+ resource['spec']['apiKeySecret'] = {
233
+ 'name' => "#{model_name}-api-key",
234
+ 'key' => 'api-key'
235
+ }
236
+ end
237
+
238
+ ctx.client.apply_resource(resource)
239
+ end
240
+
241
+ true
242
+ rescue StandardError => e
243
+ Formatters::ProgressFormatter.error("Failed to create model: #{e.message}")
244
+ false
245
+ end
246
+
247
+ def show_success(model_name, model_id, provider_info)
248
+ puts
249
+ highlighted_box(
250
+ title: 'LanguageModel Details',
251
+ rows: {
252
+ 'Name' => model_name,
253
+ 'Provider' => provider_info[:display_name],
254
+ 'Model' => model_id,
255
+ 'Endpoint' => provider_info[:endpoint],
256
+ 'Cluster' => ctx.name
257
+ }
258
+ )
259
+ puts
260
+ puts 'Next steps:'
261
+ puts
262
+ puts '1. Use this model in an agent:'
263
+ puts " #{pastel.dim("aictl agent create --model #{model_name}")}"
264
+ puts '2. View model details:'
265
+ puts " #{pastel.dim("aictl model inspect #{model_name}")}"
266
+ puts
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
@@ -1,21 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tty-prompt'
4
- require 'pastel'
5
3
  require 'k8s-ruby'
6
4
  require_relative '../formatters/progress_formatter'
5
+ require_relative '../helpers/ux_helper'
7
6
  require_relative '../../config/cluster_config'
8
7
  require_relative '../../kubernetes/client'
9
8
  require_relative '../../kubernetes/resource_builder'
9
+ require_relative '../../utils/secure_path'
10
10
 
11
11
  module LanguageOperator
12
12
  module CLI
13
13
  module Wizards
14
14
  # Interactive quickstart wizard for first-time setup
15
15
  class QuickstartWizard
16
+ include Helpers::UxHelper
17
+
16
18
  def initialize
17
- @prompt = TTY::Prompt.new
18
- @pastel = Pastel.new
19
+ # UxHelper provides pastel and prompt methods
19
20
  end
20
21
 
21
22
  def run
@@ -38,31 +39,11 @@ module LanguageOperator
38
39
 
39
40
  private
40
41
 
41
- attr_reader :prompt, :pastel
42
-
43
42
  def show_welcome
44
- puts
45
- puts pastel.cyan('╭────────────────────────────────────────────────╮')
46
- puts "#{pastel.cyan('│')} Welcome to Language Operator! 🎉 #{pastel.cyan('│')}"
47
- puts "#{pastel.cyan('│')} Let's get you set up (takes ~5 minutes) #{pastel.cyan('│')}"
48
- puts pastel.cyan('╰────────────────────────────────────────────────╯')
49
- puts
50
- puts 'This wizard will help you:'
51
- puts ' 1. Connect to your Kubernetes cluster'
52
- puts ' 2. Configure a language model'
53
- puts ' 3. Create your first autonomous agent'
54
- puts
55
- puts pastel.dim('Press Enter to begin...')
56
- $stdin.gets
43
+ logo(title: 'cluster setup quick start')
57
44
  end
58
45
 
59
46
  def setup_cluster
60
- puts
61
- puts '─' * 50
62
- puts pastel.cyan('Step 1/3: Connect to Kubernetes')
63
- puts '─' * 50
64
- puts
65
-
66
47
  # Check if user has kubectl configured
67
48
  has_kubectl = prompt.yes?('Do you have kubectl configured?')
68
49
 
@@ -99,7 +80,7 @@ module LanguageOperator
99
80
  end
100
81
 
101
82
  def get_kubectl_contexts
102
- kubeconfig_path = ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config'))
83
+ kubeconfig_path = ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config'))
103
84
 
104
85
  return [] unless File.exist?(kubeconfig_path)
105
86
 
@@ -111,7 +92,7 @@ module LanguageOperator
111
92
  end
112
93
 
113
94
  def create_cluster_config(name, context)
114
- kubeconfig_path = ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config'))
95
+ kubeconfig_path = ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config'))
115
96
 
116
97
  Formatters::ProgressFormatter.with_spinner("Creating cluster '#{name}'") do
117
98
  # Create Kubernetes client to verify connection
@@ -138,13 +119,6 @@ module LanguageOperator
138
119
 
139
120
  { name: name, namespace: namespace, kubeconfig: kubeconfig_path, context: context, k8s: k8s }
140
121
  end
141
-
142
- {
143
- name: name,
144
- namespace: (kubeconfig_path && K8s::Config.load_file(kubeconfig_path).context(context).namespace) || 'default',
145
- kubeconfig: kubeconfig_path,
146
- context: context
147
- }
148
122
  rescue StandardError => e
149
123
  puts
150
124
  Formatters::ProgressFormatter.error("Failed to connect: #{e.message}")
@@ -168,6 +168,34 @@ module LanguageOperator
168
168
  @debug
169
169
  end
170
170
 
171
+ # Cleanup all MCP connections and reset client state
172
+ #
173
+ # This method properly closes all MCP client connections and clears
174
+ # the @clients array to prevent resource leaks. Should be called
175
+ # when the client is no longer needed.
176
+ #
177
+ # @return [void]
178
+ def cleanup_connections
179
+ return if @clients.empty?
180
+
181
+ logger.debug('Cleaning up MCP connections', client_count: @clients.length)
182
+
183
+ @clients.each do |client|
184
+ # Close the client connection if it responds to close
185
+ client.close if client.respond_to?(:close)
186
+ rescue StandardError => e
187
+ logger.warn('Error closing MCP client connection',
188
+ client: client.class.name,
189
+ error: e.message)
190
+ end
191
+
192
+ # Clear the clients array and chat session
193
+ @clients.clear
194
+ @chat = nil
195
+
196
+ logger.debug('MCP connections cleanup completed')
197
+ end
198
+
171
199
  private
172
200
 
173
201
  def logger_component
@@ -24,7 +24,10 @@ module LanguageOperator
24
24
  # @return [Hash] Configuration hash
25
25
  # @raise [Errno::ENOENT] If file doesn't exist
26
26
  def self.load(path)
27
- YAML.load_file(path)
27
+ config = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
28
+ # Normalize structure to ensure defaults
29
+ config['mcp_servers'] ||= []
30
+ config
28
31
  end
29
32
 
30
33
  # Load configuration from environment variables
@@ -10,7 +10,7 @@ module LanguageOperator
10
10
  #
11
11
  # @return [void]
12
12
  def connect_mcp_servers
13
- enabled_servers = @config['mcp_servers'].select { |s| s['enabled'] }
13
+ enabled_servers = (@config['mcp_servers'] || []).select { |s| s['enabled'] }
14
14
 
15
15
  if enabled_servers.empty?
16
16
  logger.info('No MCP servers configured, agent will run without tools')