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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tty-table'
4
- require_relative '../helpers/pastel_helper'
3
+ require_relative '../helpers/ux_helper'
5
4
  require_relative 'status_formatter'
6
5
 
7
6
  module LanguageOperator
@@ -10,43 +9,57 @@ module LanguageOperator
10
9
  # Table output for CLI list commands
11
10
  class TableFormatter
12
11
  class << self
13
- include Helpers::PastelHelper
12
+ include Helpers::UxHelper
14
13
 
15
14
  def clusters(clusters)
16
15
  return ProgressFormatter.info('No clusters found') if clusters.empty?
17
16
 
18
- headers = %w[NAME NAMESPACE AGENTS TOOLS MODELS STATUS]
17
+ headers = ['', 'NAME', 'NAMESPACE', 'STATUS', 'DOMAIN']
19
18
  rows = clusters.map do |cluster|
19
+ # Extract asterisk for selected cluster
20
+ name = cluster[:name].to_s.gsub(' *', '')
21
+ is_current = cluster[:name].to_s.include?(' *')
22
+
23
+ # Apply bold yellow formatting if current cluster
24
+ formatted_name = is_current ? pastel.bold.yellow(name) : name
25
+
26
+ # Format domain - show empty cell if nil or empty, handle error states
27
+ domain_display = case cluster[:domain]
28
+ when nil, ''
29
+ ''
30
+ when '?', '-'
31
+ cluster[:domain]
32
+ else
33
+ cluster[:domain]
34
+ end
35
+
20
36
  [
21
- cluster[:name],
37
+ StatusFormatter.dot(cluster[:status] || 'Unknown'),
38
+ formatted_name,
22
39
  cluster[:namespace],
23
- cluster[:agents] || 0,
24
- cluster[:tools] || 0,
25
- cluster[:models] || 0,
26
- StatusFormatter.format(cluster[:status] || 'Unknown')
40
+ cluster[:status] || 'Unknown',
41
+ domain_display
27
42
  ]
28
43
  end
29
44
 
30
- table = TTY::Table.new(headers, rows)
31
- puts table.render(:unicode, padding: [0, 1])
45
+ puts table(headers, rows)
32
46
  end
33
47
 
34
48
  def agents(agents)
35
49
  return ProgressFormatter.info('No agents found') if agents.empty?
36
50
 
37
- headers = ['NAME', 'MODE', 'STATUS', 'NEXT RUN', 'EXECUTIONS']
51
+ headers = ['', 'NAME', 'NAMESPACE', 'STATUS', 'MODE']
38
52
  rows = agents.map do |agent|
39
53
  [
54
+ StatusFormatter.dot(agent[:status]),
40
55
  agent[:name],
41
- agent[:mode],
42
- StatusFormatter.format(agent[:status]),
43
- agent[:next_run] || 'N/A',
44
- agent[:executions] || 0
56
+ agent[:namespace],
57
+ agent[:status] || 'Unknown',
58
+ agent[:mode]
45
59
  ]
46
60
  end
47
61
 
48
- table = TTY::Table.new(headers, rows)
49
- puts table.render(:unicode, padding: [0, 1])
62
+ puts table(headers, rows)
50
63
  end
51
64
 
52
65
  def all_agents(agents_by_cluster)
@@ -68,25 +81,23 @@ module LanguageOperator
68
81
  end
69
82
  end
70
83
 
71
- table = TTY::Table.new(headers, rows)
72
- puts table.render(:unicode, padding: [0, 1])
84
+ puts table(headers, rows)
73
85
  end
74
86
 
75
87
  def tools(tools)
76
88
  return ProgressFormatter.info('No tools found') if tools.empty?
77
89
 
78
- headers = ['NAME', 'TYPE', 'STATUS', 'AGENTS USING']
90
+ headers = ['', 'NAME', 'NAMESPACE', 'STATUS']
79
91
  rows = tools.map do |tool|
80
92
  [
93
+ StatusFormatter.dot(tool[:status]),
81
94
  tool[:name],
82
- tool[:type],
83
- StatusFormatter.format(tool[:status]),
84
- tool[:agents_using] || 0
95
+ tool[:namespace],
96
+ tool[:status] || 'Unknown'
85
97
  ]
