language-operator 0.1.30 → 0.1.35

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +49 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +28 -7
  7. data/Rakefile +29 -0
  8. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  9. data/docs/dsl/agent-reference.md +13 -0
  10. data/lib/language_operator/agent/base.rb +10 -6
  11. data/lib/language_operator/agent/executor.rb +19 -97
  12. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  13. data/lib/language_operator/agent/safety/safe_executor.rb +39 -2
  14. data/lib/language_operator/agent/scheduler.rb +60 -0
  15. data/lib/language_operator/agent/task_executor.rb +548 -0
  16. data/lib/language_operator/agent.rb +90 -27
  17. data/lib/language_operator/cli/base_command.rb +117 -0
  18. data/lib/language_operator/cli/commands/agent.rb +351 -466
  19. data/lib/language_operator/cli/commands/cluster.rb +276 -256
  20. data/lib/language_operator/cli/commands/install.rb +110 -119
  21. data/lib/language_operator/cli/commands/model.rb +284 -184
  22. data/lib/language_operator/cli/commands/persona.rb +220 -289
  23. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  24. data/lib/language_operator/cli/commands/status.rb +36 -53
  25. data/lib/language_operator/cli/commands/system.rb +760 -0
  26. data/lib/language_operator/cli/commands/tool.rb +356 -422
  27. data/lib/language_operator/cli/commands/use.rb +19 -22
  28. data/lib/language_operator/cli/formatters/code_formatter.rb +3 -7
  29. data/lib/language_operator/cli/formatters/log_formatter.rb +3 -5
  30. data/lib/language_operator/cli/formatters/progress_formatter.rb +3 -7
  31. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  32. data/lib/language_operator/cli/formatters/table_formatter.rb +10 -26
  33. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  34. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  35. data/lib/language_operator/cli/main.rb +4 -0
  36. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  37. data/lib/language_operator/client/config.rb +20 -21
  38. data/lib/language_operator/config.rb +115 -3
  39. data/lib/language_operator/constants.rb +54 -0
  40. data/lib/language_operator/dsl/agent_context.rb +7 -7
  41. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  42. data/lib/language_operator/dsl/config.rb +30 -66
  43. data/lib/language_operator/dsl/main_definition.rb +114 -0
  44. data/lib/language_operator/dsl/schema.rb +1143 -0
  45. data/lib/language_operator/dsl/task_definition.rb +315 -0
  46. data/lib/language_operator/dsl.rb +1 -1
  47. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  48. data/lib/language_operator/logger.rb +4 -4
  49. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  50. data/lib/language_operator/templates/README.md +23 -0
  51. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +133 -0
  52. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  53. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  54. data/lib/language_operator/templates/schema/CHANGELOG.md +119 -0
  55. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  56. data/lib/language_operator/templates/schema/agent_dsl_schema.json +494 -0
  57. data/lib/language_operator/type_coercion.rb +250 -0
  58. data/lib/language_operator/ux/base.rb +81 -0
  59. data/lib/language_operator/ux/concerns/README.md +155 -0
  60. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  61. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  62. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  63. data/lib/language_operator/ux/create_agent.rb +252 -0
  64. data/lib/language_operator/ux/create_model.rb +267 -0
  65. data/lib/language_operator/ux/quickstart.rb +594 -0
  66. data/lib/language_operator/version.rb +1 -1
  67. data/lib/language_operator.rb +2 -0
  68. data/requirements/ARCHITECTURE.md +1 -0
  69. data/requirements/SCRATCH.md +153 -0
  70. data/requirements/dsl.md +0 -0
  71. data/requirements/features +1 -0
  72. data/requirements/personas +1 -0
  73. data/requirements/proposals +1 -0
  74. data/requirements/tasks/iterate.md +14 -15
  75. data/requirements/tasks/optimize.md +13 -4
  76. data/synth/001/Makefile +90 -0
  77. data/synth/001/agent.rb +26 -0
  78. data/synth/001/agent.yaml +7 -0
  79. data/synth/001/output.log +44 -0
  80. data/synth/Makefile +39 -0
  81. data/synth/README.md +342 -0
  82. metadata +49 -18
  83. data/examples/README.md +0 -569
  84. data/examples/agent_example.rb +0 -86
  85. data/examples/chat_endpoint_agent.rb +0 -118
  86. data/examples/github_webhook_agent.rb +0 -171
  87. data/examples/mcp_agent.rb +0 -158
  88. data/examples/oauth_callback_agent.rb +0 -296
  89. data/examples/stripe_webhook_agent.rb +0 -219
  90. data/examples/webhook_agent.rb +0 -80
  91. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  92. data/test_agent_dsl.rb +0 -108
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thor'
4
3
  require 'yaml'
