prompt_objects 0.1.0

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 (117) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +108 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +231 -0
  5. data/IMPLEMENTATION_PLAN.md +1073 -0
  6. data/LICENSE +21 -0
  7. data/README.md +73 -0
  8. data/Rakefile +27 -0
  9. data/design-doc-v2.md +1232 -0
  10. data/exe/prompt_objects +572 -0
  11. data/exe/prompt_objects_mcp +34 -0
  12. data/frontend/.gitignore +3 -0
  13. data/frontend/index.html +13 -0
  14. data/frontend/package-lock.json +4417 -0
  15. data/frontend/package.json +32 -0
  16. data/frontend/postcss.config.js +6 -0
  17. data/frontend/src/App.tsx +95 -0
  18. data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
  19. data/frontend/src/components/ChatPanel.tsx +251 -0
  20. data/frontend/src/components/Dashboard.tsx +83 -0
  21. data/frontend/src/components/Header.tsx +141 -0
  22. data/frontend/src/components/MarkdownMessage.tsx +153 -0
  23. data/frontend/src/components/MessageBus.tsx +55 -0
  24. data/frontend/src/components/ModelSelector.tsx +112 -0
  25. data/frontend/src/components/NotificationPanel.tsx +134 -0
  26. data/frontend/src/components/POCard.tsx +56 -0
  27. data/frontend/src/components/PODetail.tsx +117 -0
  28. data/frontend/src/components/PromptPanel.tsx +51 -0
  29. data/frontend/src/components/SessionsPanel.tsx +174 -0
  30. data/frontend/src/components/ThreadsSidebar.tsx +119 -0
  31. data/frontend/src/components/index.ts +11 -0
  32. data/frontend/src/hooks/useWebSocket.ts +363 -0
  33. data/frontend/src/index.css +37 -0
  34. data/frontend/src/main.tsx +10 -0
  35. data/frontend/src/store/index.ts +246 -0
  36. data/frontend/src/types/index.ts +146 -0
  37. data/frontend/tailwind.config.js +25 -0
  38. data/frontend/tsconfig.json +30 -0
  39. data/frontend/vite.config.ts +29 -0
  40. data/lib/prompt_objects/capability.rb +46 -0
  41. data/lib/prompt_objects/cli.rb +431 -0
  42. data/lib/prompt_objects/connectors/base.rb +73 -0
  43. data/lib/prompt_objects/connectors/mcp.rb +524 -0
  44. data/lib/prompt_objects/environment/exporter.rb +83 -0
  45. data/lib/prompt_objects/environment/git.rb +118 -0
  46. data/lib/prompt_objects/environment/importer.rb +159 -0
  47. data/lib/prompt_objects/environment/manager.rb +401 -0
  48. data/lib/prompt_objects/environment/manifest.rb +218 -0
  49. data/lib/prompt_objects/environment.rb +283 -0
  50. data/lib/prompt_objects/human_queue.rb +144 -0
  51. data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
  52. data/lib/prompt_objects/llm/factory.rb +84 -0
  53. data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
  54. data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
  55. data/lib/prompt_objects/llm/response.rb +61 -0
  56. data/lib/prompt_objects/loader.rb +32 -0
  57. data/lib/prompt_objects/mcp/server.rb +167 -0
  58. data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
  59. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
  60. data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
  61. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
  62. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
  63. data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
  64. data/lib/prompt_objects/message_bus.rb +97 -0
  65. data/lib/prompt_objects/primitive.rb +13 -0
  66. data/lib/prompt_objects/primitives/http_get.rb +72 -0
  67. data/lib/prompt_objects/primitives/list_files.rb +95 -0
  68. data/lib/prompt_objects/primitives/read_file.rb +81 -0
  69. data/lib/prompt_objects/primitives/write_file.rb +73 -0
  70. data/lib/prompt_objects/prompt_object.rb +415 -0
  71. data/lib/prompt_objects/registry.rb +88 -0
  72. data/lib/prompt_objects/server/api/routes.rb +297 -0
  73. data/lib/prompt_objects/server/app.rb +174 -0
  74. data/lib/prompt_objects/server/file_watcher.rb +113 -0
  75. data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
  76. data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
  77. data/lib/prompt_objects/server/public/index.html +14 -0
  78. data/lib/prompt_objects/server/websocket_handler.rb +619 -0
  79. data/lib/prompt_objects/server.rb +166 -0
  80. data/lib/prompt_objects/session/store.rb +826 -0
  81. data/lib/prompt_objects/universal/add_capability.rb +74 -0
  82. data/lib/prompt_objects/universal/add_primitive.rb +113 -0
  83. data/lib/prompt_objects/universal/ask_human.rb +109 -0
  84. data/lib/prompt_objects/universal/create_capability.rb +219 -0
  85. data/lib/prompt_objects/universal/create_primitive.rb +170 -0
  86. data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
  87. data/lib/prompt_objects/universal/list_primitives.rb +145 -0
  88. data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
  89. data/lib/prompt_objects/universal/request_primitive.rb +287 -0
  90. data/lib/prompt_objects/universal/think.rb +41 -0
  91. data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
  92. data/lib/prompt_objects.rb +62 -0
  93. data/objects/coordinator.md +48 -0
  94. data/objects/greeter.md +30 -0
  95. data/objects/reader.md +33 -0
  96. data/prompt_objects.gemspec +50 -0
  97. data/templates/basic/.gitignore +2 -0
  98. data/templates/basic/manifest.yml +7 -0
  99. data/templates/basic/objects/basic.md +32 -0
  100. data/templates/developer/.gitignore +5 -0
  101. data/templates/developer/manifest.yml +17 -0
  102. data/templates/developer/objects/code_reviewer.md +33 -0
  103. data/templates/developer/objects/coordinator.md +39 -0
  104. data/templates/developer/objects/debugger.md +35 -0
  105. data/templates/empty/.gitignore +5 -0
  106. data/templates/empty/manifest.yml +14 -0
  107. data/templates/empty/objects/.gitkeep +0 -0
  108. data/templates/empty/objects/assistant.md +41 -0
  109. data/templates/minimal/.gitignore +5 -0
  110. data/templates/minimal/manifest.yml +7 -0
  111. data/templates/minimal/objects/assistant.md +41 -0
  112. data/templates/writer/.gitignore +5 -0
  113. data/templates/writer/manifest.yml +17 -0
  114. data/templates/writer/objects/coordinator.md +33 -0
  115. data/templates/writer/objects/editor.md +33 -0
  116. data/templates/writer/objects/researcher.md +34 -0
  117. metadata +343 -0