86
98
  end
87
99
 
88
- table = TTY::Table.new(headers, rows)
89
- puts table.render(:unicode, padding: [0, 1])
100
+ puts table(headers, rows)
90
101
  end
91
102
 
92
103
  def personas(personas)
@@ -102,25 +113,26 @@ module LanguageOperator
102
113
  ]
103
114
  end
104
115
 
105
- table = TTY::Table.new(headers, rows)
106
- puts table.render(:unicode, padding: [0, 1])
116
+ puts table(headers, rows)
107
117
  end
108
118
 
109
119
  def models(models)
110
120
  return ProgressFormatter.info('No models found') if models.empty?
111
121
 
112
- headers = %w[NAME PROVIDER MODEL STATUS]
122
+ headers = ['', 'NAME', 'NAMESPACE', 'STATUS', 'PROVIDER/MODEL']
113
123
  rows = models.map do |model|
124
+ provider_model = "#{model[:provider]}/#{model[:model]}"
125
+
114
126
  [
127
+ StatusFormatter.dot(model[:status]),
115
128
  model[:name],
116
- model[:provider],
117
- model[:model],
118
- StatusFormatter.format(model[:status])
129
+ model[:namespace],
130
+ model[:status] || 'Unknown',
131
+ provider_model
119
132
  ]
120
133
  end
121
134
 
122
- table = TTY::Table.new(headers, rows)
123
- puts table.render(:unicode, padding: [0, 1])
135
+ puts table(headers, rows)
124
136
  end
125
137
 
126
138
  def status_dashboard(cluster_summary, current_cluster: nil)
@@ -140,8 +152,7 @@ module LanguageOperator
140
152
  ]
141
153
  end
142
154
 
143
- table = TTY::Table.new(headers, rows)
144
- puts table.render(:unicode, padding: [0, 1])
155
+ puts table(headers, rows)
145
156
 
146
157
  return unless current_cluster
147
158
 
@@ -42,6 +42,42 @@ module LanguageOperator
42
42
  end
43
43
  end
44
44
 