5
- require 'pastel'
4
+ require_relative '../base_command'
6
5
  require_relative '../formatters/progress_formatter'
7
6
  require_relative '../formatters/table_formatter'
8
7
  require_relative '../helpers/cluster_validator'
9
- require_relative '../helpers/cluster_context'
10
8
  require_relative '../helpers/user_prompts'
11
9
  require_relative '../helpers/resource_dependency_checker'
12
10
  require_relative '../helpers/editor_helper'
11
+ require_relative '../helpers/pastel_helper'
13
12
  require_relative '../../config/cluster_config'
14
13
  require_relative '../../kubernetes/client'
15
14
  require_relative '../../kubernetes/resource_builder'
@@ -18,112 +17,96 @@ module LanguageOperator
18
17
  module CLI
19
18
  module Commands
20
19
  # Persona management commands
21
- class Persona < Thor
20
+ class Persona < BaseCommand
22
21
  include Helpers::ClusterValidator
22
+ include Helpers::PastelHelper
23
23
 
24
24
  desc 'list', 'List all personas in current cluster'
25
25
  option :cluster, type: :string, desc: 'Override current cluster context'
26
26
  def list
27
- ctx = Helpers::ClusterContext.from_options(options)
27
+ handle_command_error('list personas') do
28
+ personas = list_resources_or_empty('LanguagePersona') do
29
+ puts
30
+ puts 'Personas define the personality and capabilities of agents.'
31
+ puts
32
+ puts 'Create a persona with:'
33
+ puts ' aictl persona create <name>'
34
+ end
28
35
 
29
- personas = ctx.client.list_resources('LanguagePersona', namespace: ctx.namespace)
36
+ return if personas.empty?
30
37
 
31
- if personas.empty?
32
- Formatters::ProgressFormatter.info("No personas found in cluster '#{ctx.name}'")
33
- puts
34
- puts 'Personas define the personality and capabilities of agents.'
35
- puts
36
- puts 'Create a persona with:'
37
- puts ' aictl persona create <name>'
38
- return
39
- end
38
+ # Get agents to count usage
39
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
40
40
 
41
- # Get agents to count usage
42
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
41
+ table_data = personas.map do |persona|
42
+ name = persona.dig('metadata', 'name')
43
+ used_by = agents.count { |a| a.dig('spec', 'persona') == name }
43
44
 
44
- table_data = personas.map do |persona|
45
- name = persona.dig('metadata', 'name')
46
- used_by = agents.count { |a| a.dig('spec', 'persona') == name }
45
+ {
46
+ name: name,
47
+ tone: persona.dig('spec', 'tone') || 'neutral',
48
+ used_by: used_by,
49
+ description: persona.dig('spec', 'description') || ''
50
+ }
51
+ end
47
52
 
48
- {
49
- name: name,
50
- tone: persona.dig('spec', 'tone') || 'neutral',
51
- used_by: used_by,
52
- description: persona.dig('spec', 'description') || ''
53
- }
53
+ Formatters::TableFormatter.personas(table_data)
54
54
  end
55
-
56
- Formatters::TableFormatter.personas(table_data)
57
- rescue StandardError => e
58
- Formatters::ProgressFormatter.error("Failed to list personas: #{e.message}")
59
- raise if ENV['DEBUG']
60
-
61
- exit 1
62
55
  end
63
56
 
64
57
  desc 'show NAME', 'Display full persona details'
65
58
  option :cluster, type: :string, desc: 'Override current cluster context'
66
59
  def show(name)
67
- ctx = Helpers::ClusterContext.from_options(options)
60
+ handle_command_error('show persona') do
61
+ persona = get_resource_or_exit('LanguagePersona', name)
68
62
 
