language-operator 0.0.1 → 0.1.31

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +88 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +82 -0
  8. data/README.md +3 -11
  9. data/Rakefile +63 -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/SCHEMA_VERSION.md +250 -0
  16. data/docs/dsl/agent-reference.md +604 -0
  17. data/docs/dsl/best-practices.md +1078 -0
  18. data/docs/dsl/chat-endpoints.md +895 -0
  19. data/docs/dsl/constraints.md +671 -0
  20. data/docs/dsl/mcp-integration.md +1177 -0
  21. data/docs/dsl/webhooks.md +932 -0
  22. data/docs/dsl/workflows.md +744 -0
  23. data/lib/language_operator/agent/base.rb +110 -0
  24. data/lib/language_operator/agent/executor.rb +440 -0
  25. data/lib/language_operator/agent/instrumentation.rb +54 -0
  26. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  27. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  28. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  29. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  30. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  31. data/lib/language_operator/agent/safety/manager.rb +207 -0
  32. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  33. data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
  34. data/lib/language_operator/agent/scheduler.rb +183 -0
  35. data/lib/language_operator/agent/telemetry.rb +116 -0
  36. data/lib/language_operator/agent/web_server.rb +610 -0
  37. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  38. data/lib/language_operator/agent.rb +149 -0
  39. data/lib/language_operator/cli/commands/agent.rb +1205 -0
  40. data/lib/language_operator/cli/commands/cluster.rb +371 -0
  41. data/lib/language_operator/cli/commands/install.rb +404 -0
  42. data/lib/language_operator/cli/commands/model.rb +266 -0
  43. data/lib/language_operator/cli/commands/persona.rb +393 -0
  44. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  45. data/lib/language_operator/cli/commands/status.rb +143 -0
  46. data/lib/language_operator/cli/commands/system.rb +772 -0
  47. data/lib/language_operator/cli/commands/tool.rb +537 -0
  48. data/lib/language_operator/cli/commands/use.rb +47 -0
  49. data/lib/language_operator/cli/errors/handler.rb +180 -0
  50. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  51. data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
  52. data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
  53. data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
  54. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  55. data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
  56. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  57. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  58. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  59. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  60. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  61. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  62. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  63. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  64. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  65. data/lib/language_operator/cli/main.rb +236 -0
  66. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  67. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  68. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  69. data/lib/language_operator/client/base.rb +214 -0
  70. data/lib/language_operator/client/config.rb +136 -0
  71. data/lib/language_operator/client/cost_calculator.rb +37 -0
  72. data/lib/language_operator/client/mcp_connector.rb +123 -0
  73. data/lib/language_operator/client.rb +19 -0
  74. data/lib/language_operator/config/cluster_config.rb +101 -0
  75. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  76. data/lib/language_operator/config/tool_registry.rb +96 -0
  77. data/lib/language_operator/config.rb +138 -0
  78. data/lib/language_operator/dsl/adapter.rb +124 -0
  79. data/lib/language_operator/dsl/agent_context.rb +90 -0
  80. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  81. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  82. data/lib/language_operator/dsl/config.rb +119 -0
  83. data/lib/language_operator/dsl/context.rb +50 -0
  84. data/lib/language_operator/dsl/execution_context.rb +47 -0
  85. data/lib/language_operator/dsl/helpers.rb +109 -0
  86. data/lib/language_operator/dsl/http.rb +184 -0
  87. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  88. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  89. data/lib/language_operator/dsl/registry.rb +36 -0
  90. data/lib/language_operator/dsl/schema.rb +1102 -0
  91. data/lib/language_operator/dsl/shell.rb +125 -0
  92. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  93. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  94. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  95. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  96. data/lib/language_operator/dsl.rb +161 -0
  97. data/lib/language_operator/errors.rb +60 -0
  98. data/lib/language_operator/kubernetes/client.rb +279 -0
  99. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  100. data/lib/language_operator/loggable.rb +47 -0
  101. data/lib/language_operator/logger.rb +141 -0
  102. data/lib/language_operator/retry.rb +123 -0
  103. data/lib/language_operator/retryable.rb +132 -0
  104. data/lib/language_operator/templates/README.md +23 -0
  105. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
  106. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  107. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  108. data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
  109. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  110. data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
  111. data/lib/language_operator/tool_loader.rb +242 -0
  112. data/lib/language_operator/validators.rb +170 -0
  113. data/lib/language_operator/version.rb +1 -1
  114. data/lib/language_operator.rb +65 -3
  115. data/requirements/tasks/challenge.md +9 -0
  116. data/requirements/tasks/iterate.md +36 -0
  117. data/requirements/tasks/optimize.md +21 -0
  118. data/requirements/tasks/tag.md +5 -0
  119. data/test_agent_dsl.rb +108 -0
  120. metadata +507 -20
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cluster_validator'
4
+
5
+ module LanguageOperator
6
+ module CLI
7
+ module Helpers
8
+ # Encapsulates cluster context (name, config, client) to reduce boilerplate
9
+ # in command implementations.
10
+ #
11
+ # Instead of repeating:
12
+ # cluster = ClusterValidator.get_cluster(options[:cluster])
13
+ # cluster_config = ClusterValidator.get_cluster_config(cluster)
14
+ # k8s = ClusterValidator.kubernetes_client(options[:cluster])
15
+ #
16
+ # Use:
17
+ # ctx = ClusterContext.from_options(options)
18
+ # # Access: ctx.name, ctx.config, ctx.client, ctx.namespace
19
+ class ClusterContext
20
+ attr_reader :name, :config, :client, :namespace
21
+
22
+ # Create ClusterContext from command options hash
23
+ # @param options [Hash] Thor command options (expects :cluster key)
24
+ # @return [ClusterContext] Initialized context
25
+ def self.from_options(options)
26
+ name = ClusterValidator.get_cluster(options[:cluster])
27
+ config = ClusterValidator.get_cluster_config(name)
28
+ client = ClusterValidator.kubernetes_client(options[:cluster])
29
+ new(name, config, client)
30
+ end
31
+
32
+ # Initialize with cluster details
33
+ # @param name [String] Cluster name
34
+ # @param config [Hash] Cluster configuration
35
+ # @param client [LanguageOperator::Kubernetes::Client] K8s client
36
+ def initialize(name, config, client)
37
+ @name = name
38
+ @config = config
39
+ @client = client
40
+ @namespace = config[:namespace]
41
+ end
42
+
43
+ # Build kubectl command args for this cluster context
44
+ # @return [Hash] kubectl arguments
45
+ def kubectl_args
46
+ {
47
+ kubeconfig: config[:kubeconfig] ? "--kubeconfig=#{config[:kubeconfig]}" : '',
48
+ context: config[:context] ? "--context=#{config[:context]}" : '',
49
+ namespace: "-n #{namespace}"
50
+ }
51
+ end
52
+
53
+ # Build kubectl command prefix string
54
+ # @return [String] kubectl command prefix
55
+ def kubectl_prefix
56
+ args = kubectl_args
57
+ "kubectl #{args[:kubeconfig]} #{args[:context]} #{args[:namespace]}".strip.squeeze(' ')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../formatters/progress_formatter'
4
+ require_relative '../errors/handler'
5
+ require_relative 'kubeconfig_validator'
6
+ require_relative '../../config/cluster_config'
7
+
8
+ module LanguageOperator
9
+ module CLI
10
+ module Helpers
11
+ # Validates that a cluster is selected before executing commands
12
+ module ClusterValidator
13
+ class << self
14
+ # Ensure a cluster is selected, exit with helpful message if not
15
+ def ensure_cluster_selected!
16
+ return current_cluster if current_cluster
17
+
18
+ Errors::Handler.handle_no_cluster_selected
19
+ end
20
+
21
+ # Get current cluster, or allow override via --cluster flag
22
+ def get_cluster(cluster_override = nil)
23
+ if cluster_override
24
+ validate_cluster_exists!(cluster_override)
25
+ cluster_override
26
+ else
27
+ ensure_cluster_selected!
28
+ end
29
+ end
30
+
31
+ # Validate that a specific cluster exists
32
+ def validate_cluster_exists!(name)
33
+ return if Config::ClusterConfig.cluster_exists?(name)
34
+
35
+ # Build context with available clusters for fuzzy matching
36
+ clusters = Config::ClusterConfig.list_clusters
37
+ available_names = clusters.map { |c| c[:name] }
38
+
39
+ # Use error handler with fuzzy matching
40
+ error = K8s::Error::NotFound.new(404, 'Not Found', 'cluster')
41
+ Errors::Handler.handle_not_found(error,
42
+ resource_type: 'cluster',
43
+ resource_name: name,
44
+ available_resources: available_names)
45
+ end
46
+
47
+ # Get current cluster name
48
+ def current_cluster
49
+ Config::ClusterConfig.current_cluster
50
+ end
51
+
52
+ # Get current cluster config
53
+ def current_cluster_config
54
+ cluster_name = ensure_cluster_selected!
55
+ Config::ClusterConfig.get_cluster(cluster_name)
56
+ end
57
+
58
+ # Get cluster config by name (with validation)
59
+ def get_cluster_config(name)
60
+ validate_cluster_exists!(name)
61
+ config = Config::ClusterConfig.get_cluster(name)
62
+
63
+ # Validate kubeconfig exists and is accessible
64
+ validate_kubeconfig!(config)
65
+
66
+ config
67
+ end
68
+
69
+ # Validate kubeconfig for the given cluster config
70
+ def validate_kubeconfig!(cluster_config)
71
+ # Check if kubeconfig file exists
72
+ kubeconfig_path = cluster_config[:kubeconfig]
73
+ return if kubeconfig_path && File.exist?(kubeconfig_path)
74
+
75
+ Formatters::ProgressFormatter.error("Kubeconfig not found: #{kubeconfig_path}")
76
+ puts
77
+ puts 'The kubeconfig file for this cluster does not exist.'
78
+ puts
79
+ puts 'To fix this issue:'
80
+ puts ' 1. Verify the kubeconfig path in ~/.aictl/config.yaml'
81
+ puts ' 2. Re-create the cluster configuration:'
82
+ puts " aictl cluster create #{cluster_config[:name]}"
83
+ exit 1
84
+ end
85
+
86
+ # Create a Kubernetes client for the given cluster
87
+ def kubernetes_client(cluster_override = nil)
88
+ cluster = get_cluster(cluster_override)
89
+ cluster_config = get_cluster_config(cluster)
90
+
91
+ require_relative '../../kubernetes/client'
92
+ Kubernetes::Client.new(
93
+ kubeconfig: cluster_config[:kubeconfig],
94
+ context: cluster_config[:context]
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module LanguageOperator
6
+ module CLI
7
+ module Helpers
8
+ # Helper module for editing content in the user's preferred editor.
9
+ # Handles tempfile creation, cleanup, and editor invocation.
10
+ module EditorHelper
11
+ # Edit content in the user's preferred editor
12
+ #
13
+ # @param content [String] Content to edit
14
+ # @param filename_prefix [String] Prefix for the temp file name
15
+ # @param extension [String] File extension (default: '.txt')
16
+ # @param default_editor [String] Editor to use if $EDITOR not set (default: 'vi')
17
+ # @return [String] The edited content
18
+ # @raise [RuntimeError] If editor command fails
19
+ #
20
+ # @example Edit agent instructions
21
+ # new_content = EditorHelper.edit_content(
22
+ # current_instructions,
23
+ # 'agent-instructions-',
24
+ # '.txt'
25
+ # )
26
+ #
27
+ # @example Edit YAML configuration
28
+ # new_yaml = EditorHelper.edit_content(
29
+ # model.to_yaml,
30
+ # 'model-',
31
+ # '.yaml',
32
+ # default_editor: 'vim'
33
+ # )
34
+ def self.edit_content(content, filename_prefix, extension = '.txt', default_editor: 'vi')
35
+ editor = ENV['EDITOR'] || default_editor
36
+ tempfile = Tempfile.new([filename_prefix, extension])
37
+
38
+ begin
39
+ # Write content and flush to ensure it's on disk
40
+ tempfile.write(content)
41
+ tempfile.flush
42
+ tempfile.close
43
+
44
+ # Open in editor
45
+ success = system("#{editor} #{tempfile.path}")
46
+ raise "Editor command failed: #{editor}" unless success
47
+
48
+ # Read edited content
49
+ File.read(tempfile.path)
50
+ ensure
51
+ # Clean up temp file
52
+ tempfile.unlink
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../formatters/progress_formatter'
4
+ require_relative '../../kubernetes/client'
5
+
6
+ module LanguageOperator
7
+ module CLI
8
+ module Helpers
9
+ # Validates kubeconfig and cluster connectivity
10
+ class KubeconfigValidator
11
+ class << self
12
+ # Validate kubeconfig exists and cluster is accessible
13
+ # Returns [valid, error_message]
14
+ def validate
15
+ # Check if kubeconfig file exists
16
+ kubeconfig_path = detect_kubeconfig
17
+ return [false, kubeconfig_missing_message(kubeconfig_path)] unless kubeconfig_path && File.exist?(kubeconfig_path)
18
+
19
+ # Try to connect to cluster
20
+ begin
21
+ k8s = Kubernetes::Client.new(kubeconfig: kubeconfig_path)
22
+
23
+ # Test connectivity by listing namespaces
24
+ k8s.client.api('v1').resource('namespaces').list
25
+
26
+ # Check if operator is installed
27
+ return [false, operator_missing_message] unless k8s.operator_installed?
28
+
29
+ [true, nil]
30
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
31
+ [false, connection_failed_message(e)]
32
+ rescue K8s::Error::Unauthorized => e
33
+ [false, auth_failed_message(e)]
34
+ rescue StandardError => e
35
+ [false, generic_error_message(e)]
36
+ end
37
+ end
38
+
39
+ # Validate and exit with error if invalid
40
+ def validate!
41
+ valid, error_message = validate
42
+ return if valid
43
+
44
+ Formatters::ProgressFormatter.error(error_message)
45
+ exit 1
46
+ end
47
+
48
+ # Detect kubeconfig path
49
+ def detect_kubeconfig
50
+ ENV.fetch('KUBECONFIG', nil) || default_kubeconfig_path
51
+ end
52
+
53
+ # Check if kubeconfig exists
54
+ def kubeconfig_exists?
55
+ path = detect_kubeconfig
56
+ path && File.exist?(path)
57
+ end
58
+
59
+ private
60
+
61
+ def default_kubeconfig_path
62
+ File.expand_path('~/.kube/config')
63
+ end
64
+
65
+ def kubeconfig_missing_message(path)
66
+ <<~MSG
67
+ Kubeconfig file not found
68
+
69
+ Expected location: #{path || default_kubeconfig_path}
70
+
71
+ To fix this issue:
72
+ 1. Ensure you have a Kubernetes cluster configured
73
+ 2. Set KUBECONFIG environment variable to point to your kubeconfig file:
74
+ export KUBECONFIG=/path/to/your/kubeconfig
75
+
76
+ Or place your kubeconfig at: ~/.kube/config
77
+
78
+ For local development, you can use:
79
+ - kind (https://kind.sigs.k8s.io/)
80
+ - k3d (https://k3d.io/)
81
+ - minikube (https://minikube.sigs.k8s.io/)
82
+ MSG
83
+ end
84
+
85
+ def connection_failed_message(error)
86
+ <<~MSG
87
+ Failed to connect to Kubernetes cluster
88
+
89
+ Error: #{error.message}
90
+
91
+ To fix this issue:
92
+ 1. Check if your cluster is running:
93
+ kubectl cluster-info
94
+
95
+ 2. Verify your kubeconfig is correct:
96
+ kubectl config view
97
+
98
+ 3. Check your cluster context:
99
+ kubectl config current-context
100
+
101
+ 4. Test basic connectivity:
102
+ kubectl get namespaces
103
+ MSG
104
+ end
105
+
106
+ def auth_failed_message(error)
107
+ <<~MSG
108
+ Kubernetes authentication failed
109
+
110
+ Error: #{error.message}
111
+
112
+ To fix this issue:
113
+ 1. Verify your credentials are valid:
114
+ kubectl config view
115
+
116
+ 2. Check if your authentication token/certificate is expired
117
+
118
+ 3. Re-authenticate with your cluster provider
119
+
120
+ 4. Test authentication:
121
+ kubectl get namespaces
122
+ MSG
123
+ end
124
+
125
+ def operator_missing_message
126
+ <<~MSG
127
+ Language Operator is not installed in the cluster
128
+
129
+ The Language Operator CRDs were not found in the cluster.
130
+
131
+ To install the operator:
132
+
133
+ 1. Using aictl (recommended):
134
+ aictl install
135
+
136
+ 2. Or manually with Helm:
137
+ helm repo add language-operator https://language-operator.github.io/charts
138
+ helm repo update
139
+ helm install language-operator language-operator/language-operator
140
+
141
+ 3. Verify installation:
142
+ kubectl get deployment -n language-operator-system language-operator
143
+
144
+ For more information, visit: https://github.com/language-operator/language-operator
145
+ MSG
146
+ end
147
+
148
+ def generic_error_message(error)
149
+ <<~MSG
150
+ Unexpected error validating cluster connection
151
+
152
+ Error: #{error.class}: #{error.message}
153
+
154
+ Please check:
155
+ 1. Your kubeconfig file is valid
156
+ 2. Your cluster is accessible
157
+ 3. You have appropriate permissions
158
+
159
+ For debugging, run with DEBUG=1:
160
+ DEBUG=1 aictl <command>
161
+ MSG
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module LanguageOperator
6
+ module CLI
7
+ module Helpers
8
+ # Shared module providing Pastel color functionality
9
+ # to CLI commands, formatters, and helpers.
10
+ #
11
+ # Usage:
12
+ # include PastelHelper
13
+ # puts pastel.green("Success!")
14
+ module PastelHelper
15
+ # Returns a memoized Pastel instance for colorizing terminal output
16
+ #
17
+ # @return [Pastel] Pastel instance
18
+ def pastel
19
+ @pastel ||= Pastel.new
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ module CLI
5
+ module Helpers
6
+ # Helper module for checking which agents depend on specific resources
7
+ # (tools, models, personas). Used by CLI commands to warn before deletion.
8
+ module ResourceDependencyChecker
9
+ # Find agents that use a specific tool
10
+ #
11
+ # @param agents [Array<Hash>] Array of agent resources from kubectl
12
+ # @param tool_name [String] Name of the tool to check
13
+ # @return [Array<Hash>] Agents that reference this tool
14
+ def self.agents_using_tool(agents, tool_name)
15
+ agents.select do |agent|
16
+ agent_tools = agent.dig('spec', 'tools') || []
17
+ agent_tools.include?(tool_name)
18
+ end
19
+ end
20
+
21
+ # Find agents that use a specific language model
22
+ #
23
+ # @param agents [Array<Hash>] Array of agent resources from kubectl
24
+ # @param model_name [String] Name of the model to check
25
+ # @return [Array<Hash>] Agents that reference this model
26
+ def self.agents_using_model(agents, model_name)
27
+ agents.select do |agent|
28
+ agent_model_refs = agent.dig('spec', 'modelRefs') || []
29
+ agent_models = agent_model_refs.map { |ref| ref['name'] }
30
+ agent_models.include?(model_name)
31
+ end
32
+ end
33
+
34
+ # Find agents that use a specific persona
35
+ #
36
+ # @param agents [Array<Hash>] Array of agent resources from kubectl
37
+ # @param persona_name [String] Name of the persona to check
38
+ # @return [Array<Hash>] Agents that reference this persona
39
+ def self.agents_using_persona(agents, persona_name)
40
+ agents.select do |agent|
41
+ agent.dig('spec', 'persona') == persona_name
42
+ end
43
+ end
44
+
45
+ # Count how many agents use a specific tool
46
+ #
47
+ # @param agents [Array<Hash>] Array of agent resources
48
+ # @param tool_name [String] Name of the tool
49
+ # @return [Integer] Count of agents using this tool
50
+ def self.tool_usage_count(agents, tool_name)
51
+ agents_using_tool(agents, tool_name).size
52
+ end
53
+
54
+ # Count how many agents use a specific model
55
+ #
56
+ # @param agents [Array<Hash>] Array of agent resources
57
+ # @param model_name [String] Name of the model
58
+ # @return [Integer] Count of agents using this model
59
+ def self.model_usage_count(agents, model_name)
60
+ agents_using_model(agents, model_name).size
61
+ end
62
+
63
+ # Count how many agents use a specific persona
64
+ #
65
+ # @param agents [Array<Hash>] Array of agent resources
66
+ # @param persona_name [String] Name of the persona
67
+ # @return [Integer] Count of agents using this persona
68
+ def self.persona_usage_count(agents, persona_name)
69
+ agents_using_persona(agents, persona_name).size
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ module CLI
5
+ module Helpers
6
+ # Helper for building schedules from natural language inputs
7
+ class ScheduleBuilder
8
+ class << self
9
+ # Parse natural language time input and return 24-hour format
10
+ # Examples: "4pm" -> "16:00", "9:30am" -> "09:30", "16:00" -> "16:00"
11
+ def parse_time(input)
12
+ input = input.strip.downcase
13
+
14
+ # Handle 24-hour format (e.g., "16:00", "9:30")
15
+ if input.match?(/^\d{1,2}:\d{2}$/)
16
+ hours, minutes = input.split(':').map(&:to_i)
17
+ return format_time(hours, minutes) if valid_time?(hours, minutes)
18
+
19
+ raise ArgumentError, "Invalid time: #{input}"
20
+ end
21
+
22
+ # Handle 12-hour format with am/pm (e.g., "4pm", "9:30am")
23
+ match = input.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/i)
24
+ raise ArgumentError, "Invalid time format: #{input}" unless match
25
+
26
+ hours = match[1].to_i
27
+ minutes = match[2].to_i
28
+ period = match[3].downcase
29
+
30
+ # Convert to 24-hour format
31
+ hours = 0 if hours == 12 && period == 'am'
32
+ hours = 12 if hours == 12 && period == 'pm'
33
+ hours += 12 if period == 'pm' && hours != 12
34
+
35
+ raise ArgumentError, "Invalid time: #{input}" unless valid_time?(hours, minutes)
36
+
37
+ format_time(hours, minutes)
38
+ end
39
+
40
+ # Build a cron expression for daily execution at a specific time
41
+ def daily_cron(time_string)
42
+ hours, minutes = time_string.split(':').map(&:to_i)
43
+ "#{minutes} #{hours} * * *"
44
+ end
45
+
46
+ # Build a cron expression for interval-based execution
47
+ def interval_cron(interval, unit)
48
+ case unit.downcase
49
+ when 'minutes', 'minute'
50
+ "*/#{interval} * * * *"
51
+ when 'hours', 'hour'
52
+ "0 */#{interval} * * *"
53
+ when 'days', 'day'
54
+ "0 0 */#{interval} * *"
55
+ else
56
+ raise ArgumentError, "Invalid unit: #{unit}"
57
+ end
58
+ end
59
+
60
+ # Convert cron expression to human-readable format
61
+ def cron_to_human(cron_expr)
62
+ parts = cron_expr.split
63
+ return cron_expr if parts.length != 5
64
+
65
+ minute, hour, day, month, weekday = parts
66
+
67
+ # Daily at specific time
68
+ if minute =~ /^\d+$/ && hour =~ /^\d+$/ && day == '*' && month == '*' && weekday == '*'
69
+ time_str = format_time(hour.to_i, minute.to_i)
70
+ return "Daily at #{time_str}"
71
+ end
72
+
73
+ # Every N minutes
74
+ if minute.start_with?('*/') && hour == '*'
75
+ interval = minute[2..].to_i
76
+ return "Every #{interval} minute#{'s' if interval > 1}"
77
+ end
78
+
79
+ # Every N hours
80
+ if minute == '0' && hour.start_with?('*/')
81
+ interval = hour[2..].to_i
82
+ return "Every #{interval} hour#{'s' if interval > 1}"
83
+ end
84
+
85
+ # Every N days
86
+ if minute == '0' && hour == '0' && day.start_with?('*/')
87
+ interval = day[2..].to_i
88
+ return "Every #{interval} day#{'s' if interval > 1}"
89
+ end
90
+
91
+ # Fallback to cron expression
92
+ cron_expr
93
+ end
94
+
95
+ private
96
+
97
+ def valid_time?(hours, minutes)
98
+ hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60
99
+ end
100
+
101
+ def format_time(hours, minutes)
102
+ format('%<hours>02d:%<minutes>02d', hours: hours, minutes: minutes)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ module CLI
5
+ module Helpers
6
+ # Helper module for user confirmation prompts and interactive input.
7
+ # Consolidates the repeated confirmation pattern used throughout commands.
8
+ module UserPrompts
9
+ # Ask user for confirmation
10
+ # @param message [String] The confirmation message
11
+ # @param force [Boolean] Skip prompt if true (default: false)
12
+ # @return [Boolean] true if confirmed, false otherwise
13
+ # rubocop:disable Naming/PredicateMethod
14
+ def self.confirm(message, force: false)
15
+ return true if force
16
+
17
+ print "#{message} (y/N): "
18
+ response = $stdin.gets&.chomp || ''
19
+ puts
20
+ response.downcase == 'y'
21
+ end
22
+ # rubocop:enable Naming/PredicateMethod
23
+
24
+ # Ask user for confirmation and exit if not confirmed
25
+ # @param message [String] The confirmation message
26
+ # @param force [Boolean] Skip prompt if true (default: false)
27
+ # @param cancel_message [String] Message to display on cancellation
28
+ # @return [void] Returns if confirmed, exits otherwise
29
+ def self.confirm!(message, force: false, cancel_message: 'Operation cancelled')
30
+ return if confirm(message, force: force)
31
+
32
+ puts cancel_message
33
+ exit 0
34
+ end
35
+
36
+ # Ask user for text input
37
+ # @param prompt [String] The prompt message
38
+ # @param default [String, nil] Default value if user enters nothing
39
+ # @return [String] User input
40
+ def self.ask(prompt, default: nil)
41
+ prompt_text = default ? "#{prompt} [#{default}]" : prompt
42
+ print "#{prompt_text}: "
43
+ response = $stdin.gets&.chomp || ''
44
+ response.empty? && default ? default : response
45
+ end
46
+
47
+ # Ask user to select from options
48
+ # @param prompt [String] The prompt message
49
+ # @param options [Array<String>] Available options
50
+ # @return [String] Selected option
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}): "
57
+
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
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end