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,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'fileutils'
5
+ require_relative 'commands/cluster'
6
+ require_relative 'commands/use'
7
+ require_relative 'commands/agent'
8
+ require_relative 'commands/status'
9
+ require_relative 'commands/persona'
10
+ require_relative 'commands/tool'
11
+ require_relative 'commands/model'
12
+ require_relative 'commands/quickstart'
13
+ require_relative 'commands/install'
14
+ require_relative 'formatters/progress_formatter'
15
+ require_relative '../config/cluster_config'
16
+ require_relative '../kubernetes/client'
17
+
18
+ module LanguageOperator
19
+ module CLI
20
+ # Main CLI class for aictl command
21
+ #
22
+ # Provides commands for creating, running, and managing language-operator resources.
23
+ class Main < Thor
24
+ def self.exit_on_failure?
25
+ true
26
+ end
27
+
28
+ desc 'status', 'Show system status and overview'
29
+ def status
30
+ Commands::Status.new.invoke(:overview)
31
+ end
32
+
33
+ desc 'version', 'Show aictl and operator version'
34
+ def version
35
+ puts "aictl v#{LanguageOperator::VERSION}"
36
+ puts
37
+
38
+ # Try to get operator version from current cluster
39
+ current_cluster = Config::ClusterConfig.current_cluster
40
+ if current_cluster
41
+ cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
42
+ begin
43
+ k8s = Kubernetes::Client.new(
44
+ kubeconfig: cluster_config[:kubeconfig],
45
+ context: cluster_config[:context]
46
+ )
47
+
48
+ if k8s.operator_installed?
49
+ operator_version = k8s.operator_version || 'unknown'
50
+ puts "Operator: v#{operator_version}"
51
+ puts "Cluster: #{current_cluster}"
52
+
53
+ # Check compatibility (simple version check)
54
+ # In the future, this could be more sophisticated
55
+ puts
56
+ Formatters::ProgressFormatter.success('Versions are compatible')
57
+ else
58
+ Formatters::ProgressFormatter.warn("Operator not installed in cluster '#{current_cluster}'")
59
+ end
60
+ rescue StandardError => e
61
+ Formatters::ProgressFormatter.error("Could not connect to cluster: #{e.message}")
62
+ end
63
+ else
64
+ puts 'No cluster selected'
65
+ puts
66
+ puts 'Select a cluster to check operator version:'
67
+ puts ' aictl use <cluster>'
68
+ end
69
+ end
70
+
71
+ desc 'cluster SUBCOMMAND ...ARGS', 'Manage language clusters'
72
+ subcommand 'cluster', Commands::Cluster
73
+
74
+ desc 'use CLUSTER', 'Switch to a different cluster context'
75
+ def use(cluster_name)
76
+ Commands::Use.new.switch(cluster_name)
77
+ end
78
+
79
+ desc 'agent SUBCOMMAND ...ARGS', 'Manage autonomous agents'
80
+ subcommand 'agent', Commands::Agent
81
+
82
+ desc 'persona SUBCOMMAND ...ARGS', 'Manage agent personas'
83
+ subcommand 'persona', Commands::Persona
84
+
85
+ desc 'tool SUBCOMMAND ...ARGS', 'Manage MCP tools'
86
+ subcommand 'tool', Commands::Tool
87
+
88
+ desc 'model SUBCOMMAND ...ARGS', 'Manage language models'
89
+ subcommand 'model', Commands::Model
90
+
91
+ desc 'quickstart', 'Interactive setup wizard for first-time users'
92
+ def quickstart
93
+ Commands::Quickstart.new.invoke(:start)
94
+ end
95
+
96
+ desc 'install', 'Install the language-operator using Helm'
97
+ long_desc Commands::Install.long_desc_for(:install)
98
+ option :values, type: :string, desc: 'Path to custom Helm values file'
99
+ option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
100
+ option :version, type: :string, desc: 'Specific chart version to install'
101
+ option :dry_run, type: :boolean, default: false, desc: 'Preview installation without applying'
102
+ option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
103
+ option :create_namespace, type: :boolean, default: true, desc: 'Create namespace if it does not exist'
104
+ def install
105
+ Commands::Install.new([], options).install
106
+ end
107
+
108
+ desc 'upgrade', 'Upgrade the language-operator using Helm'
109
+ long_desc Commands::Install.long_desc_for(:upgrade)
110
+ option :values, type: :string, desc: 'Path to custom Helm values file'
111
+ option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
112
+ option :version, type: :string, desc: 'Specific chart version to upgrade to'
113
+ option :dry_run, type: :boolean, default: false, desc: 'Preview upgrade without applying'
114
+ option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
115
+ def upgrade
116
+ Commands::Install.new([], options).upgrade
117
+ end
118
+
119
+ desc 'uninstall', 'Uninstall the language-operator using Helm'
120
+ long_desc Commands::Install.long_desc_for(:uninstall)
121
+ option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
122
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation prompt'
123
+ def uninstall
124
+ Commands::Install.new([], options).uninstall
125
+ end
126
+
127
+ desc 'completion SHELL', 'Install shell completion for aictl (bash, zsh, fish)'
128
+ long_desc <<-DESC
129
+ Install shell completion for aictl. Supports bash, zsh, and fish.
130
+
131
+ Examples:
132
+ aictl completion bash
133
+ aictl completion zsh
134
+ aictl completion fish
135
+
136
+ Manual installation:
137
+ bash: Add to ~/.bashrc:
138
+ source <(aictl completion bash --stdout)
139
+
140
+ zsh: Add to ~/.zshrc:
141
+ source <(aictl completion zsh --stdout)
142
+
143
+ fish: Run once:
144
+ aictl completion fish | source
145
+ DESC
146
+ option :stdout, type: :boolean, desc: 'Print completion script to stdout instead of installing'
147
+ def completion(shell)
148
+ case shell.downcase
149
+ when 'bash'
150
+ install_bash_completion
151
+ when 'zsh'
152
+ install_zsh_completion
153
+ when 'fish'
154
+ install_fish_completion
155
+ else
156
+ Formatters::ProgressFormatter.error("Unsupported shell: #{shell}")
157
+ puts
158
+ puts 'Supported shells: bash, zsh, fish'
159
+ exit 1
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def install_bash_completion
166
+ completion_file = File.expand_path('../../completions/aictl.bash', __dir__)
167
+
168
+ if options[:stdout]
169
+ puts File.read(completion_file)
170
+ return
171
+ end
172
+
173
+ target = File.expand_path('~/.bash_completion.d/aictl')
174
+ FileUtils.mkdir_p(File.dirname(target))
175
+ FileUtils.cp(completion_file, target)
176
+
177
+ Formatters::ProgressFormatter.success('Bash completion installed')
178
+ puts
179
+ puts 'Add to your ~/.bashrc:'
180
+ puts ' [ -f ~/.bash_completion.d/aictl ] && source ~/.bash_completion.d/aictl'
181
+ puts
182
+ puts 'Then reload your shell:'
183
+ puts ' source ~/.bashrc'
184
+ end
185
+
186
+ def install_zsh_completion
187
+ completion_file = File.expand_path('../../completions/_aictl', __dir__)
188
+
189
+ if options[:stdout]
190
+ puts File.read(completion_file)
191
+ return
192
+ end
193
+
194
+ # Check if user has a custom fpath directory
195
+ fpath_dir = File.expand_path('~/.zsh/completions')
196
+ FileUtils.mkdir_p(fpath_dir)
197
+
198
+ target = File.join(fpath_dir, '_aictl')
199
+ FileUtils.cp(completion_file, target)
200
+
201
+ Formatters::ProgressFormatter.success('Zsh completion installed')
202
+ puts
203
+ puts 'Add to your ~/.zshrc (before compinit):'
204
+ puts ' fpath=(~/.zsh/completions $fpath)'
205
+ puts ' autoload -Uz compinit && compinit'
206
+ puts
207
+ puts 'Then reload your shell:'
208
+ puts ' source ~/.zshrc'
209
+ end
210
+
211
+ def install_fish_completion
212
+ completion_file = File.expand_path('../../completions/aictl.fish', __dir__)
213
+
214
+ if options[:stdout]
215
+ puts File.read(completion_file)
216
+ return
217
+ end
218
+
219
+ target = File.expand_path('~/.config/fish/completions/aictl.fish')
220
+ FileUtils.mkdir_p(File.dirname(target))
221
+ FileUtils.cp(completion_file, target)
222
+
223
+ Formatters::ProgressFormatter.success('Fish completion installed')
224
+ puts
225
+ puts 'Reload completions:'
226
+ puts ' fish_update_completions'
227
+ puts
228
+ puts 'Or restart your fish shell'
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,66 @@
1
+ apiVersion: langop.io/v1alpha1
2
+ kind: LanguageTool
3
+ metadata:
4
+ name: <%= name %>
5
+ namespace: <%= namespace %>
6
+ spec:
7
+ type: <%= type || 'mcp' %>
8
+ image: <%= image || "changeme/#{name}-tool:latest" %>
9
+ imagePullPolicy: Always
10
+ deploymentMode: <%= deployment_mode || 'sidecar' %>
11
+ port: <%= port || 8080 %>
12
+
13
+ resources:
14
+ requests:
15
+ cpu: 50m
16
+ memory: 64Mi
17
+ limits:
18
+ cpu: 200m
19
+ memory: 256Mi
20
+
21
+ <% if egress && !egress.empty? -%>
22
+ egress:
23
+ <% egress.each do |rule| -%>
24
+ - description: <%= rule['description'] %>
25
+ <% if rule['cidr'] -%>
26
+ cidr: <%= rule['cidr'] %>
27
+ <% end -%>
28
+ <% if rule['dns'] -%>
29
+ dns:
30
+ <% rule['dns'].each do |dns| -%>
31
+ - "<%= dns %>"
32
+ <% end -%>
33
+ <% end -%>
34
+ <% if rule['ports'] -%>
35
+ ports:
36
+ <% rule['ports'].each do |port_rule| -%>
37
+ - port: <%= port_rule['port'] %>
38
+ protocol: <%= port_rule['protocol'] %>
39
+ <% end -%>
40
+ <% end -%>
41
+ <% end -%>
42
+ <% else -%>
43
+ egress: []
44
+ <% end -%>
45
+
46
+ <% if rbac -%>
47
+ rbac:
48
+ <% if rbac['clusterRole'] -%>
49
+ clusterRole:
50
+ rules:
51
+ <% rbac['clusterRole']['rules'].each do |rule| -%>
52
+ - apiGroups:
53
+ <% rule['apiGroups'].each do |group| -%>
54
+ - <%= group %>
55
+ <% end -%>
56
+ resources:
57
+ <% rule['resources'].each do |resource| -%>
58
+ - <%= resource %>
59
+ <% end -%>
60
+ verbs:
61
+ <% rule['verbs'].each do |verb| -%>
62
+ - <%= verb %>
63
+ <% end -%>
64
+ <% end -%>
65
+ <% end -%>
66
+ <% end -%>
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require 'pastel'
5
+ require_relative '../helpers/schedule_builder'
6
+
7
+ module LanguageOperator
8
+ module CLI
9
+ module Wizards
10
+ # Interactive wizard for creating agents
11
+ class AgentWizard
12
+ attr_reader :prompt, :pastel
13
+
14
+ def initialize
15
+ @prompt = TTY::Prompt.new
16
+ @pastel = Pastel.new
17
+ end
18
+
19
+ # Run the wizard and return the generated description
20
+ def run
21
+ show_welcome
22
+
23
+ # Step 1: Get task description
24
+ task = ask_task_description
25
+
26
+ # Step 2: Determine schedule
27
+ schedule_info = ask_schedule
28
+
29
+ # Step 3: Tool detection and configuration
30
+ tools_config = configure_tools(task)
31
+
32
+ # Step 4: Preview and confirm
33
+ description = build_description(task, schedule_info, tools_config)
34
+
35
+ show_preview(description, schedule_info, tools_config)
36
+
37
+ return nil unless confirm_creation?
38
+
39
+ description
40
+ end
41
+
42
+ private
43
+
44
+ def show_welcome
45
+ puts
46
+ puts pastel.cyan('╭────────────────────────────────────────╮')
47
+ puts pastel.cyan('│ Let\'s create your agent! 🤖 │')
48
+ puts pastel.cyan('╰────────────────────────────────────────╯')
49
+ puts
50
+ end
51
+
52
+ def ask_task_description
53
+ prompt.ask('What should your agent do?') do |q|
54
+ q.required true
55
+ q.validate(/\w+/)
56
+ q.messages[:valid?] = 'Please describe a task (cannot be empty)'
57
+ end
58
+ end
59
+
60
+ def ask_schedule
61
+ puts
62
+ schedule_type = prompt.select('How often should it run?') do |menu|
63
+ menu.choice 'Every day at a specific time', :daily
64
+ menu.choice 'Every few minutes/hours', :interval
65
+ menu.choice 'Continuously (whenever something changes)', :continuous
66
+ menu.choice 'Only when I trigger it manually', :manual
67
+ end
68
+
69
+ case schedule_type
70
+ when :daily
71
+ ask_daily_schedule
72
+ when :interval
73
+ ask_interval_schedule
74
+ when :continuous
75
+ { type: :continuous, description: 'continuously' }
76
+ when :manual
77
+ { type: :manual, description: 'on manual trigger' }
78
+ end
79
+ end
80
+
81
+ def ask_daily_schedule
82
+ puts
83
+ time_input = prompt.ask('What time each day? (e.g., 4pm, 9:30am, 16:00):') do |q|
84
+ q.required true
85
+ q.validate(lambda do |input|
86
+ Helpers::ScheduleBuilder.parse_time(input)
87
+ true
88
+ rescue ArgumentError
89
+ false
90
+ end)
91
+ q.messages[:valid?] = 'Invalid time format. Try: 4pm, 9:30am, or 16:00'
92
+ end
93
+
94
+ time_24h = Helpers::ScheduleBuilder.parse_time(time_input)
95
+ cron = Helpers::ScheduleBuilder.daily_cron(time_24h)
96
+
97
+ {
98
+ type: :daily,
99
+ time: time_24h,
100
+ cron: cron,
101
+ description: "daily at #{time_input}"
102
+ }
103
+ end
104
+
105
+ def ask_interval_schedule
106
+ puts
107
+ interval = prompt.ask('How often?', convert: :int) do |q|
108
+ q.required true
109
+ q.validate(->(v) { v.to_i.positive? })
110
+ q.messages[:valid?] = 'Please enter a positive number'
111
+ end
112
+
113
+ unit = prompt.select('Minutes, hours, or days?', %w[minutes hours days])
114
+
115
+ cron = Helpers::ScheduleBuilder.interval_cron(interval, unit)
116
+
117
+ {
118
+ type: :interval,
119
+ interval: interval,
120
+ unit: unit,
121
+ cron: cron,
122
+ description: "every #{interval} #{unit}"
123
+ }
124
+ end
125
+
126
+ def configure_tools(task_description)
127
+ detected = detect_tools(task_description)
128
+ config = {}
129
+
130
+ return config if detected.empty?
131
+
132
+ puts
133
+ puts "I detected these tools: #{pastel.yellow(detected.join(', '))}"
134
+ puts
135
+
136
+ detected.each do |tool|
137
+ case tool
138
+ when 'email'
139
+ config[:email] = ask_email_config
140
+ when 'google-sheets', 'spreadsheet'
141
+ config[:spreadsheet] = ask_spreadsheet_config
142
+ when 'slack'
143
+ config[:slack] = ask_slack_config
144
+ end
145
+ end
146
+
147
+ config[:tools] = detected
148
+ config
149
+ end
150
+
151
+ def detect_tools(description)
152
+ tools = []
153
+ text = description.downcase
154
+
155
+ tools << 'email' if text.match?(/email|mail|send.*message/i)
156
+ tools << 'google-sheets' if text.match?(/spreadsheet|sheet|excel|csv/i)
157
+ tools << 'slack' if text.match?(/slack/i)
158
+ tools << 'github' if text.match?(/github|git|repo/i)
159
+
160
+ tools
161
+ end
162
+
163
+ def ask_email_config
164
+ email = prompt.ask('Your email for notifications:') do |q|
165
+ q.required true
166
+ q.validate(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
167
+ q.messages[:valid?] = 'Please enter a valid email address'
168
+ end
169
+ { address: email }
170
+ end
171
+
172
+ def ask_spreadsheet_config
173
+ url = prompt.ask('Spreadsheet URL:') do |q|
174
+ q.required true
175
+ q.validate(%r{^https?://}i)
176
+ q.messages[:valid?] = 'Please enter a valid URL'
177
+ end
178
+ { url: url }
179
+ end
180
+
181
+ def ask_slack_config
182
+ channel = prompt.ask('Slack channel (e.g., #general):') do |q|
183
+ q.required true
184
+ end
185
+ { channel: channel }
186
+ end
187
+
188
+ def build_description(task, schedule_info, tools_config)
189
+ parts = [task]
190
+
191
+ # Add schedule information
192
+ case schedule_info[:type]
193
+ when :daily
194
+ parts << schedule_info[:description]
195
+ when :interval
196
+ parts << schedule_info[:description]
197
+ when :continuous
198
+ # "continuously" might already be implied in task
199
+ parts << 'continuously' unless task.downcase.include?('continuous')
200
+ when :manual
201
+ # Manual trigger doesn't need to be in description
202
+ end
203
+
204
+ # Add tool-specific details
205
+ parts << "and email me at #{tools_config[:email][:address]}" if tools_config[:email]
206
+
207
+ if tools_config[:spreadsheet] && !task.include?('http')
208
+ # Replace generic "spreadsheet" with specific URL if not already present
209
+ parts << "using spreadsheet at #{tools_config[:spreadsheet][:url]}"
210
+ end
211
+
212
+ parts << "and send results to #{tools_config[:slack][:channel]}" if tools_config[:slack]
213
+
214
+ parts.join(' ')
215
+ end
216
+
217
+ def show_preview(description, schedule_info, tools_config)
218
+ puts
219
+ puts pastel.cyan('╭─ Preview ──────────────────────────────╮')
220
+ puts '│'
221
+ puts "│ #{pastel.bold('Task:')} #{description}"
222
+
223
+ if schedule_info[:type] == :manual
224
+ puts "│ #{pastel.bold('Mode:')} Manual trigger"
225
+ else
226
+ schedule_text = schedule_info[:description] || 'on demand'
227
+ puts "│ #{pastel.bold('Schedule:')} #{schedule_text}"
228
+ end
229
+
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?
233
+
234
+ puts '│'
235
+ puts pastel.cyan('╰────────────────────────────────────────╯')
236
+ puts
237
+ end
238
+
239
+ def confirm_creation?
240
+ puts
241
+ prompt.yes?('Create this agent?')
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end