69
- begin
70
- persona = ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
71
- rescue K8s::Error::NotFound
72
- Formatters::ProgressFormatter.error("Persona '#{name}' not found in cluster '#{ctx.name}'")
73
- exit 1
74
- end
63
+ puts
64
+ puts "Persona: #{pastel.cyan.bold(name)}"
65
+ puts '═' * 80
66
+ puts
75
67
 
76
- puts
77
- puts "Persona: #{pastel.cyan.bold(name)}"
78
- puts '' * 80
79
- puts
80
-
81
- # Format and display key persona details
82
- spec = persona['spec'] || {}
83
- puts "#{pastel.bold('Display Name:')} #{spec['displayName']}"
84
- puts "#{pastel.bold('Tone:')} #{pastel.yellow(spec['tone'])}" if spec['tone']
85
- puts
86
- puts pastel.bold('Description:')
87
- puts " #{spec['description']}"
88
- puts
89
-
90
- if spec['systemPrompt']
91
- puts pastel.bold('System Prompt:')
92
- puts " #{spec['systemPrompt']}"
68
+ # Format and display key persona details
69
+ spec = persona['spec'] || {}
70
+ puts "#{pastel.bold('Display Name:')} #{spec['displayName']}"
71
+ puts "#{pastel.bold('Tone:')} #{pastel.yellow(spec['tone'])}" if spec['tone']
72
+ puts
73
+ puts pastel.bold('Description:')
74
+ puts " #{spec['description']}"
93
75
  puts
94
- end
95
76
 
96
- if spec['capabilities']&.any?
97
- puts pastel.bold('Capabilities:')
98
- spec['capabilities'].each do |cap|
99
- puts " #{pastel.green('•')} #{cap}"
77
+ if spec['systemPrompt']
78
+ puts pastel.bold('System Prompt:')
79
+ puts " #{spec['systemPrompt']}"
80
+ puts
100
81
  end
101
- puts
102
- end
103
82
 
104
- if spec['toolPreferences']&.any?
105
- puts pastel.bold('Tool Preferences:')
106
- spec['toolPreferences'].each do |pref|
107
- puts " #{pastel.green('•')} #{pref}"
83
+ if spec['capabilities']&.any?
84
+ puts pastel.bold('Capabilities:')
85
+ spec['capabilities'].each do |cap|
86
+ puts " #{pastel.green('•')} #{cap}"
87
+ end
88
+ puts
108
89
  end
109
- puts
110
- end
111
90
 
112
- if spec['responseFormat']
113
- puts "#{pastel.bold('Response Format:')} #{spec['responseFormat']}"
114
- puts
115
- end
91
+ if spec['toolPreferences']&.any?
92
+ puts pastel.bold('Tool Preferences:')
93
+ spec['toolPreferences'].each do |pref|
94
+ puts " #{pastel.green('•')} #{pref}"
95
+ end
96
+ puts
97
+ end
116
98
 
117
- puts '' * 80
118
- puts
119
- Formatters::ProgressFormatter.info('Use this persona when creating agents:')
120
- puts " aictl agent create \"description\" --persona #{name}"
121
- puts
122
- rescue StandardError => e
123
- Formatters::ProgressFormatter.error("Failed to show persona: #{e.message}")
124
- raise if ENV['DEBUG']
99
+ if spec['responseFormat']
100
+ puts "#{pastel.bold('Response Format:')} #{spec['responseFormat']}"
101
+ puts
102
+ end
125
103
 
126
- exit 1
104
+ puts '═' * 80
105
+ puts
106
+ Formatters::ProgressFormatter.info('Use this persona when creating agents:')
107
+ puts " aictl agent create \"description\" --persona #{name}"
108
+ puts
109
+ end
127
110
  end
128
111
 
129
112
  desc 'create NAME', 'Create a new persona'
@@ -137,124 +120,115 @@ module LanguageOperator
137
120
  option :cluster, type: :string, desc: 'Override current cluster context'
138
121
  option :from, type: :string, desc: 'Copy from existing persona as starting point'
139
122
  def create(name)
