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.
- checksums.yaml +7 -0
- data/CLAUDE.md +108 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +231 -0
- data/IMPLEMENTATION_PLAN.md +1073 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +27 -0
- data/design-doc-v2.md +1232 -0
- data/exe/prompt_objects +572 -0
- data/exe/prompt_objects_mcp +34 -0
- data/frontend/.gitignore +3 -0
- data/frontend/index.html +13 -0
- data/frontend/package-lock.json +4417 -0
- data/frontend/package.json +32 -0
- data/frontend/postcss.config.js +6 -0
- data/frontend/src/App.tsx +95 -0
- data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
- data/frontend/src/components/ChatPanel.tsx +251 -0
- data/frontend/src/components/Dashboard.tsx +83 -0
- data/frontend/src/components/Header.tsx +141 -0
- data/frontend/src/components/MarkdownMessage.tsx +153 -0
- data/frontend/src/components/MessageBus.tsx +55 -0
- data/frontend/src/components/ModelSelector.tsx +112 -0
- data/frontend/src/components/NotificationPanel.tsx +134 -0
- data/frontend/src/components/POCard.tsx +56 -0
- data/frontend/src/components/PODetail.tsx +117 -0
- data/frontend/src/components/PromptPanel.tsx +51 -0
- data/frontend/src/components/SessionsPanel.tsx +174 -0
- data/frontend/src/components/ThreadsSidebar.tsx +119 -0
- data/frontend/src/components/index.ts +11 -0
- data/frontend/src/hooks/useWebSocket.ts +363 -0
- data/frontend/src/index.css +37 -0
- data/frontend/src/main.tsx +10 -0
- data/frontend/src/store/index.ts +246 -0
- data/frontend/src/types/index.ts +146 -0
- data/frontend/tailwind.config.js +25 -0
- data/frontend/tsconfig.json +30 -0
- data/frontend/vite.config.ts +29 -0
- data/lib/prompt_objects/capability.rb +46 -0
- data/lib/prompt_objects/cli.rb +431 -0
- data/lib/prompt_objects/connectors/base.rb +73 -0
- data/lib/prompt_objects/connectors/mcp.rb +524 -0
- data/lib/prompt_objects/environment/exporter.rb +83 -0
- data/lib/prompt_objects/environment/git.rb +118 -0
- data/lib/prompt_objects/environment/importer.rb +159 -0
- data/lib/prompt_objects/environment/manager.rb +401 -0
- data/lib/prompt_objects/environment/manifest.rb +218 -0
- data/lib/prompt_objects/environment.rb +283 -0
- data/lib/prompt_objects/human_queue.rb +144 -0
- data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
- data/lib/prompt_objects/llm/factory.rb +84 -0
- data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
- data/lib/prompt_objects/llm/response.rb +61 -0
- data/lib/prompt_objects/loader.rb +32 -0
- data/lib/prompt_objects/mcp/server.rb +167 -0
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
- data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
- data/lib/prompt_objects/message_bus.rb +97 -0
- data/lib/prompt_objects/primitive.rb +13 -0
- data/lib/prompt_objects/primitives/http_get.rb +72 -0
- data/lib/prompt_objects/primitives/list_files.rb +95 -0
- data/lib/prompt_objects/primitives/read_file.rb +81 -0
- data/lib/prompt_objects/primitives/write_file.rb +73 -0
- data/lib/prompt_objects/prompt_object.rb +415 -0
- data/lib/prompt_objects/registry.rb +88 -0
- data/lib/prompt_objects/server/api/routes.rb +297 -0
- data/lib/prompt_objects/server/app.rb +174 -0
- data/lib/prompt_objects/server/file_watcher.rb +113 -0
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
- data/lib/prompt_objects/server/public/index.html +14 -0
- data/lib/prompt_objects/server/websocket_handler.rb +619 -0
- data/lib/prompt_objects/server.rb +166 -0
- data/lib/prompt_objects/session/store.rb +826 -0
- data/lib/prompt_objects/universal/add_capability.rb +74 -0
- data/lib/prompt_objects/universal/add_primitive.rb +113 -0
- data/lib/prompt_objects/universal/ask_human.rb +109 -0
- data/lib/prompt_objects/universal/create_capability.rb +219 -0
- data/lib/prompt_objects/universal/create_primitive.rb +170 -0
- data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
- data/lib/prompt_objects/universal/list_primitives.rb +145 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
- data/lib/prompt_objects/universal/request_primitive.rb +287 -0
- data/lib/prompt_objects/universal/think.rb +41 -0
- data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
- data/lib/prompt_objects.rb +62 -0
- data/objects/coordinator.md +48 -0
- data/objects/greeter.md +30 -0
- data/objects/reader.md +33 -0
- data/prompt_objects.gemspec +50 -0
- data/templates/basic/.gitignore +2 -0
- data/templates/basic/manifest.yml +7 -0
- data/templates/basic/objects/basic.md +32 -0
- data/templates/developer/.gitignore +5 -0
- data/templates/developer/manifest.yml +17 -0
- data/templates/developer/objects/code_reviewer.md +33 -0
- data/templates/developer/objects/coordinator.md +39 -0
- data/templates/developer/objects/debugger.md +35 -0
- data/templates/empty/.gitignore +5 -0
- data/templates/empty/manifest.yml +14 -0
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/empty/objects/assistant.md +41 -0
- data/templates/minimal/.gitignore +5 -0
- data/templates/minimal/manifest.yml +7 -0
- data/templates/minimal/objects/assistant.md +41 -0
- data/templates/writer/.gitignore +5 -0
- data/templates/writer/manifest.yml +17 -0
- data/templates/writer/objects/coordinator.md +33 -0
- data/templates/writer/objects/editor.md +33 -0
- data/templates/writer/objects/researcher.md +34 -0
- metadata +343 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rack"
|
|
5
|
+
|
|
6
|
+
module PromptObjects
|
|
7
|
+
module Server
|
|
8
|
+
module API
|
|
9
|
+
# REST API routes for the PromptObjects server.
|
|
10
|
+
# Provides endpoints for listing POs, sessions, and environment info.
|
|
11
|
+
class Routes
|
|
12
|
+
def initialize(runtime)
|
|
13
|
+
@runtime = runtime
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
request = Rack::Request.new(env)
|
|
18
|
+
path = request.path_info.sub("/api", "")
|
|
19
|
+
|
|
20
|
+
response = route(request, path)
|
|
21
|
+
json_response(response)
|
|
22
|
+
rescue => e
|
|
23
|
+
json_response({ error: e.message }, status: 500)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def route(request, path)
|
|
29
|
+
method = request.request_method
|
|
30
|
+
|
|
31
|
+
case [method, path]
|
|
32
|
+
|
|
33
|
+
# Environment
|
|
34
|
+
when ["GET", "/environment"]
|
|
35
|
+
get_environment
|
|
36
|
+
|
|
37
|
+
# Prompt Objects
|
|
38
|
+
when ["GET", "/prompt_objects"]
|
|
39
|
+
list_prompt_objects
|
|
40
|
+
|
|
41
|
+
when ["GET", %r{^/prompt_objects/([^/]+)$}]
|
|
42
|
+
get_prompt_object(path_param(path, 1))
|
|
43
|
+
|
|
44
|
+
when ["POST", "/prompt_objects"]
|
|
45
|
+
create_prompt_object(request.body.read)
|
|
46
|
+
|
|
47
|
+
# Sessions
|
|
48
|
+
when ["GET", %r{^/prompt_objects/([^/]+)/sessions$}]
|
|
49
|
+
list_sessions(path_param(path, 1))
|
|
50
|
+
|
|
51
|
+
when ["GET", %r{^/prompt_objects/([^/]+)/sessions/([^/]+)$}]
|
|
52
|
+
get_session(path_param(path, 1), path_param(path, 2))
|
|
53
|
+
|
|
54
|
+
when ["POST", %r{^/prompt_objects/([^/]+)/sessions$}]
|
|
55
|
+
create_session(path_param(path, 1), request.body.read)
|
|
56
|
+
|
|
57
|
+
# Primitives
|
|
58
|
+
when ["GET", "/primitives"]
|
|
59
|
+
list_primitives
|
|
60
|
+
|
|
61
|
+
# Message Bus
|
|
62
|
+
when ["GET", "/bus/recent"]
|
|
63
|
+
get_recent_bus_messages
|
|
64
|
+
|
|
65
|
+
else
|
|
66
|
+
{ error: "Not found", path: path }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# === Environment ===
|
|
71
|
+
|
|
72
|
+
def get_environment
|
|
73
|
+
manifest_data = if @runtime.manifest
|
|
74
|
+
{
|
|
75
|
+
name: @runtime.manifest.name,
|
|
76
|
+
description: @runtime.manifest.description,
|
|
77
|
+
icon: @runtime.manifest.icon,
|
|
78
|
+
created_at: @runtime.manifest.created_at&.iso8601,
|
|
79
|
+
last_opened: @runtime.manifest.last_opened&.iso8601
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
name: @runtime.name,
|
|
85
|
+
path: @runtime.env_path,
|
|
86
|
+
prompt_object_count: @runtime.registry.prompt_objects.size,
|
|
87
|
+
primitive_count: @runtime.registry.primitives.size,
|
|
88
|
+
manifest: manifest_data
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# === Prompt Objects ===
|
|
93
|
+
|
|
94
|
+
def list_prompt_objects
|
|
95
|
+
pos = @runtime.registry.prompt_objects.map do |po|
|
|
96
|
+
po_summary(po)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{ prompt_objects: pos }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_prompt_object(name)
|
|
103
|
+
po = @runtime.registry.get(name)
|
|
104
|
+
|
|
105
|
+
unless po.is_a?(PromptObject)
|
|
106
|
+
return { error: "Not found", name: name }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
po_full(po)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def create_prompt_object(body)
|
|
113
|
+
# TODO: Implement PO creation via API
|
|
114
|
+
{ error: "Not implemented" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# === Sessions ===
|
|
118
|
+
|
|
119
|
+
def list_sessions(po_name)
|
|
120
|
+
po = @runtime.registry.get(po_name)
|
|
121
|
+
|
|
122
|
+
unless po.is_a?(PromptObject)
|
|
123
|
+
return { error: "Not found", name: po_name }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sessions = po.list_sessions.map do |s|
|
|
127
|
+
{
|
|
128
|
+
id: s[:id],
|
|
129
|
+
name: s[:name],
|
|
130
|
+
message_count: s[:message_count] || 0,
|
|
131
|
+
created_at: s[:created_at]&.iso8601,
|
|
132
|
+
updated_at: s[:updated_at]&.iso8601
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
{ sessions: sessions }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def get_session(po_name, session_id)
|
|
140
|
+
po = @runtime.registry.get(po_name)
|
|
141
|
+
|
|
142
|
+
unless po.is_a?(PromptObject)
|
|
143
|
+
return { error: "Prompt object not found", name: po_name }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return { error: "No session store" } unless @runtime.session_store
|
|
147
|
+
|
|
148
|
+
session = @runtime.session_store.get_session(session_id)
|
|
149
|
+
|
|
150
|
+
unless session && session[:po_name] == po_name
|
|
151
|
+
return { error: "Session not found", id: session_id }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
messages = @runtime.session_store.get_messages(session_id)
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
id: session[:id],
|
|
158
|
+
name: session[:name],
|
|
159
|
+
po_name: session[:po_name],
|
|
160
|
+
messages: messages.map { |m| format_message(m) },
|
|
161
|
+
created_at: session[:created_at]&.iso8601,
|
|
162
|
+
updated_at: session[:updated_at]&.iso8601
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def create_session(po_name, body)
|
|
167
|
+
po = @runtime.registry.get(po_name)
|
|
168
|
+
|
|
169
|
+
unless po.is_a?(PromptObject)
|
|
170
|
+
return { error: "Not found", name: po_name }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
params = body.empty? ? {} : JSON.parse(body)
|
|
174
|
+
session_name = params["name"]
|
|
175
|
+
|
|
176
|
+
session_id = po.new_session(name: session_name)
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
success: true,
|
|
180
|
+
session_id: session_id,
|
|
181
|
+
name: session_name
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# === Primitives ===
|
|
186
|
+
|
|
187
|
+
def list_primitives
|
|
188
|
+
primitives = @runtime.registry.primitives.map do |p|
|
|
189
|
+
{
|
|
190
|
+
name: p.name,
|
|
191
|
+
description: p.description
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
{ primitives: primitives }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# === Message Bus ===
|
|
199
|
+
|
|
200
|
+
def get_recent_bus_messages
|
|
201
|
+
entries = @runtime.bus.recent(50)
|
|
202
|
+
|
|
203
|
+
messages = entries.map do |e|
|
|
204
|
+
{
|
|
205
|
+
from: e[:from],
|
|
206
|
+
to: e[:to],
|
|
207
|
+
message: e[:message],
|
|
208
|
+
timestamp: e[:timestamp].iso8601
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
{ messages: messages }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# === Helpers ===
|
|
216
|
+
|
|
217
|
+
def po_summary(po)
|
|
218
|
+
{
|
|
219
|
+
name: po.name,
|
|
220
|
+
description: po.description,
|
|
221
|
+
capabilities: po.config["capabilities"] || [],
|
|
222
|
+
session_count: po.list_sessions.size
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def po_full(po)
|
|
227
|
+
{
|
|
228
|
+
name: po.name,
|
|
229
|
+
description: po.description,
|
|
230
|
+
capabilities: po.config["capabilities"] || [],
|
|
231
|
+
body: po.body,
|
|
232
|
+
config: po.config,
|
|
233
|
+
sessions: po.list_sessions.map do |s|
|
|
234
|
+
{
|
|
235
|
+
id: s[:id],
|
|
236
|
+
name: s[:name],
|
|
237
|
+
message_count: s[:message_count] || 0
|
|
238
|
+
}
|
|
239
|
+
end,
|
|
240
|
+
current_session: po.session_id,
|
|
241
|
+
history: po.history.map { |m| format_history_message(m) }
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def format_message(msg)
|
|
246
|
+
{
|
|
247
|
+
role: msg[:role].to_s,
|
|
248
|
+
content: msg[:content],
|
|
249
|
+
from_po: msg[:from_po],
|
|
250
|
+
tool_calls: msg[:tool_calls],
|
|
251
|
+
tool_results: msg[:tool_results],
|
|
252
|
+
created_at: msg[:created_at]&.iso8601
|
|
253
|
+
}.compact
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def format_history_message(msg)
|
|
257
|
+
case msg[:role]
|
|
258
|
+
when :user
|
|
259
|
+
{ role: "user", content: msg[:content], from: msg[:from] }
|
|
260
|
+
when :assistant
|
|
261
|
+
h = { role: "assistant", content: msg[:content] }
|
|
262
|
+
if msg[:tool_calls]
|
|
263
|
+
h[:tool_calls] = msg[:tool_calls].map do |tc|
|
|
264
|
+
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
h
|
|
268
|
+
when :tool
|
|
269
|
+
{ role: "tool", results: msg[:results] }
|
|
270
|
+
else
|
|
271
|
+
{ role: msg[:role].to_s, content: msg[:content] }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def path_param(path, index)
|
|
276
|
+
# Extract path parameter from regex match
|
|
277
|
+
# /prompt_objects/foo -> foo (index 1)
|
|
278
|
+
# /prompt_objects/foo/sessions/bar -> foo (1), bar (2)
|
|
279
|
+
parts = path.split("/").reject(&:empty?)
|
|
280
|
+
parts[index]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def json_response(data, status: 200)
|
|
284
|
+
body = JSON.generate(data)
|
|
285
|
+
[
|
|
286
|
+
status,
|
|
287
|
+
{
|
|
288
|
+
"content-type" => "application/json",
|
|
289
|
+
"access-control-allow-origin" => "*"
|
|
290
|
+
},
|
|
291
|
+
[body]
|
|
292
|
+
]
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/websocket/adapters/rack"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rack"
|
|
6
|
+
|
|
7
|
+
module PromptObjects
|
|
8
|
+
module Server
|
|
9
|
+
# Main Rack application for the PromptObjects web server.
|
|
10
|
+
# Routes requests to WebSocket handler, API, or static assets.
|
|
11
|
+
class App
|
|
12
|
+
STATIC_EXTENSIONS = %w[.html .js .css .png .svg .ico .woff .woff2 .map .json].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :connections
|
|
15
|
+
|
|
16
|
+
def initialize(runtime)
|
|
17
|
+
@runtime = runtime
|
|
18
|
+
@api = API::Routes.new(runtime)
|
|
19
|
+
@public_path = File.expand_path("public", __dir__)
|
|
20
|
+
@connections = []
|
|
21
|
+
@connections_mutex = Mutex.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
request_path = env["PATH_INFO"]
|
|
26
|
+
|
|
27
|
+
if websocket_request?(env)
|
|
28
|
+
handle_websocket(env)
|
|
29
|
+
elsif request_path.start_with?("/api/")
|
|
30
|
+
@api.call(env)
|
|
31
|
+
elsif static_asset?(request_path)
|
|
32
|
+
serve_static(request_path)
|
|
33
|
+
else
|
|
34
|
+
serve_index
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Broadcast a message to all connected WebSocket clients.
|
|
39
|
+
# @param message [Hash] Message to broadcast
|
|
40
|
+
def broadcast(message)
|
|
41
|
+
@connections_mutex.synchronize do
|
|
42
|
+
@connections.each do |handler|
|
|
43
|
+
handler.send_message(message)
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
puts "Broadcast error: #{e.message}" if ENV["DEBUG"]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Register a connection handler.
|
|
51
|
+
def register_connection(handler)
|
|
52
|
+
@connections_mutex.synchronize do
|
|
53
|
+
@connections << handler
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Unregister a connection handler.
|
|
58
|
+
def unregister_connection(handler)
|
|
59
|
+
@connections_mutex.synchronize do
|
|
60
|
+
@connections.delete(handler)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def websocket_request?(env)
|
|
67
|
+
env["HTTP_UPGRADE"]&.downcase == "websocket"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_websocket(env)
|
|
71
|
+
Async::WebSocket::Adapters::Rack.open(env, protocols: ["json"]) do |connection|
|
|
72
|
+
handler = WebSocketHandler.new(
|
|
73
|
+
runtime: @runtime,
|
|
74
|
+
connection: connection,
|
|
75
|
+
app: self
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
register_connection(handler)
|
|
79
|
+
begin
|
|
80
|
+
handler.run
|
|
81
|
+
ensure
|
|
82
|
+
unregister_connection(handler)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def static_asset?(path)
|
|
88
|
+
STATIC_EXTENSIONS.any? { |ext| path.end_with?(ext) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serve_static(path)
|
|
92
|
+
# Security: prevent directory traversal
|
|
93
|
+
safe_path = File.expand_path(File.join(@public_path, path))
|
|
94
|
+
unless safe_path.start_with?(@public_path)
|
|
95
|
+
return [403, { "content-type" => "text/plain" }, ["Forbidden"]]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if File.exist?(safe_path) && File.file?(safe_path)
|
|
99
|
+
content_type = content_type_for(path)
|
|
100
|
+
body = File.read(safe_path)
|
|
101
|
+
[200, { "content-type" => content_type }, [body]]
|
|
102
|
+
else
|
|
103
|
+
[404, { "content-type" => "text/plain" }, ["Not found"]]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def serve_index
|
|
108
|
+
index_path = File.join(@public_path, "index.html")
|
|
109
|
+
|
|
110
|
+
if File.exist?(index_path)
|
|
111
|
+
body = File.read(index_path)
|
|
112
|
+
[200, { "content-type" => "text/html" }, [body]]
|
|
113
|
+
else
|
|
114
|
+
# If no frontend is built yet, serve a placeholder
|
|
115
|
+
[200, { "content-type" => "text/html" }, [placeholder_html]]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def content_type_for(path)
|
|
120
|
+
case File.extname(path)
|
|
121
|
+
when ".html" then "text/html"
|
|
122
|
+
when ".js" then "application/javascript"
|
|
123
|
+
when ".css" then "text/css"
|
|
124
|
+
when ".json" then "application/json"
|
|
125
|
+
when ".svg" then "image/svg+xml"
|
|
126
|
+
when ".png" then "image/png"
|
|
127
|
+
when ".ico" then "image/x-icon"
|
|
128
|
+
when ".woff" then "font/woff"
|
|
129
|
+
when ".woff2" then "font/woff2"
|
|
130
|
+
when ".map" then "application/json"
|
|
131
|
+
else "application/octet-stream"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def placeholder_html
|
|
136
|
+
<<~HTML
|
|
137
|
+
<!DOCTYPE html>
|
|
138
|
+
<html>
|
|
139
|
+
<head>
|
|
140
|
+
<title>PromptObjects</title>
|
|
141
|
+
<style>
|
|
142
|
+
body {
|
|
143
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
144
|
+
max-width: 600px;
|
|
145
|
+
margin: 100px auto;
|
|
146
|
+
padding: 20px;
|
|
147
|
+
background: #1a1a2e;
|
|
148
|
+
color: #eee;
|
|
149
|
+
}
|
|
150
|
+
h1 { color: #7c3aed; }
|
|
151
|
+
code {
|
|
152
|
+
background: #2d2d44;
|
|
153
|
+
padding: 2px 6px;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
}
|
|
156
|
+
.status { color: #22c55e; }
|
|
157
|
+
</style>
|
|
158
|
+
</head>
|
|
159
|
+
<body>
|
|
160
|
+
<h1>PromptObjects</h1>
|
|
161
|
+
<p class="status">Server is running</p>
|
|
162
|
+
<p>Environment: <code>#{@runtime.name}</code></p>
|
|
163
|
+
<p>Prompt Objects: <code>#{@runtime.registry.prompt_objects.size}</code></p>
|
|
164
|
+
<p>WebSocket endpoint: <code>ws://localhost:PORT/</code></p>
|
|
165
|
+
<hr>
|
|
166
|
+
<p>The React frontend has not been built yet.</p>
|
|
167
|
+
<p>For now, you can connect via WebSocket to interact with POs.</p>
|
|
168
|
+
</body>
|
|
169
|
+
</html>
|
|
170
|
+
HTML
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "listen"
|
|
4
|
+
|
|
5
|
+
module PromptObjects
|
|
6
|
+
module Server
|
|
7
|
+
# Watches the environment directory for file changes and notifies subscribers.
|
|
8
|
+
# This enables live updates when POs are created/modified/deleted.
|
|
9
|
+
class FileWatcher
|
|
10
|
+
def initialize(runtime:, env_path:)
|
|
11
|
+
@runtime = runtime
|
|
12
|
+
@env_path = env_path
|
|
13
|
+
@subscribers = []
|
|
14
|
+
@listener = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start
|
|
18
|
+
objects_dir = File.join(@env_path, "objects")
|
|
19
|
+
|
|
20
|
+
unless Dir.exist?(objects_dir)
|
|
21
|
+
puts "FileWatcher: objects directory not found at #{objects_dir}"
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@listener = Listen.to(objects_dir, only: /\.md$/) do |modified, added, removed|
|
|
26
|
+
handle_changes(modified: modified, added: added, removed: removed)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@listener.start
|
|
30
|
+
puts "FileWatcher: watching #{objects_dir} for changes"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop
|
|
34
|
+
@listener&.stop
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def subscribe(&block)
|
|
38
|
+
@subscribers << block
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unsubscribe(block)
|
|
42
|
+
@subscribers.delete(block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def handle_changes(modified:, added:, removed:)
|
|
48
|
+
# Handle added files
|
|
49
|
+
added.each { |path| handle_po_added(path) }
|
|
50
|
+
|
|
51
|
+
# Handle modified files
|
|
52
|
+
modified.each { |path| handle_po_modified(path) }
|
|
53
|
+
|
|
54
|
+
# Handle removed files
|
|
55
|
+
removed.each { |path| handle_po_removed(path) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handle_po_added(path)
|
|
59
|
+
name = File.basename(path, ".md")
|
|
60
|
+
|
|
61
|
+
# Skip if already loaded (e.g., by create_capability)
|
|
62
|
+
# The on_po_registered callback will have already broadcast
|
|
63
|
+
if @runtime.registry.exists?(name)
|
|
64
|
+
puts "FileWatcher: PO already loaded - #{name} (skipping)"
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "FileWatcher: PO added - #{name}"
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
po = @runtime.load_prompt_object(path)
|
|
72
|
+
@runtime.load_dependencies(po)
|
|
73
|
+
notify(:po_added, po)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
puts "FileWatcher: Failed to load #{name}: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_po_modified(path)
|
|
80
|
+
name = File.basename(path, ".md")
|
|
81
|
+
puts "FileWatcher: PO modified - #{name}"
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
# Remove the old version from registry
|
|
85
|
+
@runtime.registry.unregister(name)
|
|
86
|
+
|
|
87
|
+
# Load the new version
|
|
88
|
+
po = @runtime.load_prompt_object(path)
|
|
89
|
+
@runtime.load_dependencies(po)
|
|
90
|
+
notify(:po_modified, po)
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
puts "FileWatcher: Failed to reload #{name}: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_po_removed(path)
|
|
97
|
+
name = File.basename(path, ".md")
|
|
98
|
+
puts "FileWatcher: PO removed - #{name}"
|
|
99
|
+
|
|
100
|
+
removed = @runtime.registry.unregister(name)
|
|
101
|
+
notify(:po_removed, { name: name }) if removed
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def notify(event, data)
|
|
105
|
+
@subscribers.each do |subscriber|
|
|
106
|
+
subscriber.call(event, data)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
puts "FileWatcher subscriber error: #{e.message}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|