@@ -0,0 +1,431 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ # Command-line interface for environment management.
5
+ module CLI
6
+ # Handle env subcommands: list, create, info, archive, restore, clone, etc.
7
+ class EnvCommand
8
+ def initialize(manager: nil)
9
+ @manager = manager || Env::Manager.new
10
+ end
11
+
12
+ # Run the env subcommand.
13
+ # @param args [Array<String>] Arguments after 'env'
14
+ def run(args)
15
+ subcommand = args.shift || "list"
16
+
17
+ case subcommand
18
+ when "list", "ls"
19
+ list
20
+ when "create", "new"
21
+ create(args)
22
+ when "info"
23
+ info(args)
24
+ when "export"
25
+ export(args)
26
+ when "import"
27
+ import(args)
28
+ when "archive"
29
+ archive(args)
30
+ when "restore"
31
+ restore(args)
32
+ when "clone", "cp"
33
+ clone(args)
34
+ when "delete"
35
+ delete(args)
36
+ when "default"
37
+ set_default(args)
38
+ else
39
+ puts "Unknown env command: #{subcommand}"
40
+ puts
41
+ help
42
+ exit 1
43
+ end
44
+ end
45
+
46
+ def list
47
+ @manager.setup!
48
+ envs = @manager.list_with_manifests
49
+
50
+ if envs.empty?
51
+ puts "No environments found."
52
+ puts "Create one with: prompt_objects env create <name>"
53
+ return
54
+ end
55
+
56
+ default = @manager.default_environment
57
+
58
+ puts "Environments:"
59
+ puts
60
+ envs.each do |manifest|
61
+ default_marker = manifest.name == default ? " (default)" : ""
62
+ last_opened = manifest.last_opened&.strftime("%Y-%m-%d") || "never"
63
+ puts " #{manifest.icon} #{manifest.name}#{default_marker}"
64
+ puts " #{manifest.description}" if manifest.description
65
+ puts " Last opened: #{last_opened}, Objects: #{manifest.stats['po_count']}"
66
+ puts
67
+ end
68
+ end
69
+
70
+ def create(args)
71
+ name = args.shift
72
+ unless name
73
+ puts "Usage: prompt_objects env create <name> [--template <template>]"
74
+ exit 1
75
+ end
76
+
77
+ template = nil
78
+ args.each_with_index do |arg, i|
79
+ if arg == "--template" || arg == "-t"
80
+ template = args[i + 1]
81
+ end
82
+ end
83
+
84
+ @manager.setup!
85
+
86
+ path = @manager.create(name: name, template: template)
87
+ puts "Created environment: #{name}"
88
+ puts "Location: #{path}"
89
+
90
+ if @manager.list.size == 1
91
+ @manager.set_default_environment(name)
92
+ puts "Set as default environment."
93
+ end
94
+ end
95
+
96
+ def info(args)
97
+ name = args.shift || @manager.default_environment
98
+ unless name
99
+ puts "Usage: prompt_objects env info <name>"
100
+ exit 1
101
+ end
102
+
103
+ manifest = @manager.manifest_for(name)
104
+ unless manifest
105
+ puts "Environment '#{name}' not found."
106
+ exit 1
107
+ end
108
+
109
+ path = @manager.environment_path(name)
110
+ objects = Dir.glob(File.join(path, "objects", "*.md")).map { |f| File.basename(f, ".md") }
111
+ primitives = Dir.glob(File.join(path, "primitives", "*.rb")).map { |f| File.basename(f, ".rb") }
112
+
113
+ puts manifest.info
114
+ puts
115
+ puts " Location: #{path}"
116
+ puts " Objects: #{objects.join(', ')}" if objects.any?
117
+ puts " Custom primitives: #{primitives.join(', ')}" if primitives.any?
118
+ puts
119
+
120
+ if Env::Git.repo?(path)
121
+ commits = Env::Git.commit_count(path)
122
+ dirty = Env::Git.dirty?(path)
123
+ puts " Git: #{commits} commits#{dirty ? ' (uncommitted changes)' : ''}"
124
+ end
125
+ end
126
+
127
+ def export(args)
128
+ @manager.setup!
129
+
130
+ # Parse arguments
131
+ name = nil
132
+ output = nil
133
+
134
+ i = 0
135
+ while i < args.length
136
+ case args[i]
137
+ when "-o", "--output"
138
+ output = args[i + 1]
139
+ i += 1
140
+ else
141
+ name ||= args[i]
142
+ end
143
+ i += 1
144
+ end
145
+
146
+ # Default to current directory name if no output specified
147
+ name ||= @manager.default_environment
148
+ unless name
149
+ puts "Usage: prompt_objects env export <name> [-o output.poenv]"
150
+ exit 1
151
+ end
152
+
153
+ unless @manager.environment_exists?(name)
154
+ puts "Environment '#{name}' not found."
155
+ exit 1
156
+ end
157
+
158
+ output ||= "#{name}.poenv"
159
+ env_path = @manager.environment_path(name)
160
+
161
+ exporter = Env::Exporter.new(env_path)
162
+ result = exporter.export(output)
163
+
164
+ if result[:success]
165
+ puts "Exported '#{name}' to: #{result[:path]}"
166
+ puts
167
+ puts "Stats:"
168
+ puts " Commits: #{result[:stats][:commits]}"
169
+ puts " Objects: #{result[:stats][:objects]}"
170
+ puts " Primitives: #{result[:stats][:primitives]}"
171
+ else
172
+ puts "Export failed: #{result[:error]}"
173
+ exit 1
174
+ end
175
+ end
176
+
177
+ def import(args)
178
+ @manager.setup!
179
+
180
+ # Parse arguments
181
+ bundle_path = nil
182
+ import_as = nil
183
+ trust = false
184
+
185
+ i = 0
186
+ while i < args.length
187
+ case args[i]
188
+ when "--as"
189
+ import_as = args[i + 1]
190
+ i += 1
191
+ when "--trust"
192
+ trust = true
193
+ else
194
+ bundle_path ||= args[i]
195
+ end
196
+ i += 1
197
+ end
198
+
199
+ unless bundle_path
200
+ puts "Usage: prompt_objects env import <bundle.poenv> [--as <name>] [--trust]"
201
+ puts
202
+ puts "Options:"
203
+ puts " --as <name> Import with a different name"
204
+ puts " --trust Trust custom primitives (skip sandbox warnings)"
205
+ exit 1
206
+ end
207
+
208
+ importer = Env::Importer.new(bundle_path)
209
+
210
+ # First, inspect and show what's in the bundle
211
+ info = importer.inspect_bundle
212
+ unless info.valid
213
+ puts "Invalid bundle: #{info.error}"
214
+ exit 1
215
+ end
216
+
217
+ puts "Bundle contents:"
218
+ puts " Name: #{info.name}"
219
+ puts " Description: #{info.description}" if info.description
220
+ puts " Objects: #{info.objects.join(', ')}" if info.objects.any?
221
+ puts " Primitives: #{info.primitives.join(', ')}" if info.primitives.any?
222
+ puts " Commits: #{info.commits}"
223
+ puts
224
+
225
+ # Warn about primitives
226
+ if info.primitives.any? && !trust
227
+ puts "⚠️ WARNING: This bundle contains custom primitives."
228
+ puts "Custom primitives can execute arbitrary Ruby code."
229
+ puts "Review the code before running, or use --trust to skip this warning."
230
+ puts
231
+ end
232
+
233
+ # Import
234
+ import_name = import_as || info.name
235
+ result = importer.import(manager: @manager, name: import_name, trust_primitives: trust)
236
+
237
+ if result[:success]
238
+ puts "Imported as '#{result[:name]}'"
239
+ puts "Location: #{result[:path]}"
240
+
241
+ if result[:warnings].any?
242
+ puts
243
+ result[:warnings].each { |w| puts w }
244
+ end
245
+ else
246
+ puts "Import failed: #{result[:error]}"
247
+ exit 1
248
+ end
249
+ end
250
+
251
+ def archive(args)
252
+ name = args.shift
253
+ unless name
254
+ puts "Usage: prompt_objects env archive <name>"
255
+ exit 1
256
+ end
257
+
258
+ path = @manager.archive(name)
259
+ puts "Archived environment '#{name}' to:"
260
+ puts " #{path}"
261
+ end
262
+
263
+ def restore(args)
264
+ archived_name = args.shift
265
+ unless archived_name
266
+ # Show available archived environments
267
+ archived = @manager.list_archived
268
+ if archived.empty?
269
+ puts "No archived environments."
270
+ else
271
+ puts "Archived environments:"
272
+ archived.each { |name| puts " - #{name}" }
273
+ puts
274
+ puts "Usage: prompt_objects env restore <archived_name> [--as <new_name>]"
275
+ end
276
+ return
277
+ end
278
+
279
+ restore_as = nil
280
+ args.each_with_index do |arg, i|
281
+ restore_as = args[i + 1] if arg == "--as"
282
+ end
283
+
284
+ path = @manager.restore(archived_name, restore_as: restore_as)
285
+ puts "Restored to: #{path}"
286
+ end
287
+
288
+ def clone(args)
289
+ source = args.shift
290
+ target = args.shift
291
+
292
+ unless source && target
293
+ puts "Usage: prompt_objects env clone <source> <target>"
294
+ exit 1
295
+ end
296
+
297
+ path = @manager.clone(source, target)
298
+ puts "Cloned '#{source}' to '#{target}'"
299
+ puts "Location: #{path}"
300
+ end
301
+
302
+ def delete(args)
303
+ name = args.shift
304
+ permanent = args.include?("--permanent")
305
+
306
+ unless name
307
+ puts "Usage: prompt_objects env delete <archived_name> --permanent"
308
+ puts
309
+ puts "Note: Only archived environments can be permanently deleted."
310
+ puts "First archive an environment, then delete it."
311
+ return
312
+ end
313
+
314
+ unless permanent
315
+ puts "Use --permanent to confirm deletion."
316
+ puts "This cannot be undone."
317
+ return
318
+ end
319
+
320
+ @manager.delete_archived(name)
321
+ puts "Permanently deleted: #{name}"
322
+ end
323
+
324
+ def set_default(args)
325
+ name = args.shift
326
+ unless name
327
+ current = @manager.default_environment
328
+ if current
329
+ puts "Current default: #{current}"
330
+ else
331
+ puts "No default environment set."
332
+ end
333
+ puts
334
+ puts "Usage: prompt_objects env default <name>"
335
+ return
336
+ end
337
+
338
+ @manager.set_default_environment(name)
339
+ puts "Default environment set to: #{name}"
340
+ end
341
+
342
+ def help
343
+ puts <<~HELP
344
+ Environment management commands:
345
+
346
+ prompt_objects env list List all environments
347
+ prompt_objects env create <name> Create new environment
348
+ --template, -t <template> Use template (minimal, developer, writer, empty)
349
+ prompt_objects env info <name> Show environment details
350
+ prompt_objects env export <name> Export environment as .poenv bundle
351
+ -o, --output <file> Output file path
352
+ prompt_objects env import <file> Import environment from .poenv bundle
353
+ --as <name> Import with different name
354
+ --trust Trust custom primitives
355
+ prompt_objects env archive <name> Archive (soft delete) environment
356
+ prompt_objects env restore <name> Restore archived environment
357
+ --as <new_name> Restore with different name
358
+ prompt_objects env clone <src> <dest> Clone environment
359
+ prompt_objects env default <name> Set default environment
360
+ prompt_objects env delete <name> --permanent Delete archived env
361
+
362
+ Available templates:
363
+ basic - No capabilities, learns as needed (great for demos!)
364
+ minimal - Basic assistant with file reading
365
+ developer - Code review, debugging, testing specialists
366
+ writer - Editor, researcher for content creation
367
+ empty - Bootstrap assistant only
368
+ HELP
369
+ end
370
+ end
371
+
372
+ # Parse arguments and run appropriate command.
373
+ # @param args [Array<String>] Command line arguments
374
+ # @return [Hash] Parsed options for main command
375
+ def self.parse(args)
376
+ options = {
377
+ env_name: nil,
378
+ dev_mode: false,
379
+ sandbox: false,
380
+ command: :run
381
+ }
382
+
383
+ i = 0
384
+ while i < args.length
385
+ arg = args[i]
386
+ case arg
387
+ when "env"
388
+ options[:command] = :env
389
+ options[:env_args] = args[(i + 1)..]
390
+ return options
391
+ when "--env", "-e"
392
+ options[:env_name] = args[i + 1]
393
+ i += 1
394
+ when "--dev"
395
+ options[:dev_mode] = true
396
+ when "--sandbox", "-s"
397
+ options[:sandbox] = true
398
+ when "--help", "-h"
399
+ options[:command] = :help
400
+ return options
401
+ else
402
+ # Assume it's a PO name or objects_dir (legacy)
403
+ options[:legacy_args] ||= []
404
+ options[:legacy_args] << arg
405
+ end
406
+ i += 1
407
+ end
408
+
409
+ options
410
+ end
411
+
412
+ # List available templates.
413
+ # @return [Array<Hash>] Template info
414
+ def self.list_templates
415
+ templates_dir = File.expand_path("../../templates", __dir__)
416
+ return [] unless Dir.exist?(templates_dir)
417
+
418
+ Dir.children(templates_dir).filter_map do |name|
419
+ manifest_path = File.join(templates_dir, name, "manifest.yml")
420
+ next unless File.exist?(manifest_path)
421
+
422
+ manifest = YAML.safe_load(File.read(manifest_path))
423
+ {
424
+ name: name,
425
+ description: manifest["description"],
426
+ icon: manifest["icon"]
427
+ }
428
+ end
429
+ end
430
+ end
431
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Connectors
5
+ # Base class for connectors that provide different interfaces to environments.
6
+ # Each connector (MCP, API, Web, etc.) inherits from this class.
7
+ class Base
8
+ attr_reader :runtime, :config
9
+
10
+ # @param runtime [Runtime] The environment runtime
11
+ # @param config [Hash] Connector-specific configuration
12
+ def initialize(runtime:, config: {})
13
+ @runtime = runtime
14
+ @config = config
15
+ @running = false
16
+ end
17
+
18
+ # Start the connector (may be blocking or non-blocking depending on implementation)
19
+ def start
20
+ raise NotImplementedError, "#{self.class} must implement #start"
21
+ end
22
+
23
+ # Stop the connector gracefully
24
+ def stop
25
+ @running = false
26
+ end
27
+
28
+ # Whether the connector is currently running
29
+ def running?
30
+ @running
31
+ end
32
+
33
+ # Connector identifier for session source tracking
34
+ # @return [String] e.g., "mcp", "api", "web"
35
+ def source_name
36
+ raise NotImplementedError, "#{self.class} must implement #source_name"
37
+ end
38
+
39
+ protected
40
+
41
+ # Helper to get or create a session for a PO with source tracking
42
+ # @param po [PromptObject] The prompt object
43
+ # @param session_name [String, nil] Optional session name
44
+ # @return [String] Session ID
45
+ def get_or_create_session(po, session_name: nil)
46
+ return nil unless runtime.session_store
47
+
48
+ # Check if PO already has a session
49
+ session_id = po.instance_variable_get(:@session_id)
50
+ return session_id if session_id
51
+
52
+ # Create new session with source tracking
53
+ session_id = runtime.session_store.create_session(
54
+ po_name: po.name,
55
+ name: session_name || "#{source_name.upcase} Session #{Time.now.strftime('%H:%M')}",
56
+ source: source_name
57
+ )
58
+
59
+ # Attach to PO
60
+ po.instance_variable_set(:@session_id, session_id)
61
+ session_id
62
+ end
63
+
64
+ # Update last_message_source when a message is sent
65
+ # @param session_id [String] Session ID
66
+ def update_session_source(session_id)
67
+ return unless runtime.session_store && session_id
68
+
69
+ runtime.session_store.update_session(session_id, last_message_source: source_name)
70
+ end
71
+ end
72
+ end
73
+ end