140
- ctx = Helpers::ClusterContext.from_options(options)
141
-
142
- # Check if persona already exists
143
- begin
144
- ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
145
- Formatters::ProgressFormatter.error("Persona '#{name}' already exists in cluster '#{ctx.name}'")
146
- puts
147
- puts 'Use a different name or delete the existing persona first:'
148
- puts " aictl persona delete #{name}"
149
- exit 1
150
- rescue K8s::Error::NotFound
151
- # Good - persona doesn't exist yet
152
- end
153
-
154
- # If --from flag provided, copy from existing persona
155
- base_persona = nil
156
- if options[:from]
123
+ handle_command_error('create persona') do
124
+ # Check if persona already exists
157
125
  begin
158
- base_persona = ctx.client.get_resource('LanguagePersona', options[:from], ctx.namespace)
159
- Formatters::ProgressFormatter.info("Copying from persona '#{options[:from]}'")
126
+ ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
127
+ Formatters::ProgressFormatter.error("Persona '#{name}' already exists in cluster '#{ctx.name}'")
160
128
  puts
161
- rescue K8s::Error::NotFound
162
- Formatters::ProgressFormatter.error("Source persona '#{options[:from]}' not found")
129
+ puts 'Use a different name or delete the existing persona first:'
130
+ puts " aictl persona delete #{name}"
163
131
  exit 1
132
+ rescue K8s::Error::NotFound
133
+ # Good - persona doesn't exist yet
164
134
  end
165
- end
166
135
 
167
- # Interactive prompts
168
- require 'tty-prompt'
169
- prompt = TTY::Prompt.new
136
+ # If --from flag provided, copy from existing persona
137
+ base_persona = nil
138
+ if options[:from]
139
+ base_persona = get_resource_or_exit('LanguagePersona', options[:from],
140
+ error_message: "Source persona '#{options[:from]}' not found")
141
+ Formatters::ProgressFormatter.info("Copying from persona '#{options[:from]}'")
142
+ puts
143
+ end
170
144
 
171
- puts
172
- puts '=' * 80
173
- puts ' Create New Persona'
174
- puts '=' * 80
175
- puts
145
+ # Interactive prompts
146
+ require 'tty-prompt'
147
+ prompt = TTY::Prompt.new
176
148
 
177
- # Get display name
178
- default_display_name = base_persona&.dig('spec', 'displayName') || name.split('-').map(&:capitalize).join(' ')
179
- display_name = prompt.ask('Display Name:', default: default_display_name)
149
+ puts
150
+ puts '=' * 80
151
+ puts ' Create New Persona'
152
+ puts '=' * 80
153
+ puts
180
154
 
181
- # Get description
182
- default_description = base_persona&.dig('spec', 'description') || ''
183
- description = prompt.ask('Description:', default: default_description) do |q|
184
- q.required true
185
- end
155
+ # Get display name
156
+ default_display_name = base_persona&.dig('spec', 'displayName') || name.split('-').map(&:capitalize).join(' ')
157
+ display_name = prompt.ask('Display Name:', default: default_display_name)
186
158
 
187
- # Get tone
188
- default_tone = base_persona&.dig('spec', 'tone') || 'neutral'
189
- tone = prompt.select('Tone:', %w[neutral friendly professional technical creative], default: default_tone)
159
+ # Get description
160
+ default_description = base_persona&.dig('spec', 'description') || ''
161
+ description = prompt.ask('Description:', default: default_description) do |q|
162
+ q.required true
163
+ end
190
164
 
191
- # Get system prompt
192
- puts
193
- puts 'System Prompt (press Enter to open editor):'
194
- prompt.keypress('Press any key to continue...')
165
+ # Get tone
166
+ default_tone = base_persona&.dig('spec', 'tone') || 'neutral'
167
+ tone = prompt.select('Tone:', %w[neutral friendly professional technical creative], default: default_tone)
195
168
 
196
- default_system_prompt = base_persona&.dig('spec', 'systemPrompt') || ''
197
- system_prompt = edit_in_editor(default_system_prompt, 'persona-system-prompt')
169
+ # Get system prompt
170
+ puts
171
+ puts 'System Prompt (press Enter to open editor):'
172
+ prompt.keypress('Press any key to continue...')
198
173
 
199
- if system_prompt.strip.empty?
200
- Formatters::ProgressFormatter.error('System prompt cannot be empty')
201
- exit 1
202
- end
174
+ default_system_prompt = base_persona&.dig('spec', 'systemPrompt') || ''
175
+ system_prompt = Helpers::EditorHelper.edit_content(default_system_prompt, 'persona-system-prompt', '.txt')
203
176
 
