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,524 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module PromptObjects
|
|
7
|
+
module Connectors
|
|
8
|
+
# MCP (Model Context Protocol) connector for exposing environments to
|
|
9
|
+
# Claude Desktop, Cursor, and other MCP clients.
|
|
10
|
+
class MCP < Base
|
|
11
|
+
attr_reader :mcp_server
|
|
12
|
+
|
|
13
|
+
def source_name
|
|
14
|
+
"mcp"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start
|
|
18
|
+
@running = true
|
|
19
|
+
setup_mcp_server
|
|
20
|
+
run_stdio_transport
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stop
|
|
24
|
+
@running = false
|
|
25
|
+
# MCP stdio transport doesn't have a clean shutdown, process exit handles it
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def setup_mcp_server
|
|
31
|
+
@mcp_server = ::MCP::Server.new(
|
|
32
|
+
name: "prompt_objects",
|
|
33
|
+
version: "0.1.0",
|
|
34
|
+
tools: build_tools,
|
|
35
|
+
server_context: {
|
|
36
|
+
env: @runtime,
|
|
37
|
+
context: @runtime.context(tui_mode: true),
|
|
38
|
+
connector: self
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
setup_resource_handlers
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_tools
|
|
46
|
+
[
|
|
47
|
+
Tools::ListPromptObjects,
|
|
48
|
+
Tools::SendMessage,
|
|
49
|
+
Tools::GetConversation,
|
|
50
|
+
Tools::ListSessions,
|
|
51
|
+
Tools::GetPendingRequests,
|
|
52
|
+
Tools::RespondToRequest,
|
|
53
|
+
Tools::InspectPO
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def setup_resource_handlers
|
|
58
|
+
@mcp_server.resources_read_handler do |params|
|
|
59
|
+
handle_resource_read(params)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def handle_resource_read(params)
|
|
64
|
+
uri = params[:uri]
|
|
65
|
+
|
|
66
|
+
case uri
|
|
67
|
+
when %r{^po://([^/]+)/conversation$}
|
|
68
|
+
po_name = ::Regexp.last_match(1)
|
|
69
|
+
read_conversation(po_name)
|
|
70
|
+
when %r{^po://([^/]+)/config$}
|
|
71
|
+
po_name = ::Regexp.last_match(1)
|
|
72
|
+
read_config(po_name)
|
|
73
|
+
when %r{^po://([^/]+)/prompt$}
|
|
74
|
+
po_name = ::Regexp.last_match(1)
|
|
75
|
+
read_prompt(po_name)
|
|
76
|
+
when "bus://messages"
|
|
77
|
+
read_bus_messages
|
|
78
|
+
when %r{^sessions://([^/]+)$}
|
|
79
|
+
po_name = ::Regexp.last_match(1)
|
|
80
|
+
read_sessions(po_name)
|
|
81
|
+
when "sessions://all"
|
|
82
|
+
read_all_sessions
|
|
83
|
+
else
|
|
84
|
+
[{ uri: uri, mimeType: "text/plain", text: "Unknown resource: #{uri}" }]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_conversation(po_name)
|
|
89
|
+
po = @runtime.registry.get(po_name)
|
|
90
|
+
return [{ uri: "po://#{po_name}/conversation", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
91
|
+
|
|
92
|
+
history = po.history.map do |msg|
|
|
93
|
+
{ role: msg[:role].to_s, content: msg[:content] }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
[{
|
|
97
|
+
uri: "po://#{po_name}/conversation",
|
|
98
|
+
mimeType: "application/json",
|
|
99
|
+
text: JSON.pretty_generate(history)
|
|
100
|
+
}]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def read_config(po_name)
|
|
104
|
+
po = @runtime.registry.get(po_name)
|
|
105
|
+
return [{ uri: "po://#{po_name}/config", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
106
|
+
|
|
107
|
+
[{
|
|
108
|
+
uri: "po://#{po_name}/config",
|
|
109
|
+
mimeType: "application/json",
|
|
110
|
+
text: JSON.pretty_generate(po.config)
|
|
111
|
+
}]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_prompt(po_name)
|
|
115
|
+
po = @runtime.registry.get(po_name)
|
|
116
|
+
return [{ uri: "po://#{po_name}/prompt", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
117
|
+
|
|
118
|
+
[{
|
|
119
|
+
uri: "po://#{po_name}/prompt",
|
|
120
|
+
mimeType: "text/markdown",
|
|
121
|
+
text: po.body
|
|
122
|
+
}]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read_bus_messages
|
|
126
|
+
entries = @runtime.bus.recent(50).map do |entry|
|
|
127
|
+
{
|
|
128
|
+
from: entry[:from],
|
|
129
|
+
to: entry[:to],
|
|
130
|
+
message: entry[:message],
|
|
131
|
+
timestamp: entry[:timestamp]&.iso8601
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[{
|
|
136
|
+
uri: "bus://messages",
|
|
137
|
+
mimeType: "application/json",
|
|
138
|
+
text: JSON.pretty_generate(entries)
|
|
139
|
+
}]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def read_sessions(po_name)
|
|
143
|
+
return [{ uri: "sessions://#{po_name}", mimeType: "text/plain", text: "Sessions not available" }] unless @runtime.session_store
|
|
144
|
+
|
|
145
|
+
sessions = @runtime.session_store.list_sessions(po_name: po_name)
|
|
146
|
+
[{
|
|
147
|
+
uri: "sessions://#{po_name}",
|
|
148
|
+
mimeType: "application/json",
|
|
149
|
+
text: JSON.pretty_generate(sessions)
|
|
150
|
+
}]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def read_all_sessions
|
|
154
|
+
return [{ uri: "sessions://all", mimeType: "text/plain", text: "Sessions not available" }] unless @runtime.session_store
|
|
155
|
+
|
|
156
|
+
sessions = @runtime.session_store.list_all_sessions
|
|
157
|
+
[{
|
|
158
|
+
uri: "sessions://all",
|
|
159
|
+
mimeType: "application/json",
|
|
160
|
+
text: JSON.pretty_generate(sessions)
|
|
161
|
+
}]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def run_stdio_transport
|
|
165
|
+
transport = ::MCP::Server::Transports::StdioTransport.new(@mcp_server)
|
|
166
|
+
transport.open
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# MCP Tools
|
|
170
|
+
module Tools
|
|
171
|
+
# List all prompt objects in the environment
|
|
172
|
+
class ListPromptObjects < ::MCP::Tool
|
|
173
|
+
tool_name "list_prompt_objects"
|
|
174
|
+
description "List all available prompt objects in the environment"
|
|
175
|
+
|
|
176
|
+
input_schema(
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {},
|
|
179
|
+
required: []
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def self.call(server_context:)
|
|
183
|
+
env = server_context[:env]
|
|
184
|
+
|
|
185
|
+
pos = env.registry.prompt_objects.map do |po|
|
|
186
|
+
{
|
|
187
|
+
name: po.name,
|
|
188
|
+
description: po.description,
|
|
189
|
+
state: po.state.to_s,
|
|
190
|
+
capabilities: po.config["capabilities"] || []
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
::MCP::Tool::Response.new([{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: JSON.generate({ prompt_objects: pos })
|
|
197
|
+
}])
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Send a message to a prompt object
|
|
202
|
+
class SendMessage < ::MCP::Tool
|
|
203
|
+
tool_name "send_message"
|
|
204
|
+
description "Send a message to a prompt object. The PO will process it (potentially calling tools) and return a response."
|
|
205
|
+
|
|
206
|
+
input_schema(
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
po_name: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Name of the prompt object to message"
|
|
212
|
+
},
|
|
213
|
+
message: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "The message to send"
|
|
216
|
+
},
|
|
217
|
+
session_id: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Optional session ID to continue. If not provided, uses or creates a default session."
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
required: %w[po_name message]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def self.call(po_name:, message:, session_id: nil, server_context:)
|
|
226
|
+
env = server_context[:env]
|
|
227
|
+
context = server_context[:context]
|
|
228
|
+
connector = server_context[:connector]
|
|
229
|
+
|
|
230
|
+
po = env.registry.get(po_name)
|
|
231
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
232
|
+
return ::MCP::Tool::Response.new([{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: JSON.generate({ error: "Prompt object '#{po_name}' not found" })
|
|
235
|
+
}])
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Handle session
|
|
239
|
+
current_session_id = setup_session(po, session_id, env, connector)
|
|
240
|
+
|
|
241
|
+
# Log to message bus
|
|
242
|
+
env.bus.publish(from: "mcp_client", to: po_name, message: message)
|
|
243
|
+
|
|
244
|
+
# Set context for this interaction
|
|
245
|
+
context.current_capability = po_name
|
|
246
|
+
po.state = :working
|
|
247
|
+
|
|
248
|
+
begin
|
|
249
|
+
response = po.receive(message, context: context)
|
|
250
|
+
po.state = :idle
|
|
251
|
+
|
|
252
|
+
# Update session source
|
|
253
|
+
connector&.send(:update_session_source, current_session_id) if current_session_id
|
|
254
|
+
|
|
255
|
+
# Log response
|
|
256
|
+
env.bus.publish(from: po_name, to: "mcp_client", message: response)
|
|
257
|
+
|
|
258
|
+
::MCP::Tool::Response.new([{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: JSON.generate({
|
|
261
|
+
po_name: po_name,
|
|
262
|
+
response: response,
|
|
263
|
+
session_id: current_session_id,
|
|
264
|
+
history_length: po.history.length
|
|
265
|
+
})
|
|
266
|
+
}])
|
|
267
|
+
rescue StandardError => e
|
|
268
|
+
po.state = :idle
|
|
269
|
+
::MCP::Tool::Response.new([{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: JSON.generate({ error: e.message, backtrace: e.backtrace.first(5) })
|
|
272
|
+
}])
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.setup_session(po, session_id, env, connector)
|
|
277
|
+
return nil unless env.session_store
|
|
278
|
+
|
|
279
|
+
if session_id
|
|
280
|
+
# Switch to specified session
|
|
281
|
+
session = env.session_store.get_session(session_id)
|
|
282
|
+
if session && session[:po_name] == po.name
|
|
283
|
+
po.switch_session(session_id)
|
|
284
|
+
return session_id
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Use existing session or create new one
|
|
289
|
+
existing_id = po.instance_variable_get(:@session_id)
|
|
290
|
+
return existing_id if existing_id
|
|
291
|
+
|
|
292
|
+
# Create new MCP session
|
|
293
|
+
new_id = env.session_store.create_session(
|
|
294
|
+
po_name: po.name,
|
|
295
|
+
name: "MCP Session #{Time.now.strftime('%H:%M')}",
|
|
296
|
+
source: connector&.source_name || "mcp"
|
|
297
|
+
)
|
|
298
|
+
po.instance_variable_set(:@session_id, new_id)
|
|
299
|
+
po.send(:reload_history_from_session) if po.respond_to?(:reload_history_from_session, true)
|
|
300
|
+
new_id
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Get conversation history for a PO
|
|
305
|
+
class GetConversation < ::MCP::Tool
|
|
306
|
+
tool_name "get_conversation"
|
|
307
|
+
description "Get the conversation history for a prompt object"
|
|
308
|
+
|
|
309
|
+
input_schema(
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
po_name: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Name of the prompt object"
|
|
315
|
+
},
|
|
316
|
+
session_id: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description: "Optional session ID. If not provided, returns current session."
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
required: %w[po_name]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def self.call(po_name:, session_id: nil, server_context:)
|
|
325
|
+
env = server_context[:env]
|
|
326
|
+
|
|
327
|
+
po = env.registry.get(po_name)
|
|
328
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
329
|
+
return ::MCP::Tool::Response.new([{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: JSON.generate({ error: "Prompt object '#{po_name}' not found" })
|
|
332
|
+
}])
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
history = if session_id && env.session_store
|
|
336
|
+
messages = env.session_store.get_messages(session_id)
|
|
337
|
+
messages.map { |m| { role: m[:role], content: m[:content] } }
|
|
338
|
+
else
|
|
339
|
+
po.history.map { |msg| { role: msg[:role].to_s, content: msg[:content] } }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
::MCP::Tool::Response.new([{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: JSON.generate({
|
|
345
|
+
po_name: po_name,
|
|
346
|
+
session_id: session_id || po.instance_variable_get(:@session_id),
|
|
347
|
+
history: history
|
|
348
|
+
})
|
|
349
|
+
}])
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# List sessions for a PO or all sessions
|
|
354
|
+
class ListSessions < ::MCP::Tool
|
|
355
|
+
tool_name "list_sessions"
|
|
356
|
+
description "List sessions. Optionally filter by PO name or source."
|
|
357
|
+
|
|
358
|
+
input_schema(
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: {
|
|
361
|
+
po_name: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "Filter by prompt object name (optional)"
|
|
364
|
+
},
|
|
365
|
+
source: {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "Filter by source: tui, mcp, api, web (optional)"
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
required: []
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def self.call(po_name: nil, source: nil, server_context:)
|
|
374
|
+
env = server_context[:env]
|
|
375
|
+
|
|
376
|
+
unless env.session_store
|
|
377
|
+
return ::MCP::Tool::Response.new([{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: JSON.generate({ error: "Sessions not available in this environment" })
|
|
380
|
+
}])
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
sessions = if po_name
|
|
384
|
+
env.session_store.list_sessions(po_name: po_name)
|
|
385
|
+
else
|
|
386
|
+
env.session_store.list_all_sessions(source: source)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
sessions_data = sessions.map do |s|
|
|
390
|
+
{
|
|
391
|
+
id: s[:id],
|
|
392
|
+
po_name: s[:po_name],
|
|
393
|
+
name: s[:name],
|
|
394
|
+
source: s[:source],
|
|
395
|
+
message_count: env.session_store.message_count(s[:id]),
|
|
396
|
+
created_at: s[:created_at]&.iso8601,
|
|
397
|
+
updated_at: s[:updated_at]&.iso8601
|
|
398
|
+
}
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
::MCP::Tool::Response.new([{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: JSON.generate({ sessions: sessions_data })
|
|
404
|
+
}])
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Get pending human requests
|
|
409
|
+
class GetPendingRequests < ::MCP::Tool
|
|
410
|
+
tool_name "get_pending_requests"
|
|
411
|
+
description "Get all pending human requests across all prompt objects"
|
|
412
|
+
|
|
413
|
+
input_schema(
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {},
|
|
416
|
+
required: []
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def self.call(server_context:)
|
|
420
|
+
env = server_context[:env]
|
|
421
|
+
|
|
422
|
+
requests = env.human_queue.pending.map do |req|
|
|
423
|
+
{
|
|
424
|
+
id: req[:id],
|
|
425
|
+
from: req[:from],
|
|
426
|
+
question: req[:question],
|
|
427
|
+
timestamp: req[:timestamp]&.iso8601
|
|
428
|
+
}
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
::MCP::Tool::Response.new([{
|
|
432
|
+
type: "text",
|
|
433
|
+
text: JSON.generate({ pending_requests: requests })
|
|
434
|
+
}])
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Respond to a human request
|
|
439
|
+
class RespondToRequest < ::MCP::Tool
|
|
440
|
+
tool_name "respond_to_request"
|
|
441
|
+
description "Respond to a pending human request"
|
|
442
|
+
|
|
443
|
+
input_schema(
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: {
|
|
446
|
+
request_id: {
|
|
447
|
+
type: "string",
|
|
448
|
+
description: "ID of the request to respond to"
|
|
449
|
+
},
|
|
450
|
+
response: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "The response to provide"
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
required: %w[request_id response]
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def self.call(request_id:, response:, server_context:)
|
|
459
|
+
env = server_context[:env]
|
|
460
|
+
|
|
461
|
+
success = env.human_queue.respond(request_id, response)
|
|
462
|
+
|
|
463
|
+
if success
|
|
464
|
+
::MCP::Tool::Response.new([{
|
|
465
|
+
type: "text",
|
|
466
|
+
text: JSON.generate({ success: true, request_id: request_id })
|
|
467
|
+
}])
|
|
468
|
+
else
|
|
469
|
+
::MCP::Tool::Response.new([{
|
|
470
|
+
type: "text",
|
|
471
|
+
text: JSON.generate({ error: "Request not found or already responded", request_id: request_id })
|
|
472
|
+
}])
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Inspect a prompt object's details
|
|
478
|
+
class InspectPO < ::MCP::Tool
|
|
479
|
+
tool_name "inspect_po"
|
|
480
|
+
description "Get detailed information about a prompt object including its prompt and capabilities"
|
|
481
|
+
|
|
482
|
+
input_schema(
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
po_name: {
|
|
486
|
+
type: "string",
|
|
487
|
+
description: "Name of the prompt object to inspect"
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
required: %w[po_name]
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def self.call(po_name:, server_context:)
|
|
494
|
+
env = server_context[:env]
|
|
495
|
+
|
|
496
|
+
po = env.registry.get(po_name)
|
|
497
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
498
|
+
return ::MCP::Tool::Response.new([{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: JSON.generate({ error: "Prompt object '#{po_name}' not found" })
|
|
501
|
+
}])
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
info = {
|
|
505
|
+
name: po.name,
|
|
506
|
+
description: po.description,
|
|
507
|
+
state: po.state.to_s,
|
|
508
|
+
prompt: po.body,
|
|
509
|
+
capabilities: po.config["capabilities"] || [],
|
|
510
|
+
config: po.config,
|
|
511
|
+
session_id: po.instance_variable_get(:@session_id),
|
|
512
|
+
history_length: po.history.length
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
::MCP::Tool::Response.new([{
|
|
516
|
+
type: "text",
|
|
517
|
+
text: JSON.generate(info)
|
|
518
|
+
}])
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Env
|
|
5
|
+
# Exports an environment as a git bundle (.poenv file).
|
|
6
|
+
# The bundle contains all commits, objects, primitives, and manifest.
|
|
7
|
+
# Sessions are NOT included (private data).
|
|
8
|
+
class Exporter
|
|
9
|
+
# @param env_path [String] Path to the environment directory
|
|
10
|
+
def initialize(env_path)
|
|
11
|
+
@env_path = env_path
|
|
12
|
+
@name = File.basename(env_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Export the environment to a git bundle.
|
|
16
|
+
# @param output_path [String] Path for the output .poenv file
|
|
17
|
+
# @param commit_message [String, nil] Message for any uncommitted changes
|
|
18
|
+
# @return [Hash] Export result with :success, :path, :stats
|
|
19
|
+
def export(output_path, commit_message: nil)
|
|
20
|
+
validate_environment!
|
|
21
|
+
|
|
22
|
+
# Ensure we have a clean state
|
|
23
|
+
commit_changes(commit_message) if Git.dirty?(@env_path)
|
|
24
|
+
|
|
25
|
+
# Create the bundle
|
|
26
|
+
output_path = normalize_output_path(output_path)
|
|
27
|
+
success = Git.bundle(@env_path, output_path)
|
|
28
|
+
|
|
29
|
+
unless success
|
|
30
|
+
return { success: false, error: "Failed to create git bundle" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
success: true,
|
|
35
|
+
path: output_path,
|
|
36
|
+
stats: gather_stats
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def validate_environment!
|
|
43
|
+
unless Dir.exist?(@env_path)
|
|
44
|
+
raise Error, "Environment not found: #{@env_path}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
unless Git.repo?(@env_path)
|
|
48
|
+
raise Error, "Environment is not a git repository: #{@env_path}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Must have at least one commit
|
|
52
|
+
if Git.commit_count(@env_path) == 0
|
|
53
|
+
raise Error, "Environment has no commits. Make at least one change first."
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def commit_changes(message)
|
|
58
|
+
msg = message || "Export preparation"
|
|
59
|
+
Git.commit(@env_path, msg)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def normalize_output_path(path)
|
|
63
|
+
# Add .poenv extension if not present
|
|
64
|
+
path = "#{path}.poenv" unless path.end_with?(".poenv")
|
|
65
|
+
|
|
66
|
+
# Expand to absolute path
|
|
67
|
+
File.expand_path(path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def gather_stats
|
|
71
|
+
objects_dir = File.join(@env_path, "objects")
|
|
72
|
+
primitives_dir = File.join(@env_path, "primitives")
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
name: @name,
|
|
76
|
+
commits: Git.commit_count(@env_path),
|
|
77
|
+
objects: Dir.exist?(objects_dir) ? Dir.glob(File.join(objects_dir, "*.md")).count : 0,
|
|
78
|
+
primitives: Dir.exist?(primitives_dir) ? Dir.glob(File.join(primitives_dir, "*.rb")).count : 0
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|