45
+ # Format time elapsed since past time (opposite of time_until)
46
+ #
47
+ # @param past_time [Time] Past timestamp
48
+ # @return [String] Formatted string like "5m ago" or "2h 15m ago"
49
+ #
50
+ # @example
51
+ # ValueFormatter.time_ago(Time.now - 300) # => "5m ago"
52
+ def self.time_ago(past_time)
53
+ diff = Time.now - past_time
54
+
55
+ if diff < 0
56
+ 'in the future' # Edge case for clock skew
57
+ elsif diff < SECONDS_PER_MINUTE
58
+ "#{diff.to_i}s ago"
59
+ elsif diff < SECONDS_PER_HOUR
60
+ minutes = (diff / SECONDS_PER_MINUTE).to_i
61
+ "#{minutes}m ago"
62
+ elsif diff < SECONDS_PER_DAY
63
+ hours = (diff / SECONDS_PER_HOUR).to_i
64
+ minutes = ((diff % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE).to_i
65
+ if minutes > 0
66
+ "#{hours}h #{minutes}m ago"
67
+ else
68
+ "#{hours}h ago"
69
+ end
70
+ else
71
+ days = (diff / SECONDS_PER_DAY).to_i
72
+ hours = ((diff % SECONDS_PER_DAY) / SECONDS_PER_HOUR).to_i
73
+ if hours > 0
74
+ "#{days}d #{hours}h ago"
75
+ else
76
+ "#{days}d ago"
77
+ end
78
+ end
79
+ end
80
+
45
81
  # Format a duration in seconds
46
82
  #
47
83
  # @param seconds [Numeric] Duration in seconds
@@ -107,6 +143,45 @@ module LanguageOperator
107
143
  time.strftime('%Y-%m-%d %H:%M')
108
144
  end
109
145
  end
146
+
147
+ # Format Time object as HH:MM:SS for logs
148
+ #
149
+ # @param time [Time] The time to format
150
+ # @return [String] Formatted string like "14:30:25"
151
+ #
152
+ # @example
153
+ # ValueFormatter.log_time(Time.now) # => "14:30:25"
154
+ def self.log_time(time)
155
+ time.strftime('%H:%M:%S')
156
+ end
157
+
158
+ # Parse timestamp string and format as HH:MM:SS
159
+ #
160
+ # @param timestamp_str [String] ISO or text format timestamp
161
+ # @return [String] Formatted time or empty string on parse error
162
+ #
163
+ # @example
164
+ # ValueFormatter.parse_and_format_time("2025-01-15T14:30:25Z") # => "14:30:25"
165
+ def self.parse_and_format_time(timestamp_str)
166
+ return '' unless timestamp_str
167
+
168
+ time = Time.parse(timestamp_str)
169
+ log_time(time)
170
+ rescue StandardError
171
+ ''
172
+ end
173
+
174
+ # Format time components as HH:MM for schedules
175
+ #
176
+ # @param hours [Integer] Hours (0-23)
177
+ # @param minutes [Integer] Minutes (0-59)
178
+ # @return [String] Formatted string like "14:30"
179
+ #
180
+ # @example
181
+ # ValueFormatter.schedule_time(14, 30) # => "14:30"
182
+ def self.schedule_time(hours, minutes)
183
+ format('%<hours>02d:%<minutes>02d', hours: hours, minutes: minutes)
184
+ end
110
185
  end
111
186
  end
112
187
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'shellwords'
3
4
  require_relative 'cluster_validator'
4
5
 
5
6
  module LanguageOperator
@@ -41,12 +42,13 @@ module LanguageOperator
41
42
  end
42
43
 
43
44
  # Build kubectl command args for this cluster context
45
+ # All user-controlled inputs are properly escaped to prevent command injection
44
46
  # @return [Hash] kubectl arguments
45
47
  def kubectl_args
46
48
  {
47
- kubeconfig: config[:kubeconfig] ? "--kubeconfig=#{config[:kubeconfig]}" : '',
48
- context: config[:context] ? "--context=#{config[:context]}" : '',
49
- namespace: "-n #{namespace}"
49
+ kubeconfig: config[:kubeconfig] ? "--kubeconfig=#{Shellwords.escape(config[:kubeconfig])}" : '',
50
+ context: config[:context] ? "--context=#{Shellwords.escape(config[:context])}" : '',
51
+ namespace: "-n #{Shellwords.escape(namespace)}"
50
52
  }
51
53
  end
52
54
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../formatters/progress_formatter'
4
4
  require_relative '../../kubernetes/client'
5
+ require_relative '../../utils/secure_path'
5
6
 
6
7
  module LanguageOperator
7
8
  module CLI
@@ -59,7 +60,7 @@ module LanguageOperator
59
60
  private
60
61
 
61
62
  def default_kubeconfig_path
62
- File.expand_path('~/.kube/config')
63
+ LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config')
63
64
  end
64
65
 
65
66
  def kubeconfig_missing_message(path)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../constants/kubernetes_labels'
4
+
5
+ module LanguageOperator
6
+ module CLI
7
+ module Helpers
8
+ # Utilities for working with Kubernetes labels
9
+ module LabelUtils
10
+ # Normalize an agent name for use as a Kubernetes label value
11
+ #
12
+ # Kubernetes label values must follow DNS-1123 subdomain format:
13
+ # - contain only lowercase alphanumeric characters, '-' or '.'
14
+ # - start and end with an alphanumeric character
15
+ # - be at most 63 characters
16
+ #
17
+ # @param agent_name [String] The agent name to normalize
18
+ # @return [String] A valid label value
19
+ def self.normalize_agent_name(agent_name)
20
+ # Agent names should already be valid Kubernetes resource names,
21
+ # which are compatible with label values. Just ensure lowercase.
22
+ agent_name.to_s.downcase
23
+ end
24
+
25
+ # Build a label selector for finding agent pods
26
+ #
27
+ # @param agent_name [String] The agent name
28
+ # @return [String] A label selector string
29
+ def self.agent_pod_selector(agent_name)
30
+ normalized_name = normalize_agent_name(agent_name)
31
+ Constants::KubernetesLabels.agent_selector(normalized_name)
32
+ end
33
+
34
+ # Validate that an agent name will work as a label value
35
+ #
36
+ # @param agent_name [String] The agent name to validate
37
+ # @return [Boolean] true if valid, false otherwise
38
+ def self.valid_label_value?(agent_name)
39
+ return false if agent_name.nil? || agent_name.empty?
40
+
41
+ # Check original string first (before normalization)
42
+ agent_str = agent_name.to_s
43
+
44
+ # Check DNS-1123 subdomain requirements:
45
+ # - 63 characters or less
46
+ # - lowercase letters, numbers, hyphens, and dots only
47
+ # - start and end with alphanumeric character
48
+ return false if agent_str.length > 63
49
+ return false unless agent_str.match?(/\A[a-z0-9]([a-z0-9\-.]*[a-z0-9])?\z/)
50
+
51
+ true
52
+ end
53
+
54
+ # Get debugging information about label selector matching
55
+ #
56
+ # @param ctx [ClusterContext] Kubernetes context
57
+ # @param agent_name [String] Agent name being searched
58
+ # @return [Hash] Debug information about the search
59
+ def self.debug_pod_search(ctx, agent_name)
60
+ selector = agent_pod_selector(agent_name)
61
+
62
+ {
63
+ agent_name: agent_name,
64
+ normalized_name: normalize_agent_name(agent_name),
65
+ label_selector: selector,
66
+ namespace: ctx.namespace,
67
+ valid_label_value: valid_label_value?(agent_name)
68
+ }
69
+ end
70
+
71
+ # Convert deployment labels to pod selector and find matching pods
72
+ #
73
+ # This method handles the common pattern of extracting selector labels from a deployment,
74
+ # converting them to a label selector string, and finding matching pods.
75
+ #
76
+ # @param ctx [ClusterContext] Kubernetes context with client and namespace
77
+ # @param deployment_name [String] Name of the deployment (for error messages)
78
+ # @param labels [Hash, Object] Deployment selector labels (may be K8s::Resource or Hash)
79
+ # @return [Array] Array of pod resources matching the labels
80
+ # @raise [RuntimeError] If labels are nil, empty, or no pods found
81
+ def self.find_pods_by_deployment_labels(ctx, deployment_name, labels)
82
+ raise "Deployment '#{deployment_name}' has no selector labels" if labels.nil?
83
+
84
+ # Convert to hash if needed (K8s API may return K8s::Resource objects)
85
+ labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
86
+ raise "Deployment '#{deployment_name}' has empty selector labels" if labels_hash.empty?
87
+
88
+ # Convert labels to Kubernetes label selector format
89
+ label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
90
+
91
+ # Find matching pods
92
+ ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -5,23 +5,21 @@ require 'json'
5
5
  require 'uri'
6
6
 
7
7
  module LanguageOperator
8
- module Ux
9
- module Concerns
10
- # Mixin for common LLM provider operations
8
+ module CLI
9
+ module Helpers
10
+ # LLM provider operations for CLI wizards
11
11
  #
12
- # Provides helpers for testing provider connections, fetching available models,
13
- # and handling provider-specific configuration.
12
+ # Provides helpers for testing provider connections and fetching available models.
14
13
  #
15
14
  # @example
16
- # class MyFlow < Base
17
- # include Concerns::ProviderHelpers
15
+ # class ModelWizard
16
+ # include Helpers::ProviderHelper
18
17
  #
19
- # def setup
20
- # result = test_provider_connection(:anthropic, api_key: 'sk-...')
21
- # models = fetch_provider_models(:openai, api_key: 'sk-...')
18
+ # def test_anthropic_connection(api_key)
19
+ # test_provider_connection(:anthropic, api_key: api_key)
22
20
  # end
23
21
  # end
24
- module ProviderHelpers
22
+ module ProviderHelper
25
23
  # Test connection to an LLM provider
26
24
  #
27
25
  # @param provider [Symbol] Provider type (:anthropic, :openai, :openai_compatible)
@@ -65,27 +63,10 @@ module LanguageOperator
65
63
  fetch_openai_compatible_models(endpoint, api_key)
66
64
  end
67
65
  rescue StandardError => e
68
- CLI::Formatters::ProgressFormatter.warn("Could not fetch models: #{e.message}")
66
+ Formatters::ProgressFormatter.warn("Could not fetch models: #{e.message}")
69
67
  nil
70
68
  end
71
69
 
72
- # Get provider display information
73
- #
74
- # @param provider [Symbol] Provider type
75
- # @return [Hash] Hash with :name, :docs_url keys
76
- def provider_info(provider)
77
- case provider
78
- when :anthropic
79
- { name: 'Anthropic', docs_url: 'https://console.anthropic.com' }
80
- when :openai
81
- { name: 'OpenAI', docs_url: 'https://platform.openai.com/api-keys' }
82
- when :openai_compatible
83
- { name: 'OpenAI-Compatible', docs_url: nil }
84
- else
85
- { name: provider.to_s.capitalize, docs_url: nil }
86
- end
87
- end
88
-
89
70
  private
90
71
 
91
72
  def test_anthropic(api_key)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../formatters/value_formatter'
4
+
3
5
  module LanguageOperator
4
6
  module CLI
5
7
  module Helpers
@@ -45,6 +47,8 @@ module LanguageOperator
45
47
 
46
48
  # Build a cron expression for interval-based execution
47
49
  def interval_cron(interval, unit)
50
+ validate_interval(interval, unit)
51
+
48
52
  case unit.downcase
49
53
  when 'minutes', 'minute'
50
54
  "*/#{interval} * * * *"
@@ -99,7 +103,23 @@ module LanguageOperator
99
103
  end
100
104
 
101
105
  def format_time(hours, minutes)
102
- format('%<hours>02d:%<minutes>02d', hours: hours, minutes: minutes)
106
+ Formatters::ValueFormatter.schedule_time(hours, minutes)
107
+ end
108
+
109
+ def validate_interval(interval, unit)
110
+ raise ArgumentError, "Interval must be a positive integer, got: #{interval}" unless interval.is_a?(Integer) && interval.positive?
111
+
112
+ case unit.downcase
113
+ when 'minutes', 'minute'
114
+ raise ArgumentError, "Minutes interval must be between 1-59, got: #{interval}" if interval >= 60
115
+ when 'hours', 'hour'
116
+ raise ArgumentError, "Hours interval must be between 1-23, got: #{interval}" if interval >= 24
117
+ when 'days', 'day'
118
+ raise ArgumentError, "Days interval must be between 1-31, got: #{interval}" if interval >= 32
119
+ else
120
+ # Will be caught by the existing case statement
121
+ nil
122
+ end
103
123
  end
104
124
  end
105
125
  end
@@ -49,18 +49,26 @@ module LanguageOperator
49
49
  # @param options [Array<String>] Available options
50
50
  # @return [String] Selected option
51
51
  def self.select(prompt, options)
52
- puts prompt
53
- options.each_with_index do |option, index|
54
- puts " #{index + 1}. #{option}"
55
- end
56
- print "\nSelect (1-#{options.length}): "
52
+ loop do
53
+ puts prompt
54
+ options.each_with_index do |option, index|
55
+ puts " #{index + 1}. #{option}"
56
+ end
57
+ print "\nSelect (1-#{options.length}): "
58
+
59
+ input = $stdin.gets&.chomp || ''
60
+
61
+ # Allow user to quit/cancel
62
+ if input.downcase.match?(/^q(uit)?$/)
63
+ puts 'Selection cancelled'
64
+ exit 0
65
+ end
66
+
67
+ selection = input.to_i
68
+ return options[selection - 1] if selection.between?(1, options.length)
57
69
 
58
- selection = $stdin.gets&.chomp.to_i
59
- if selection.between?(1, options.length)
60
- options[selection - 1]
61
- else
62
- puts 'Invalid selection'
63
- exit 1
70
+ puts 'Invalid selection. Please try again.'
71
+ puts
64
72
  end
65
73
  end
66
74
  end