204
- # Get capabilities
205
- puts
206
- puts 'Capabilities (optional, press Enter to open editor, or leave empty to skip):'
207
- puts 'Describe what this persona can do, one per line.'
208
- prompt.keypress('Press any key to continue...')
209
-
210
- default_capabilities = base_persona&.dig('spec', 'capabilities')&.join("\n") || ''
211
- capabilities_text = edit_in_editor(default_capabilities, 'persona-capabilities')
212
- capabilities = capabilities_text.strip.empty? ? [] : capabilities_text.strip.split("\n").map(&:strip).reject(&:empty?)
213
-
214
- # Build persona resource
215
- persona_spec = {
216
- 'displayName' => display_name,
217
- 'description' => description,
218
- 'tone' => tone,
219
- 'systemPrompt' => system_prompt.strip
220
- }
221
- persona_spec['capabilities'] = capabilities unless capabilities.empty?
222
-
223
- persona_resource = Kubernetes::ResourceBuilder.build_persona(
224
- name: name,
225
- spec: persona_spec,
226
- namespace: ctx.namespace
227
- )
228
-
229
- # Show preview
230
- puts
231
- puts '=' * 80
232
- puts 'Preview:'
233
- puts '=' * 80
234
- puts YAML.dump(persona_resource)
235
- puts '=' * 80
236
- puts
237
-
238
- # Confirm creation
239
- unless prompt.yes?('Create this persona?')
240
- puts 'Creation cancelled'
241
- return
242
- end
177
+ if system_prompt.strip.empty?
178
+ Formatters::ProgressFormatter.error('System prompt cannot be empty')
179
+ exit 1
180
+ end
243
181
 
244
- # Create persona
245
- Formatters::ProgressFormatter.with_spinner("Creating persona '#{name}'") do
246
- ctx.client.create_resource(persona_resource)
247
- end
182
+ # Get capabilities
183
+ puts
184
+ puts 'Capabilities (optional, press Enter to open editor, or leave empty to skip):'
185
+ puts 'Describe what this persona can do, one per line.'
186
+ prompt.keypress('Press any key to continue...')
187
+
188
+ default_capabilities = base_persona&.dig('spec', 'capabilities')&.join("\n") || ''
189
+ capabilities_text = Helpers::EditorHelper.edit_content(default_capabilities, 'persona-capabilities', '.txt')
190
+ capabilities = capabilities_text.strip.empty? ? [] : capabilities_text.strip.split("\n").map(&:strip).reject(&:empty?)
191
+
192
+ # Build persona resource
193
+ persona_spec = {
194
+ 'displayName' => display_name,
195
+ 'description' => description,
196
+ 'tone' => tone,
197
+ 'systemPrompt' => system_prompt.strip
198
+ }
199
+ persona_spec['capabilities'] = capabilities unless capabilities.empty?
248
200
 
249
- Formatters::ProgressFormatter.success("Persona '#{name}' created successfully")
250
- puts
251
- puts 'Use this persona when creating agents:'
252
- puts " aictl agent create \"description\" --persona #{name}"
253
- rescue StandardError => e
254
- Formatters::ProgressFormatter.error("Failed to create persona: #{e.message}")
255
- raise if ENV['DEBUG']
201
+ persona_resource = Kubernetes::ResourceBuilder.build_persona(
202
+ name: name,
203
+ spec: persona_spec,
204
+ namespace: ctx.namespace
205
+ )
256
206
 
257
- exit 1
207
+ # Show preview
208
+ puts
209
+ puts '=' * 80
210
+ puts 'Preview:'
211
+ puts '=' * 80
212
+ puts YAML.dump(persona_resource)
213
+ puts '=' * 80
214
+ puts
215
+
216
+ # Confirm creation
217
+ unless prompt.yes?('Create this persona?')
218
+ puts 'Creation cancelled'
219
+ return
220
+ end
221
+
222
+ # Create persona
223
+ Formatters::ProgressFormatter.with_spinner("Creating persona '#{name}'") do
224
+ ctx.client.create_resource(persona_resource)
225
+ end
226
+
227
+ Formatters::ProgressFormatter.success("Persona '#{name}' created successfully")
228
+ puts
229
+ puts 'Use this persona when creating agents:'
230
+ puts " aictl agent create \"description\" --persona #{name}"
231
+ end
258
232
  end
