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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +53 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +77 -0
  8. data/README.md +3 -11
  9. data/Rakefile +34 -0
  10. data/bin/aictl +7 -0
  11. data/completions/_aictl +232 -0
  12. data/completions/aictl.bash +121 -0
  13. data/completions/aictl.fish +114 -0
  14. data/docs/architecture/agent-runtime.md +585 -0
  15. data/docs/dsl/agent-reference.md +591 -0
  16. data/docs/dsl/best-practices.md +1078 -0
  17. data/docs/dsl/chat-endpoints.md +895 -0
  18. data/docs/dsl/constraints.md +671 -0
  19. data/docs/dsl/mcp-integration.md +1177 -0
  20. data/docs/dsl/webhooks.md +932 -0
  21. data/docs/dsl/workflows.md +744 -0
  22. data/examples/README.md +569 -0
  23. data/examples/agent_example.rb +86 -0
  24. data/examples/chat_endpoint_agent.rb +118 -0
  25. data/examples/github_webhook_agent.rb +171 -0
  26. data/examples/mcp_agent.rb +158 -0
  27. data/examples/oauth_callback_agent.rb +296 -0
  28. data/examples/stripe_webhook_agent.rb +219 -0
  29. data/examples/webhook_agent.rb +80 -0
  30. data/lib/language_operator/agent/base.rb +110 -0
  31. data/lib/language_operator/agent/executor.rb +440 -0
  32. data/lib/language_operator/agent/instrumentation.rb +54 -0
  33. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  34. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  35. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  36. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  37. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  38. data/lib/language_operator/agent/safety/manager.rb +207 -0
  39. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  40. data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
  41. data/lib/language_operator/agent/scheduler.rb +183 -0
  42. data/lib/language_operator/agent/telemetry.rb +116 -0
  43. data/lib/language_operator/agent/web_server.rb +610 -0
  44. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  45. data/lib/language_operator/agent.rb +149 -0
  46. data/lib/language_operator/cli/commands/agent.rb +1252 -0
  47. data/lib/language_operator/cli/commands/cluster.rb +335 -0
  48. data/lib/language_operator/cli/commands/install.rb +404 -0
  49. data/lib/language_operator/cli/commands/model.rb +266 -0
  50. data/lib/language_operator/cli/commands/persona.rb +396 -0
  51. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  52. data/lib/language_operator/cli/commands/status.rb +156 -0
  53. data/lib/language_operator/cli/commands/tool.rb +537 -0
  54. data/lib/language_operator/cli/commands/use.rb +47 -0
  55. data/lib/language_operator/cli/errors/handler.rb +180 -0
  56. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  57. data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
  58. data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
  59. data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
  60. data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
  61. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  62. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  63. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  64. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  65. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  66. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  67. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  68. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  69. data/lib/language_operator/cli/main.rb +232 -0
  70. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  71. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  72. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  73. data/lib/language_operator/client/base.rb +214 -0
  74. data/lib/language_operator/client/config.rb +136 -0
  75. data/lib/language_operator/client/cost_calculator.rb +37 -0
  76. data/lib/language_operator/client/mcp_connector.rb +123 -0
  77. data/lib/language_operator/client.rb +19 -0
  78. data/lib/language_operator/config/cluster_config.rb +101 -0
  79. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  80. data/lib/language_operator/config/tool_registry.rb +96 -0
  81. data/lib/language_operator/config.rb +138 -0
  82. data/lib/language_operator/dsl/adapter.rb +124 -0
  83. data/lib/language_operator/dsl/agent_context.rb +90 -0
  84. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  85. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  86. data/lib/language_operator/dsl/config.rb +119 -0
  87. data/lib/language_operator/dsl/context.rb +50 -0
  88. data/lib/language_operator/dsl/execution_context.rb +47 -0
  89. data/lib/language_operator/dsl/helpers.rb +109 -0
  90. data/lib/language_operator/dsl/http.rb +184 -0
  91. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  92. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  93. data/lib/language_operator/dsl/registry.rb +36 -0
  94. data/lib/language_operator/dsl/shell.rb +125 -0
  95. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  96. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  97. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  98. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  99. data/lib/language_operator/dsl.rb +160 -0
  100. data/lib/language_operator/errors.rb +60 -0
  101. data/lib/language_operator/kubernetes/client.rb +279 -0
  102. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  103. data/lib/language_operator/loggable.rb +47 -0
  104. data/lib/language_operator/logger.rb +141 -0
  105. data/lib/language_operator/retry.rb +123 -0
  106. data/lib/language_operator/retryable.rb +132 -0
  107. data/lib/language_operator/tool_loader.rb +242 -0
  108. data/lib/language_operator/validators.rb +170 -0
  109. data/lib/language_operator/version.rb +1 -1
  110. data/lib/language_operator.rb +65 -3
  111. data/requirements/tasks/challenge.md +9 -0
  112. data/requirements/tasks/iterate.md +36 -0
  113. data/requirements/tasks/optimize.md +21 -0
  114. data/requirements/tasks/tag.md +5 -0
  115. data/test_agent_dsl.rb +108 -0
  116. metadata +503 -20
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+ require 'pastel'
6
+ require_relative '../formatters/progress_formatter'
7
+ require_relative '../formatters/table_formatter'
8
+ require_relative '../helpers/cluster_validator'
9
+ require_relative '../helpers/cluster_context'
10
+ require_relative '../helpers/user_prompts'
11
+ require_relative '../helpers/resource_dependency_checker'
12
+ require_relative '../helpers/editor_helper'
13
+ require_relative '../../config/cluster_config'
14
+ require_relative '../../kubernetes/client'
15
+ require_relative '../../kubernetes/resource_builder'
16
+
17
+ module LanguageOperator
18
+ module CLI
19
+ module Commands
20
+ # Persona management commands
21
+ class Persona < Thor
22
+ include Helpers::ClusterValidator
23
+
24
+ desc 'list', 'List all personas 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
+ personas = ctx.client.list_resources('LanguagePersona', namespace: ctx.namespace)
30
+
31
+ if personas.empty?
32
+ Formatters::ProgressFormatter.info("No personas found in cluster '#{ctx.name}'")
33
+ puts
34
+ puts 'Personas define the personality and capabilities of agents.'
35
+ puts
36
+ puts 'Create a persona with:'
37
+ puts ' aictl persona create <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 = personas.map do |persona|
45
+ name = persona.dig('metadata', 'name')
46
+ used_by = agents.count { |a| a.dig('spec', 'persona') == name }
47
+
48
+ {
49
+ name: name,
50
+ tone: persona.dig('spec', 'tone') || 'neutral',
51
+ used_by: used_by,
52
+ description: persona.dig('spec', 'description') || ''
53
+ }
54
+ end
55
+
56
+ Formatters::TableFormatter.personas(table_data)
57
+ rescue StandardError => e
58
+ Formatters::ProgressFormatter.error("Failed to list personas: #{e.message}")
59
+ raise if ENV['DEBUG']
60
+
61
+ exit 1
62
+ end
63
+
64
+ desc 'show NAME', 'Display full persona details'
65
+ option :cluster, type: :string, desc: 'Override current cluster context'
66
+ def show(name)
67
+ ctx = Helpers::ClusterContext.from_options(options)
68
+
69
+ begin
70
+ persona = ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
71
+ rescue K8s::Error::NotFound
72
+ Formatters::ProgressFormatter.error("Persona '#{name}' not found in cluster '#{ctx.name}'")
73
+ exit 1
74
+ end
75
+
76
+ puts
77
+ puts "Persona: #{pastel.cyan.bold(name)}"
78
+ puts '═' * 80
79
+ puts
80
+
81
+ # Format and display key persona details
82
+ spec = persona['spec'] || {}
83
+ puts "#{pastel.bold('Display Name:')} #{spec['displayName']}"
84
+ puts "#{pastel.bold('Tone:')} #{pastel.yellow(spec['tone'])}" if spec['tone']
85
+ puts
86
+ puts pastel.bold('Description:')
87
+ puts " #{spec['description']}"
88
+ puts
89
+
90
+ if spec['systemPrompt']
91
+ puts pastel.bold('System Prompt:')
92
+ puts " #{spec['systemPrompt']}"
93
+ puts
94
+ end
95
+
96
+ if spec['capabilities']&.any?
97
+ puts pastel.bold('Capabilities:')
98
+ spec['capabilities'].each do |cap|
99
+ puts " #{pastel.green('•')} #{cap}"
100
+ end
101
+ puts
102
+ end
103
+
104
+ if spec['toolPreferences']&.any?
105
+ puts pastel.bold('Tool Preferences:')
106
+ spec['toolPreferences'].each do |pref|
107
+ puts " #{pastel.green('•')} #{pref}"
108
+ end
109
+ puts
110
+ end
111
+
112
+ if spec['responseFormat']
113
+ puts "#{pastel.bold('Response Format:')} #{spec['responseFormat']}"
114
+ puts
115
+ end
116
+
117
+ puts '═' * 80
118
+ puts
119
+ Formatters::ProgressFormatter.info('Use this persona when creating agents:')
120
+ puts " aictl agent create \"description\" --persona #{name}"
121
+ puts
122
+ rescue StandardError => e
123
+ Formatters::ProgressFormatter.error("Failed to show persona: #{e.message}")
124
+ raise if ENV['DEBUG']
125
+
126
+ exit 1
127
+ end
128
+
129
+ desc 'create NAME', 'Create a new persona'
130
+ long_desc <<-DESC
131
+ Create a new persona with the specified name using an interactive wizard.
132
+
133
+ Examples:
134
+ aictl persona create helpful-assistant
135
+ aictl persona create code-reviewer --from helpful-assistant
136
+ DESC
137
+ option :cluster, type: :string, desc: 'Override current cluster context'
138
+ option :from, type: :string, desc: 'Copy from existing persona as starting point'
139
+ def create(name)
140
+ ctx = Helpers::ClusterContext.from_options(options)
141
+
142
+ # Check if persona already exists
143
+ begin
144
+ ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
145
+ Formatters::ProgressFormatter.error("Persona '#{name}' already exists in cluster '#{ctx.name}'")
146
+ puts
147
+ puts 'Use a different name or delete the existing persona first:'
148
+ puts " aictl persona delete #{name}"
149
+ exit 1
150
+ rescue K8s::Error::NotFound
151
+ # Good - persona doesn't exist yet
152
+ end
153
+
154
+ # If --from flag provided, copy from existing persona
155
+ base_persona = nil
156
+ if options[:from]
157
+ begin
158
+ base_persona = ctx.client.get_resource('LanguagePersona', options[:from], ctx.namespace)
159
+ Formatters::ProgressFormatter.info("Copying from persona '#{options[:from]}'")
160
+ puts
161
+ rescue K8s::Error::NotFound
162
+ Formatters::ProgressFormatter.error("Source persona '#{options[:from]}' not found")
163
+ exit 1
164
+ end
165
+ end
166
+
167
+ # Interactive prompts
168
+ require 'tty-prompt'
169
+ prompt = TTY::Prompt.new
170
+
171
+ puts
172
+ puts '=' * 80
173
+ puts ' Create New Persona'
174
+ puts '=' * 80
175
+ puts
176
+
177
+ # Get display name
178
+ default_display_name = base_persona&.dig('spec', 'displayName') || name.split('-').map(&:capitalize).join(' ')
179
+ display_name = prompt.ask('Display Name:', default: default_display_name)
180
+
181
+ # Get description
182
+ default_description = base_persona&.dig('spec', 'description') || ''
183
+ description = prompt.ask('Description:', default: default_description) do |q|
184
+ q.required true
185
+ end
186
+
187
+ # Get tone
188
+ default_tone = base_persona&.dig('spec', 'tone') || 'neutral'
189
+ tone = prompt.select('Tone:', %w[neutral friendly professional technical creative], default: default_tone)
190
+
191
+ # Get system prompt
192
+ puts
193
+ puts 'System Prompt (press Enter to open editor):'
194
+ prompt.keypress('Press any key to continue...')
195
+
196
+ default_system_prompt = base_persona&.dig('spec', 'systemPrompt') || ''
197
+ system_prompt = edit_in_editor(default_system_prompt, 'persona-system-prompt')
198
+
199
+ if system_prompt.strip.empty?
200
+ Formatters::ProgressFormatter.error('System prompt cannot be empty')
201
+ exit 1
202
+ end
203
+
204
+ # Get capabilities
205
+ puts
206
+ puts 'Capabilities (optional, press Enter to open editor, or leave empty to skip):'
207
+ puts 'Describe what this persona can do, one per line.'
208
+ prompt.keypress('Press any key to continue...')
209
+
210
+ default_capabilities = base_persona&.dig('spec', 'capabilities')&.join("\n") || ''
211
+ capabilities_text = edit_in_editor(default_capabilities, 'persona-capabilities')
212
+ capabilities = capabilities_text.strip.empty? ? [] : capabilities_text.strip.split("\n").map(&:strip).reject(&:empty?)
213
+
214
+ # Build persona resource
215
+ persona_spec = {
216
+ 'displayName' => display_name,
217
+ 'description' => description,
218
+ 'tone' => tone,
219
+ 'systemPrompt' => system_prompt.strip
220
+ }
221
+ persona_spec['capabilities'] = capabilities unless capabilities.empty?
222
+
223
+ persona_resource = Kubernetes::ResourceBuilder.build_persona(
224
+ name: name,
225
+ spec: persona_spec,
226
+ namespace: ctx.namespace
227
+ )
228
+
229
+ # Show preview
230
+ puts
231
+ puts '=' * 80
232
+ puts 'Preview:'
233
+ puts '=' * 80
234
+ puts YAML.dump(persona_resource)
235
+ puts '=' * 80
236
+ puts
237
+
238
+ # Confirm creation
239
+ unless prompt.yes?('Create this persona?')
240
+ puts 'Creation cancelled'
241
+ return
242
+ end
243
+
244
+ # Create persona
245
+ Formatters::ProgressFormatter.with_spinner("Creating persona '#{name}'") do
246
+ ctx.client.create_resource(persona_resource)
247
+ end
248
+
249
+ Formatters::ProgressFormatter.success("Persona '#{name}' created successfully")
250
+ puts
251
+ puts 'Use this persona when creating agents:'
252
+ puts " aictl agent create \"description\" --persona #{name}"
253
+ rescue StandardError => e
254
+ Formatters::ProgressFormatter.error("Failed to create persona: #{e.message}")
255
+ raise if ENV['DEBUG']
256
+
257
+ exit 1
258
+ end
259
+
260
+ desc 'edit NAME', 'Edit an existing persona'
261
+ long_desc <<-DESC
262
+ Edit an existing persona by opening the YAML definition in your editor.
263
+
264
+ When you save and close the editor, the persona will be updated.
265
+ Any agents using this persona will be automatically re-synthesized.
266
+
267
+ Example:
268
+ aictl persona edit helpful-assistant
269
+ DESC
270
+ option :cluster, type: :string, desc: 'Override current cluster context'
271
+ def edit(name)
272
+ ctx = Helpers::ClusterContext.from_options(options)
273
+
274
+ # Get current persona
275
+ begin
276
+ persona = ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
277
+ rescue K8s::Error::NotFound
278
+ Formatters::ProgressFormatter.error("Persona '#{name}' not found in cluster '#{ctx.name}'")
279
+ exit 1
280
+ end
281
+
282
+ # Open in editor
283
+ original_yaml = YAML.dump(persona)
284
+ edited_yaml = edit_in_editor(original_yaml, "persona-#{name}")
285
+
286
+ # Check if changed
287
+ if edited_yaml.strip == original_yaml.strip
288
+ Formatters::ProgressFormatter.info('No changes made')
289
+ return
290
+ end
291
+
292
+ # Parse edited YAML
293
+ begin
294
+ edited_persona = YAML.safe_load(edited_yaml)
295
+ rescue Psych::SyntaxError => e
296
+ Formatters::ProgressFormatter.error("Invalid YAML: #{e.message}")
297
+ exit 1
298
+ end
299
+
300
+ # Validate structure
301
+ unless edited_persona.is_a?(Hash) && edited_persona['spec']
302
+ Formatters::ProgressFormatter.error('Invalid persona structure: missing spec')
303
+ exit 1
304
+ end
305
+
306
+ # Update persona
307
+ Formatters::ProgressFormatter.with_spinner("Updating persona '#{name}'") do
308
+ ctx.client.update_resource(edited_persona)
309
+ end
310
+
311
+ Formatters::ProgressFormatter.success("Persona '#{name}' updated successfully")
312
+
313
+ # Check for agents using this persona
314
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
315
+ agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
316
+
317
+ if agents_using.any?
318
+ puts
319
+ Formatters::ProgressFormatter.info("#{agents_using.count} agent(s) will be re-synthesized automatically:")
320
+ agents_using.each do |agent|
321
+ puts " - #{agent.dig('metadata', 'name')}"
322
+ end
323
+ end
324
+ rescue StandardError => e
325
+ Formatters::ProgressFormatter.error("Failed to edit persona: #{e.message}")
326
+ raise if ENV['DEBUG']
327
+
328
+ exit 1
329
+ end
330
+
331
+ desc 'delete NAME', 'Delete a persona'
332
+ option :cluster, type: :string, desc: 'Override current cluster context'
333
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
334
+ def delete(name)
335
+ ctx = Helpers::ClusterContext.from_options(options)
336
+
337
+ # Get persona
338
+ begin
339
+ persona = ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
340
+ rescue K8s::Error::NotFound
341
+ Formatters::ProgressFormatter.error("Persona '#{name}' not found in cluster '#{ctx.name}'")
342
+ exit 1
343
+ end
344
+
345
+ # Check for agents using this persona
346
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
347
+ agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
348
+
349
+ if agents_using.any? && !options[:force]
350
+ Formatters::ProgressFormatter.warn("Persona '#{name}' is in use by #{agents_using.count} agent(s)")
351
+ puts
352
+ puts 'Agents using this persona:'
353
+ agents_using.each do |agent|
354
+ puts " - #{agent.dig('metadata', 'name')}"
355
+ end
356
+ puts
357
+ puts 'Delete these agents first, or use --force to delete anyway.'
358
+ puts
359
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
360
+ end
361
+
362
+ # Confirm deletion unless --force
363
+ unless options[:force] || agents_using.any?
364
+ puts "This will delete persona '#{name}' from cluster '#{ctx.name}':"
365
+ puts " Tone: #{persona.dig('spec', 'tone')}"
366
+ puts " Description: #{persona.dig('spec', 'description')}"
367
+ puts
368
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
369
+ end
370
+
371
+ # Delete persona
372
+ Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
373
+ ctx.client.delete_resource('LanguagePersona', name, ctx.namespace)
374
+ end
375
+
376
+ Formatters::ProgressFormatter.success("Persona '#{name}' deleted successfully")
377
+ rescue StandardError => e
378
+ Formatters::ProgressFormatter.error("Failed to delete persona: #{e.message}")
379
+ raise if ENV['DEBUG']
380
+
381
+ exit 1
382
+ end
383
+
384
+ private
385
+
386
+ def edit_in_editor(content, filename_prefix)
387
+ Helpers::EditorHelper.edit_content(content, filename_prefix, '.txt')
388
+ end
389
+
390
+ def pastel
391
+ @pastel ||= Pastel.new
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../formatters/progress_formatter'
5
+ require_relative '../wizards/quickstart_wizard'
6
+
7
+ module LanguageOperator
8
+ module CLI
9
+ module Commands
10
+ # Quickstart wizard for first-time users
11
+ class Quickstart < Thor
12
+ desc 'start', 'Interactive setup wizard for first-time users'
13
+ def start
14
+ wizard = Wizards::QuickstartWizard.new
15
+ wizard.run
16
+ end
17
+
18
+ default_task :start
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../formatters/progress_formatter'
5
+ require_relative '../formatters/table_formatter'
6
+ require_relative '../../config/cluster_config'
7
+ require_relative '../../kubernetes/client'
8
+ require_relative '../helpers/cluster_validator'
9
+
10
+ module LanguageOperator
11
+ module CLI
12
+ module Commands
13
+ # System status and overview command
14
+ class Status < Thor
15
+ desc 'overview', 'Show system status and overview'
16
+ def overview
17
+ current_cluster = Config::ClusterConfig.current_cluster
18
+ clusters = Config::ClusterConfig.list_clusters
19
+
20
+ pastel = Pastel.new
21
+
22
+ # Current cluster context
23
+ if current_cluster
24
+ cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
25
+
26
+ puts "\nCluster Details"
27
+ puts '----------------'
28
+ puts "Name: #{pastel.bold.white(current_cluster)}"
29
+ puts "Namespace: #{pastel.bold.white(cluster_config[:namespace])}"
30
+ puts
31
+
32
+ # Check cluster health
33
+ begin
34
+ k8s = Helpers::ClusterValidator.kubernetes_client(current_cluster)
35
+
36
+ # Operator status
37
+ if k8s.operator_installed?
38
+ version = k8s.operator_version || 'installed'
39
+ Formatters::ProgressFormatter.success("Operator: #{version}")
40
+ else
41
+ Formatters::ProgressFormatter.error('Operator: not installed')
42
+ puts
43
+ puts 'Install the operator with:'
44
+ puts ' aictl install'
45
+ puts
46
+ return
47
+ end
48
+
49
+ # Agent statistics
50
+ agents = k8s.list_resources('LanguageAgent', namespace: cluster_config[:namespace])
51
+ agent_stats = categorize_by_status(agents)
52
+
53
+ puts
54
+ puts "Agents (#{agents.count} total):"
55
+ agent_stats.each do |status, count|
56
+ puts " #{format_status(status)}: #{count}"
57
+ end
58
+
59
+ # Tool statistics
60
+ tools = k8s.list_resources('LanguageTool', namespace: cluster_config[:namespace])
61
+ puts
62
+ puts "Tools (#{tools.count} total):"
63
+ if tools.any?
64
+ tool_types = tools.group_by { |t| t.dig('spec', 'type') }
65
+ tool_types.each do |type, items|
66
+ puts " #{type}: #{items.count}"
67
+ end
68
+ else
69
+ puts ' (none)'
70
+ end
71
+
72
+ # Model statistics
73
+ models = k8s.list_resources('LanguageModel', namespace: cluster_config[:namespace])
74
+ puts
75
+ puts "Models (#{models.count} total):"
76
+ if models.any?
77
+ model_providers = models.group_by { |m| m.dig('spec', 'provider') }
78
+ model_providers.each do |provider, items|
79
+ puts " #{provider}: #{items.count}"
80
+ end
81
+ else
82
+ puts ' (none)'
83
+ end
84
+
85
+ # Persona statistics
86
+ personas = k8s.list_resources('LanguagePersona', namespace: cluster_config[:namespace])
87
+ puts
88
+ puts "Personas (#{personas.count} total):"
89
+ if personas.any?
90
+ personas.each do |persona|
91
+ tone = persona.dig('spec', 'tone')
92
+ puts " - #{persona.dig('metadata', 'name')} (#{tone})"
93
+ end
94
+ else
95
+ puts ' (none)'
96
+ end
97
+ rescue StandardError => e
98
+ Formatters::ProgressFormatter.error("Connection failed: #{e.message}")
99
+ puts
100
+ puts 'Check your cluster connection and try again'
101
+ end
102
+ else
103
+ Formatters::ProgressFormatter.warn('No cluster selected')
104
+ puts
105
+ if clusters.any?
106
+ puts 'Available clusters:'
107
+ clusters.each do |cluster|
108
+ puts " - #{cluster[:name]}"
109
+ end
110
+ puts
111
+ puts 'Select a cluster with:'
112
+ puts ' aictl use <cluster>'
113
+ else
114
+ puts 'No clusters found. Create one with:'
115
+ puts ' aictl cluster create <name>'
116
+ end
117
+ end
118
+
119
+ puts
120
+ end
121
+
122
+ default_task :overview
123
+
124
+ private
125
+
126
+ def format_status(status)
127
+ require 'pastel'
128
+ pastel = Pastel.new
129
+
130
+ status_str = status.to_s
131
+ case status_str.downcase
132
+ when 'ready', 'running', 'active'
133
+ "#{pastel.green('●')} #{status_str}"
134
+ when 'pending', 'creating', 'synthesizing'
135
+ "#{pastel.yellow('●')} #{status_str}"
136
+ when 'failed', 'error'
137
+ "#{pastel.red('●')} #{status_str}"
138
+ when 'paused', 'stopped'
139
+ "#{pastel.dim('●')} #{status_str}"
140
+ else
141
+ "#{pastel.dim('●')} #{status_str}"
142
+ end
143
+ end
144
+
145
+ def categorize_by_status(resources)
146
+ stats = Hash.new(0)
147
+ resources.each do |resource|
148
+ status = resource.dig('status', 'phase') || 'Unknown'
149
+ stats[status] += 1
150
+ end
151
+ stats
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end