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,236 @@
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 'commands/system'
15
+ require_relative 'formatters/progress_formatter'
16
+ require_relative '../config/cluster_config'
17
+ require_relative '../kubernetes/client'
18
+
19
+ module LanguageOperator
20
+ module CLI
21
+ # Main CLI class for aictl command
22
+ #
23
+ # Provides commands for creating, running, and managing language-operator resources.
24
+ class Main < Thor
25
+ def self.exit_on_failure?
26
+ true
27
+ end
28
+
29
+ desc 'status', 'Show system status and overview'
30
+ def status
31
+ Commands::Status.new.invoke(:overview)
32
+ end
33
+
34
+ desc 'version', 'Show aictl and operator version'
35
+ def version
36
+ puts "aictl v#{LanguageOperator::VERSION}"
37
+ puts
38
+
39
+ # Try to get operator version from current cluster
40
+ current_cluster = Config::ClusterConfig.current_cluster
41
+ if current_cluster
42
+ cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
43
+ begin
44
+ k8s = Kubernetes::Client.new(
45
+ kubeconfig: cluster_config[:kubeconfig],
46
+ context: cluster_config[:context]
47
+ )
48
+
49
+ if k8s.operator_installed?
50
+ operator_version = k8s.operator_version || 'unknown'
51
+ puts "Operator: v#{operator_version}"
52
+ puts "Cluster: #{current_cluster}"
53
+
54
+ # Check compatibility (simple version check)
55
+ # In the future, this could be more sophisticated
56
+ puts
57
+ Formatters::ProgressFormatter.success('Versions are compatible')
58
+ else
59
+ Formatters::ProgressFormatter.warn("Operator not installed in cluster '#{current_cluster}'")
60
+ end
61
+ rescue StandardError => e
62
+ Formatters::ProgressFormatter.error("Could not connect to cluster: #{e.message}")
63
+ end
64
+ else
65
+ puts 'No cluster selected'
66
+ puts
67
+ puts 'Select a cluster to check operator version:'
68
+ puts ' aictl use <cluster>'
69
+ end
70
+ end
71
+
72
+ desc 'cluster SUBCOMMAND ...ARGS', 'Manage language clusters'
73
+ subcommand 'cluster', Commands::Cluster
74
+
75
+ desc 'use CLUSTER', 'Switch to a different cluster context'
76
+ def use(cluster_name)
77
+ Commands::Use.new.switch(cluster_name)
78
+ end
79
+
80
+ desc 'agent SUBCOMMAND ...ARGS', 'Manage autonomous agents'
81
+ subcommand 'agent', Commands::Agent
82
+
83
+ desc 'persona SUBCOMMAND ...ARGS', 'Manage agent personas'
84
+ subcommand 'persona', Commands::Persona
85
+
86
+ desc 'tool SUBCOMMAND ...ARGS', 'Manage MCP tools'
87
+ subcommand 'tool', Commands::Tool
88
+
89
+ desc 'model SUBCOMMAND ...ARGS', 'Manage language models'
90
+ subcommand 'model', Commands::Model
91
+
92
+ desc 'system SUBCOMMAND ...ARGS', 'System commands for schema and metadata'
93
+ subcommand 'system', Commands::System
94
+
95
+ desc 'quickstart', 'Interactive setup wizard for first-time users'
96
+ def quickstart
97
+ Commands::Quickstart.new.invoke(:start)
98
+ end
99
+
100
+ desc 'install', 'Install the language-operator using Helm'
101
+ long_desc Commands::Install.long_desc_for(:install)
102
+ option :values, type: :string, desc: 'Path to custom Helm values file'
103
+ option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
104
+ option :version, type: :string, desc: 'Specific chart version to install'
105
+ option :dry_run, type: :boolean, default: false, desc: 'Preview installation without applying'
106
+ option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
107
+ option :create_namespace, type: :boolean, default: true, desc: 'Create namespace if it does not exist'
108
+ def install
109
+ Commands::Install.new([], options).install
110
+ end
111
+
112
+ desc 'upgrade', 'Upgrade the language-operator using Helm'
113
+ long_desc Commands::Install.long_desc_for(:upgrade)
114
+ option :values, type: :string, desc: 'Path to custom Helm values file'
115
+ option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
116
+ option :version, type: :string, desc: 'Specific chart version to upgrade to'
117
+ option :dry_run, type: :boolean, default: false, desc: 'Preview upgrade without applying'
118
+ option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
119
+ def upgrade
120
+ Commands::Install.new([], options).upgrade
121
+ end
122
+
123
+ desc 'uninstall', 'Uninstall the language-operator using Helm'
124
+ long_desc Commands::Install.long_desc_for(:uninstall)
125
+ option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
126
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation prompt'
127
+ def uninstall
128
+ Commands::Install.new([], options).uninstall
129
+ end
130
+
131
+ desc 'completion SHELL', 'Install shell completion for aictl (bash, zsh, fish)'
132
+ long_desc <<-DESC
133
+ Install shell completion for aictl. Supports bash, zsh, and fish.
134
+
135
+ Examples:
136
+ aictl completion bash
137
+ aictl completion zsh
138
+ aictl completion fish
139
+
140
+ Manual installation:
141
+ bash: Add to ~/.bashrc:
142
+ source <(aictl completion bash --stdout)
143
+
144
+ zsh: Add to ~/.zshrc:
145
+ source <(aictl completion zsh --stdout)
146
+
147
+ fish: Run once:
148
+ aictl completion fish | source
149
+ DESC
150
+ option :stdout, type: :boolean, desc: 'Print completion script to stdout instead of installing'
151
+ def completion(shell)
152
+ case shell.downcase
153
+ when 'bash'
154
+ install_bash_completion
155
+ when 'zsh'
156
+ install_zsh_completion
157
+ when 'fish'
158
+ install_fish_completion
159
+ else
160
+ Formatters::ProgressFormatter.error("Unsupported shell: #{shell}")
161
+ puts
162
+ puts 'Supported shells: bash, zsh, fish'
163
+ exit 1
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def install_bash_completion
170
+ completion_file = File.expand_path('../../completions/aictl.bash', __dir__)
171
+
172
+ if options[:stdout]
173
+ puts File.read(completion_file)
174
+ return
175
+ end
176
+
177
+ target = File.expand_path('~/.bash_completion.d/aictl')
178
+ FileUtils.mkdir_p(File.dirname(target))
179
+ FileUtils.cp(completion_file, target)
180
+
181
+ Formatters::ProgressFormatter.success('Bash completion installed')
182
+ puts
183
+ puts 'Add to your ~/.bashrc:'
184
+ puts ' [ -f ~/.bash_completion.d/aictl ] && source ~/.bash_completion.d/aictl'
185
+ puts
186
+ puts 'Then reload your shell:'
187
+ puts ' source ~/.bashrc'
188
+ end
189
+
190
+ def install_zsh_completion
191
+ completion_file = File.expand_path('../../completions/_aictl', __dir__)
192
+
193
+ if options[:stdout]
194
+ puts File.read(completion_file)
195
+ return
196
+ end
197
+
198
+ # Check if user has a custom fpath directory
199
+ fpath_dir = File.expand_path('~/.zsh/completions')
200
+ FileUtils.mkdir_p(fpath_dir)
201
+
202
+ target = File.join(fpath_dir, '_aictl')
203
+ FileUtils.cp(completion_file, target)
204
+
205
+ Formatters::ProgressFormatter.success('Zsh completion installed')
206
+ puts
207
+ puts 'Add to your ~/.zshrc (before compinit):'
208
+ puts ' fpath=(~/.zsh/completions $fpath)'
209
+ puts ' autoload -Uz compinit && compinit'
210
+ puts
211
+ puts 'Then reload your shell:'
212
+ puts ' source ~/.zshrc'
213
+ end
214
+
215
+ def install_fish_completion
216
+ completion_file = File.expand_path('../../completions/aictl.fish', __dir__)
217
+
218
+ if options[:stdout]
219
+ puts File.read(completion_file)
220
+ return
221
+ end
222
+
223
+ target = File.expand_path('~/.config/fish/completions/aictl.fish')
224
+ FileUtils.mkdir_p(File.dirname(target))
225
+ FileUtils.cp(completion_file, target)
226
+
227
+ Formatters::ProgressFormatter.success('Fish completion installed')
228
+ puts
229
+ puts 'Reload completions:'
230
+ puts ' fish_update_completions'
231
+ puts
232
+ puts 'Or restart your fish shell'
233
+ end
234
+ end
235
+ end
236
+ 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