259
233
 
260
234
  desc 'edit NAME', 'Edit an existing persona'
@@ -269,126 +243,83 @@ module LanguageOperator
269
243
  DESC
270
244
  option :cluster, type: :string, desc: 'Override current cluster context'
271
245
  def edit(name)
272
- ctx = Helpers::ClusterContext.from_options(options)
273
-
274
- # Get current persona
275
- begin
276
- persona = ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
277
- rescue K8s::Error::NotFound
278
- Formatters::ProgressFormatter.error("Persona '#{name}' not found in cluster '#{ctx.name}'")
279
- exit 1
280
- end
281
-
282
- # Open in editor
283
- original_yaml = YAML.dump(persona)
284
- edited_yaml = edit_in_editor(original_yaml, "persona-#{name}")
285
-
286
- # Check if changed
287
- if edited_yaml.strip == original_yaml.strip
288
- Formatters::ProgressFormatter.info('No changes made')
289
- return
290
- end
246
+ handle_command_error('edit persona') do
247
+ # Get current persona
248
+ persona = get_resource_or_exit('LanguagePersona', name)
249
+
250
+ # Open in editor
251
+ original_yaml = YAML.dump(persona)
252
+ edited_yaml = Helpers::EditorHelper.edit_content(original_yaml, "persona-#{name}", '.txt')
253
+
254
+ # Check if changed
255
+ if edited_yaml.strip == original_yaml.strip
256
+ Formatters::ProgressFormatter.info('No changes made')
257
+ return
258
+ end
291
259
 
292
- # Parse edited YAML
293
- begin
294
- edited_persona = YAML.safe_load(edited_yaml)
295
- rescue Psych::SyntaxError => e
296
- Formatters::ProgressFormatter.error("Invalid YAML: #{e.message}")
297
- exit 1
298
- end
260
+ # Parse edited YAML
261
+ begin
262
+ edited_persona = YAML.safe_load(edited_yaml)
263
+ rescue Psych::SyntaxError => e
264
+ Formatters::ProgressFormatter.error("Invalid YAML: #{e.message}")
265
+ exit 1
266
+ end
299
267
 
300
- # Validate structure
301
- unless edited_persona.is_a?(Hash) && edited_persona['spec']
302
- Formatters::ProgressFormatter.error('Invalid persona structure: missing spec')
303
- exit 1
304
- end
268
+ # Validate structure
269
+ unless edited_persona.is_a?(Hash) && edited_persona['spec']
270
+ Formatters::ProgressFormatter.error('Invalid persona structure: missing spec')
271
+ exit 1
272
+ end
305
273
 
306
- # Update persona
307
- Formatters::ProgressFormatter.with_spinner("Updating persona '#{name}'") do
308
- ctx.client.update_resource(edited_persona)
309
- end
274
+ # Update persona
275
+ Formatters::ProgressFormatter.with_spinner("Updating persona '#{name}'") do
276
+ ctx.client.update_resource(edited_persona)
277
+ end
310
278
 
311
- Formatters::ProgressFormatter.success("Persona '#{name}' updated successfully")
279
+ Formatters::ProgressFormatter.success("Persona '#{name}' updated successfully")
312
280
 
313
- # Check for agents using this persona
314
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
315
- agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
281
+ # Check for agents using this persona
282
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
283
+ agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
316
284
 
317
- if agents_using.any?
318
- puts
319
- Formatters::ProgressFormatter.info("#{agents_using.count} agent(s) will be re-synthesized automatically:")
320
- agents_using.each do |agent|
321
- puts " - #{agent.dig('metadata', 'name')}"
285
+ if agents_using.any?
286
+ puts
287
+ Formatters::ProgressFormatter.info("#{agents_using.count} agent(s) will be re-synthesized automatically:")
288
+ agents_using.each do |agent|
289
+ puts " - #{agent.dig('metadata', 'name')}"
290
+ end
322
291
  end
323
292
  end
324
- rescue StandardError => e
325
- Formatters::ProgressFormatter.error("Failed to edit persona: #{e.message}")
326
- raise if ENV['DEBUG']
327
-
328
- exit 1
329
293
  end
330
294
 
331
295
  desc 'delete NAME', 'Delete a persona'
332
296
  option :cluster, type: :string, desc: 'Override current cluster context'
333
297
  option :force, type: :boolean, default: false, desc: 'Skip confirmation'
334
298
  def delete(name)
335
- ctx = Helpers::ClusterContext.from_options(options)
336
-
337
- # Get persona
338
- begin
339
- persona = ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
340
- rescue K8s::Error::NotFound
341
- Formatters::ProgressFormatter.error("Persona '#{name}' not found in cluster '#{ctx.name}'")
342
- exit 1
343
- end
344
-
345
- # Check for agents using this persona
346
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
347
- agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
348
-
349
- if agents_using.any? && !options[:force]
350
- Formatters::ProgressFormatter.warn("Persona '#{name}' is in use by #{agents_using.count} agent(s)")
351
- puts
352
- puts 'Agents using this persona:'
353
- agents_using.each do |agent|
354
- puts " - #{agent.dig('metadata', 'name')}"
299
+ handle_command_error('delete persona') do
300
+ # Get persona
301
+ persona = get_resource_or_exit('LanguagePersona', name)
302
+
303
+ # Check dependencies and get confirmation
304
+ return unless check_dependencies_and_confirm('persona', name, force: options[:force])
305
+
306
+ # Confirm deletion unless --force
307
+ if confirm_deletion(
308
+ 'persona', name, ctx.name,
309
+ details: {
310
+ 'Tone' => persona.dig('spec', 'tone'),
311
+ 'Description' => persona.dig('spec', 'description')
312
+ },
313
+ force: options[:force]
314
+ )
315
+ # Delete persona
316
+ Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
317
+ ctx.client.delete_resource('LanguagePersona', name, ctx.namespace)
318
+ end
319
+
320
+ Formatters::ProgressFormatter.success("Persona '#{name}' deleted successfully")
355
321
  end
356
- puts
357
- puts 'Delete these agents first, or use --force to delete anyway.'
358
- puts
359
- return unless Helpers::UserPrompts.confirm('Are you sure?')
360
322
  end
361
-
362
- # Confirm deletion unless --force
363
- unless options[:force] || agents_using.any?
364
- puts "This will delete persona '#{name}' from cluster '#{ctx.name}':"
365
- puts " Tone: #{persona.dig('spec', 'tone')}"
366
- puts " Description: #{persona.dig('spec', 'description')}"
367
- puts
368
- return unless Helpers::UserPrompts.confirm('Are you sure?')
369
- end
370
-
371
- # Delete persona
372
- Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
373
- ctx.client.delete_resource('LanguagePersona', name, ctx.namespace)
374
- end
375
-
376
- Formatters::ProgressFormatter.success("Persona '#{name}' deleted successfully")
377
- rescue StandardError => e
378
- Formatters::ProgressFormatter.error("Failed to delete persona: #{e.message}")
379
- raise if ENV['DEBUG']
380
-
381
- exit 1
382
- end
383
-
384
- private
385
-
386
- def edit_in_editor(content, filename_prefix)
387
- Helpers::EditorHelper.edit_content(content, filename_prefix, '.txt')
388
- end
389
-
390
- def pastel
391
- @pastel ||= Pastel.new
392
323
  end
393
324
  end
394
325
  end
@@ -1,18 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thor'
3
+ require_relative '../base_command'
4
4
  require_relative '../formatters/progress_formatter'
5
- require_relative '../wizards/quickstart_wizard'
5
+ require_relative '../../ux/quickstart'
6
6
 
7
7
  module LanguageOperator
8
8
  module CLI
9
9
  module Commands
10
10
  # Quickstart wizard for first-time users
11
- class Quickstart < Thor
11
+ class Quickstart < BaseCommand
12
12
  desc 'start', 'Interactive setup wizard for first-time users'
13
13
  def start
14
- wizard = Wizards::QuickstartWizard.new
15
- wizard.run
14
+ Ux::Quickstart.execute
16
15
  end
17
16
 
18
17
  default_task :start