poetry-agent 0.0.2
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/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/app/javascript/poetry/agent/a2ui_surface_controller.js +141 -0
- data/app/javascript/poetry/agent/adapter.js +77 -0
- data/app/javascript/poetry/agent/agui_client_tool_controller.js +53 -0
- data/app/javascript/poetry/agent/index.js +41 -0
- data/app/javascript/poetry/agent/stream_actions.js +65 -0
- data/app/javascript/poetry/agent/webmcp_controller.js +248 -0
- data/app/javascript/poetry/agent/webmcp_form_controller.js +109 -0
- data/config/controllers_manifest.json +82 -0
- data/config/importmap.rb +10 -0
- data/exe/poetry-agent +28 -0
- data/lib/poetry/agent/a2ui/catalog.rb +289 -0
- data/lib/poetry/agent/a2ui/catalogs/basic.rb +460 -0
- data/lib/poetry/agent/a2ui/catalogs/native.rb +176 -0
- data/lib/poetry/agent/a2ui/checks.rb +45 -0
- data/lib/poetry/agent/a2ui/evaluator.rb +139 -0
- data/lib/poetry/agent/a2ui/expression.rb +175 -0
- data/lib/poetry/agent/a2ui/functions.rb +417 -0
- data/lib/poetry/agent/a2ui/markdown.rb +63 -0
- data/lib/poetry/agent/a2ui/pointer.rb +113 -0
- data/lib/poetry/agent/a2ui/protocol.rb +12 -0
- data/lib/poetry/agent/a2ui/renderer.rb +242 -0
- data/lib/poetry/agent/a2ui/session.rb +302 -0
- data/lib/poetry/agent/a2ui/streams.rb +82 -0
- data/lib/poetry/agent/a2ui/surface.rb +352 -0
- data/lib/poetry/agent/a2ui.rb +48 -0
- data/lib/poetry/agent/agui/client.rb +69 -0
- data/lib/poetry/agent/agui/json_patch.rb +137 -0
- data/lib/poetry/agent/agui/relay.rb +105 -0
- data/lib/poetry/agent/agui/run_input.rb +83 -0
- data/lib/poetry/agent/agui/sse.rb +97 -0
- data/lib/poetry/agent/agui/transcript.rb +540 -0
- data/lib/poetry/agent/agui/turbo_stream.rb +68 -0
- data/lib/poetry/agent/agui.rb +87 -0
- data/lib/poetry/agent/config.rb +49 -0
- data/lib/poetry/agent/engine.rb +37 -0
- data/lib/poetry/agent/mcp/bundled.rb +54 -0
- data/lib/poetry/agent/mcp/http.rb +89 -0
- data/lib/poetry/agent/mcp/server.rb +962 -0
- data/lib/poetry/agent/version.rb +8 -0
- data/lib/poetry/agent/webmcp/origin_trial.rb +49 -0
- data/lib/poetry/agent/webmcp.rb +37 -0
- data/lib/poetry/agent.rb +66 -0
- data/lib/poetry-agent.rb +4 -0
- metadata +117 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { supported, registerTool, validToolName } from "@poetry/agent/adapter"
|
|
3
|
+
|
|
4
|
+
// The registrar: one controller on an opted-in component root
|
|
5
|
+
// (`webmcp: "country"` on the helper call renders it beside the
|
|
6
|
+
// component's own controllers) registers that instance's declared tools
|
|
7
|
+
// with document.modelContext on connect and aborts them on disconnect.
|
|
8
|
+
// Components gain zero runtime code - each tool dispatches to the
|
|
9
|
+
// component's OWN controller action (the `executes` descriptor the Ruby
|
|
10
|
+
// contract validated at class load), passing the tool's parameters
|
|
11
|
+
// positionally in declared order.
|
|
12
|
+
//
|
|
13
|
+
// Correctness rules the spec makes load-bearing:
|
|
14
|
+
// - Re-registration is skipped while the payload is unchanged (the spec
|
|
15
|
+
// documents an unregister/quick-re-register race where in-flight args
|
|
16
|
+
// for the old tool can hit the new tool's schema).
|
|
17
|
+
// - Never register under Turbo's cache preview.
|
|
18
|
+
// - Duplicate names are rejected by the browser; we warn and skip.
|
|
19
|
+
// - A per-document budget caps registrations (each tool costs the agent
|
|
20
|
+
// context; overlap confuses tool choice).
|
|
21
|
+
// - Errors come back as descriptive result strings (granular exceptions
|
|
22
|
+
// are still open spec issues; a string lets the agent self-correct):
|
|
23
|
+
// a missing or unknown parameter, a value of the wrong type or outside
|
|
24
|
+
// the enum, a missing action, a throwing action.
|
|
25
|
+
// - A result is the action's return value when it is JSON-serializable
|
|
26
|
+
// (the contract's actions return their resulting state, so an answer
|
|
27
|
+
// says what happened rather than "done"); the done marker covers
|
|
28
|
+
// actions that return nothing.
|
|
29
|
+
// - Parameters map positionally onto the action in declared order; the
|
|
30
|
+
// execute callback's {signal} is not forwarded (the actions are
|
|
31
|
+
// synchronous UI operations).
|
|
32
|
+
|
|
33
|
+
// element -> { hash, controller: AbortController, names: string[] }
|
|
34
|
+
const registrations = new Map()
|
|
35
|
+
|
|
36
|
+
// element -> the registrar instance, for every connected root whether or
|
|
37
|
+
// not the browser exposes modelContext: in-page callers (the AG-UI
|
|
38
|
+
// client-tool bridge) execute a declared tool by its registered name.
|
|
39
|
+
const instances = new Map()
|
|
40
|
+
|
|
41
|
+
const registeredCount = () =>
|
|
42
|
+
[...registrations.values()].reduce((sum, entry) => sum + entry.names.length, 0)
|
|
43
|
+
|
|
44
|
+
export default class extends Controller {
|
|
45
|
+
static values = {
|
|
46
|
+
name: String,
|
|
47
|
+
tools: Array,
|
|
48
|
+
budget: { type: Number, default: 20 }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static events = [
|
|
52
|
+
"poetry:webmcp:registered",
|
|
53
|
+
"poetry:webmcp:executed",
|
|
54
|
+
"poetry:webmcp:unregistered"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
connect() {
|
|
58
|
+
instances.set(this.element, this)
|
|
59
|
+
this.register()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
disconnect() {
|
|
63
|
+
instances.delete(this.element)
|
|
64
|
+
this.unregister()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
nameValueChanged() {
|
|
68
|
+
if (this.#connected) this.register()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
toolsValueChanged() {
|
|
72
|
+
if (this.#connected) this.register()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Registers this instance's tools; idempotent for an unchanged payload.
|
|
76
|
+
register() {
|
|
77
|
+
this.#connected = true
|
|
78
|
+
if (!supported()) return
|
|
79
|
+
if (document.documentElement.hasAttribute("data-turbo-preview")) return
|
|
80
|
+
if (!this.nameValue || !validToolName(this.nameValue)) {
|
|
81
|
+
console.warn(`[poetry-agent] webmcp: invalid instance name ${JSON.stringify(this.nameValue)}`)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hash = JSON.stringify([this.nameValue, this.toolsValue])
|
|
86
|
+
const existing = registrations.get(this.element)
|
|
87
|
+
if (existing?.hash === hash) return
|
|
88
|
+
if (existing) this.unregister()
|
|
89
|
+
|
|
90
|
+
const controller = new AbortController()
|
|
91
|
+
const entry = { hash, controller, names: [] }
|
|
92
|
+
registrations.set(this.element, entry)
|
|
93
|
+
const pending = []
|
|
94
|
+
|
|
95
|
+
for (const tool of this.toolsValue) {
|
|
96
|
+
const name = `poetry.${this.nameValue}.${tool.name}`
|
|
97
|
+
if (!validToolName(name)) {
|
|
98
|
+
console.warn(`[poetry-agent] webmcp: skipping invalid tool name ${name}`)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
if (registeredCount() >= this.budgetValue) {
|
|
102
|
+
console.warn(`[poetry-agent] webmcp: registration budget (${this.budgetValue}) reached; skipping ${name}`)
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const definition = {
|
|
107
|
+
name,
|
|
108
|
+
description: tool.description,
|
|
109
|
+
annotations: tool.annotations,
|
|
110
|
+
execute: (args) => this.#execute(tool, args ?? {})
|
|
111
|
+
}
|
|
112
|
+
if (tool.title) definition.title = tool.title
|
|
113
|
+
if (tool.inputSchema) definition.inputSchema = tool.inputSchema
|
|
114
|
+
|
|
115
|
+
entry.names.push(name)
|
|
116
|
+
pending.push(registerTool(definition, { signal: controller.signal }).catch((error) => {
|
|
117
|
+
entry.names = entry.names.filter((registered) => registered !== name)
|
|
118
|
+
console.warn(`[poetry-agent] webmcp: could not register ${name}: ${error?.message ?? error}`)
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The registered event carries what the browser ACCEPTED, so it fires
|
|
123
|
+
// once every registration settled (and not at all if this instance
|
|
124
|
+
// unregistered meanwhile).
|
|
125
|
+
Promise.allSettled(pending).then(() => {
|
|
126
|
+
if (registrations.get(this.element) !== entry) return
|
|
127
|
+
this.dispatch("registered", { prefix: "poetry:webmcp", detail: { name: this.nameValue, tools: [...entry.names] } })
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Executes one of this instance's declared tools by its full registered
|
|
132
|
+
// name (`poetry.{instance}.{tool}`) or its bare tool name, with the same
|
|
133
|
+
// validation and dispatch a WebMCP call takes; unknown names answer with
|
|
134
|
+
// an error string like any other problem.
|
|
135
|
+
execute(name, args = {}) {
|
|
136
|
+
const tool = this.toolsValue.find((candidate) =>
|
|
137
|
+
`poetry.${this.nameValue}.${candidate.name}` === name || candidate.name === name)
|
|
138
|
+
if (!tool) return Promise.resolve(`Error: no tool named ${name} on ${this.nameValue}`)
|
|
139
|
+
return this.#execute(tool, args ?? {})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Aborts every registration of this instance.
|
|
143
|
+
unregister() {
|
|
144
|
+
const entry = registrations.get(this.element)
|
|
145
|
+
if (!entry) return
|
|
146
|
+
|
|
147
|
+
registrations.delete(this.element)
|
|
148
|
+
entry.controller.abort()
|
|
149
|
+
this.dispatch("unregistered", { prefix: "poetry:webmcp", detail: { name: this.nameValue, tools: entry.names } })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async #execute(tool, args) {
|
|
153
|
+
const [identifier, method] = String(tool.executes).split("#")
|
|
154
|
+
const target = this.application.getControllerForElementAndIdentifier(this.element, identifier)
|
|
155
|
+
if (!target || typeof target[method] !== "function") {
|
|
156
|
+
return `Error: ${tool.name} cannot run - no ${identifier}#${method} on this element`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const problem = validate(tool, args)
|
|
160
|
+
if (problem) return problem
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const positional = Object.keys(tool.inputSchema?.properties ?? {}).map((key) => args[key])
|
|
164
|
+
const result = await target[method](...positional)
|
|
165
|
+
const value = serializable(result) ? result : `${tool.name}: done`
|
|
166
|
+
this.dispatch("executed", { prefix: "poetry:webmcp", detail: { tool: tool.name, args, result: value } })
|
|
167
|
+
return value ?? `${tool.name}: done`
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return `Error: ${tool.name} failed - ${error?.message ?? error}`
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#connected = false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Strict validation in code, loose in schema (Chrome's rule): the schema
|
|
177
|
+
// is a hint the agent may miss, so every call is checked here and answered
|
|
178
|
+
// with a string it can act on - which parameter, what it takes.
|
|
179
|
+
const validate = (tool, args) => {
|
|
180
|
+
const schema = tool.inputSchema ?? {}
|
|
181
|
+
const properties = schema.properties ?? {}
|
|
182
|
+
const missing = (schema.required ?? []).filter((key) => args[key] === undefined)
|
|
183
|
+
if (missing.length > 0) return `Error: missing required parameter(s) ${missing.join(", ")}`
|
|
184
|
+
|
|
185
|
+
if (schema.additionalProperties === false) {
|
|
186
|
+
const unknown = Object.keys(args).filter((key) => !(key in properties))
|
|
187
|
+
if (unknown.length > 0) {
|
|
188
|
+
const takes = Object.keys(properties).join(", ") || "no parameters"
|
|
189
|
+
return `Error: unknown parameter(s) ${unknown.join(", ")} - ${tool.name} takes ${takes}`
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [key, spec] of Object.entries(properties)) {
|
|
194
|
+
const value = args[key]
|
|
195
|
+
if (value === undefined) continue
|
|
196
|
+
if (!matchesType(value, spec.type)) return `Error: ${key} must be ${describeType(spec.type)}`
|
|
197
|
+
if (spec.enum && !spec.enum.includes(value)) return `Error: ${key} must be one of ${spec.enum.join(", ")}`
|
|
198
|
+
}
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const matchesType = (value, type) => {
|
|
203
|
+
if (!type) return true
|
|
204
|
+
const types = Array.isArray(type) ? type : [type]
|
|
205
|
+
return types.some((expected) => {
|
|
206
|
+
switch (expected) {
|
|
207
|
+
case "string": return typeof value === "string"
|
|
208
|
+
case "number": return typeof value === "number" && Number.isFinite(value)
|
|
209
|
+
case "integer": return Number.isInteger(value)
|
|
210
|
+
case "boolean": return typeof value === "boolean"
|
|
211
|
+
case "array": return Array.isArray(value)
|
|
212
|
+
case "object": return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
213
|
+
case "null": return value === null
|
|
214
|
+
default: return true
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const describeType = (type) => (Array.isArray(type) ? type.join(" or ") : `${/^[aeiou]/.test(type) ? "an" : "a"} ${type}`)
|
|
220
|
+
|
|
221
|
+
// A tool result must survive JSON serialization (the spec stringifies it);
|
|
222
|
+
// DOM objects and undefined collapse to a done-marker instead.
|
|
223
|
+
const serializable = (value) => {
|
|
224
|
+
if (value === undefined || value === null) return false
|
|
225
|
+
if (typeof value === "object" && (value instanceof Node || value instanceof Event)) return false
|
|
226
|
+
try {
|
|
227
|
+
JSON.stringify(value)
|
|
228
|
+
return true
|
|
229
|
+
} catch {
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Executes a declared tool by its full registered name on whichever
|
|
235
|
+
// connected root declares it - the in-page dispatch path (no
|
|
236
|
+
// modelContext needed). Answers an error string when no root does.
|
|
237
|
+
export const executeRegisteredTool = (application, name, args = {}) => {
|
|
238
|
+
for (const [element, controller] of instances) {
|
|
239
|
+
const owns = controller.toolsValue.some((tool) => `poetry.${controller.nameValue}.${tool.name}` === name)
|
|
240
|
+
if (owns && application.getControllerForElementAndIdentifier(element, "poetry--agent--webmcp") === controller) {
|
|
241
|
+
return controller.execute(name, args)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return Promise.resolve(`Error: no registered tool named ${name} on this page`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Test seam: the live registration table.
|
|
248
|
+
export const _registrations = registrations
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// The declarative-form companion: a form declared with poetry_webmcp_form
|
|
4
|
+
// (toolname/tooldescription on the <form>) is registered by the BROWSER;
|
|
5
|
+
// this controller only answers an agent-invoked submit with the outcome.
|
|
6
|
+
// Chrome's SubmitEvent carries agentInvoked + respondWith(promise): we
|
|
7
|
+
// submit the form ourselves (fetch, same method/action, Turbo-Stream
|
|
8
|
+
// accepting) and respond with a short descriptive result instead of
|
|
9
|
+
// navigating, so the agent learns whether the submission succeeded and
|
|
10
|
+
// what the server said (validation errors included - it can self-correct).
|
|
11
|
+
//
|
|
12
|
+
// The person's page then catches up with the answer (a beat after the
|
|
13
|
+
// result is handed to the browser, so a navigation can never swallow
|
|
14
|
+
// it): a GET answer is the page at that URL - a Turbo visit, or a plain
|
|
15
|
+
// navigation without Turbo; a Turbo-Stream answer renders; a redirected
|
|
16
|
+
// POST (redirect-after-create) visits where the redirect went. An HTML
|
|
17
|
+
// re-render of a failed POST stays put - the agent already holds the
|
|
18
|
+
// errors, and the person keeps their filled form.
|
|
19
|
+
//
|
|
20
|
+
// Submits without agentInvoked (a person pressed Submit) pass through
|
|
21
|
+
// untouched: the human path stays the human path.
|
|
22
|
+
export default class extends Controller {
|
|
23
|
+
static events = ["poetry:webmcp:form-submitted"]
|
|
24
|
+
|
|
25
|
+
connect() {
|
|
26
|
+
this.element.addEventListener("submit", this.submit)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnect() {
|
|
30
|
+
this.element.removeEventListener("submit", this.submit)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
submit = (event) => {
|
|
34
|
+
if (!event.agentInvoked || typeof event.respondWith !== "function") return
|
|
35
|
+
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
event.respondWith(this.#deliver())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async #deliver() {
|
|
41
|
+
const form = this.element
|
|
42
|
+
const method = (form.getAttribute("method") || "get").toUpperCase()
|
|
43
|
+
const data = new FormData(form)
|
|
44
|
+
let url = form.action
|
|
45
|
+
const init = { method, headers: { Accept: "text/vnd.turbo-stream.html, text/html, application/json" } }
|
|
46
|
+
|
|
47
|
+
if (method === "GET") {
|
|
48
|
+
const target = new URL(url, document.baseURI)
|
|
49
|
+
for (const [key, value] of data.entries()) target.searchParams.append(key, value)
|
|
50
|
+
url = target.toString()
|
|
51
|
+
} else {
|
|
52
|
+
init.body = data
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(url, init)
|
|
57
|
+
const text = await response.text()
|
|
58
|
+
const outcome = response.ok ? "succeeded" : "failed"
|
|
59
|
+
const summary = `${form.getAttribute("toolname")} ${outcome} (${response.status})${excerpt(text, response.headers.get("content-type") || "")}`
|
|
60
|
+
this.dispatch("form-submitted", { prefix: "poetry:webmcp", detail: { status: response.status, ok: response.ok } })
|
|
61
|
+
setTimeout(() => reflect(method, url, response, text), 0)
|
|
62
|
+
return summary
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return `${form.getAttribute("toolname")} failed - ${error?.message ?? error}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The page shows what the agent read.
|
|
70
|
+
const reflect = (method, url, response, text) => {
|
|
71
|
+
const turbo = window.Turbo
|
|
72
|
+
const type = response.headers.get("content-type") || ""
|
|
73
|
+
|
|
74
|
+
if (type.includes("text/vnd.turbo-stream.html")) {
|
|
75
|
+
turbo?.renderStreamMessage?.(text)
|
|
76
|
+
} else if (method === "GET") {
|
|
77
|
+
if (turbo?.visit) turbo.visit(url, { action: "replace" })
|
|
78
|
+
else window.location.assign(url)
|
|
79
|
+
} else if (response.redirected && response.url) {
|
|
80
|
+
if (turbo?.visit) turbo.visit(response.url)
|
|
81
|
+
else window.location.assign(response.url)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A short excerpt of the response for the agent. HTML answers are parsed:
|
|
86
|
+
// scripts, styles, and chrome are dropped, and a region the app marks
|
|
87
|
+
// `data-webmcp-result` wins over <main>, which wins over the body - so a
|
|
88
|
+
// search page answers with its results, not its navigation.
|
|
89
|
+
const excerpt = (text, contentType = "") => {
|
|
90
|
+
const plain = contentType.includes("html") || /^\s*</.test(text) ? htmlText(text) : text.replace(/\s+/g, " ").trim()
|
|
91
|
+
return plain ? `: ${plain.slice(0, 500)}` : ""
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Declarative tool attributes never reach an inert document: Chrome 151
|
|
95
|
+
// crashes the renderer when a DOMParser (or createHTMLDocument) document
|
|
96
|
+
// holds a <form toolname> - the answer page usually IS the page with the
|
|
97
|
+
// form - so they are stripped from the text before parsing. Turbo's own
|
|
98
|
+
// visit parse adopts the form into the live document and is unaffected.
|
|
99
|
+
export const stripToolAttributes = (html) =>
|
|
100
|
+
html.replace(/\stool(?:name|description|autosubmit|paramdescription)(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?/gi, "")
|
|
101
|
+
|
|
102
|
+
const htmlText = (html) => {
|
|
103
|
+
const doc = new DOMParser().parseFromString(stripToolAttributes(html), "text/html")
|
|
104
|
+
for (const node of doc.querySelectorAll("script, style, noscript, template, nav, header, footer, aside")) node.remove()
|
|
105
|
+
const region = doc.querySelector("[data-webmcp-result]") || doc.querySelector("main") || doc.body
|
|
106
|
+
// Element boundaries become spaces (textContent runs adjacent items together).
|
|
107
|
+
const spaced = new DOMParser().parseFromString((region?.innerHTML || "").replace(/<[^>]+>/g, " "), "text/html")
|
|
108
|
+
return (spaced.body.textContent || "").replace(/\s+/g, " ").trim()
|
|
109
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"poetry--agent--webmcp": {
|
|
3
|
+
"targets": [],
|
|
4
|
+
"values": {
|
|
5
|
+
"name": {
|
|
6
|
+
"type": "String"
|
|
7
|
+
},
|
|
8
|
+
"tools": {
|
|
9
|
+
"type": "Array"
|
|
10
|
+
},
|
|
11
|
+
"budget": {
|
|
12
|
+
"type": "Number",
|
|
13
|
+
"default": 20
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"classes": [],
|
|
17
|
+
"methods": [
|
|
18
|
+
"connect",
|
|
19
|
+
"disconnect",
|
|
20
|
+
"execute",
|
|
21
|
+
"register",
|
|
22
|
+
"unregister"
|
|
23
|
+
],
|
|
24
|
+
"events": [
|
|
25
|
+
"poetry:webmcp:executed",
|
|
26
|
+
"poetry:webmcp:registered",
|
|
27
|
+
"poetry:webmcp:unregistered"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"poetry--agent--webmcp-form": {
|
|
31
|
+
"targets": [],
|
|
32
|
+
"values": {},
|
|
33
|
+
"classes": [],
|
|
34
|
+
"methods": [
|
|
35
|
+
"connect",
|
|
36
|
+
"disconnect"
|
|
37
|
+
],
|
|
38
|
+
"events": [
|
|
39
|
+
"poetry:webmcp:form-submitted"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"poetry--agent--agui-client-tool": {
|
|
43
|
+
"targets": [],
|
|
44
|
+
"values": {
|
|
45
|
+
"call": {
|
|
46
|
+
"type": "Object"
|
|
47
|
+
},
|
|
48
|
+
"url": {
|
|
49
|
+
"type": "String"
|
|
50
|
+
},
|
|
51
|
+
"done": {
|
|
52
|
+
"type": "Boolean"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"classes": [],
|
|
56
|
+
"methods": [
|
|
57
|
+
"connect"
|
|
58
|
+
],
|
|
59
|
+
"events": [
|
|
60
|
+
"poetry:agui:client-tool-executed"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"poetry--agent--a2ui-surface": {
|
|
64
|
+
"targets": [],
|
|
65
|
+
"values": {
|
|
66
|
+
"program": {
|
|
67
|
+
"type": "Object"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"classes": [],
|
|
71
|
+
"methods": [
|
|
72
|
+
"apply",
|
|
73
|
+
"call",
|
|
74
|
+
"connect",
|
|
75
|
+
"evaluate",
|
|
76
|
+
"failure",
|
|
77
|
+
"read",
|
|
78
|
+
"resolve"
|
|
79
|
+
],
|
|
80
|
+
"events": []
|
|
81
|
+
}
|
|
82
|
+
}
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# poetry-agent's importmap pins (the importmap-first channel): the host
|
|
4
|
+
# app's importmap merges these, so `import { registerPoetryAgent } from
|
|
5
|
+
# "@poetry/agent"` works with zero build. Bundler hosts use the
|
|
6
|
+
# @poetry/agent npm package instead - one source, two channels.
|
|
7
|
+
|
|
8
|
+
pin "@poetry/agent", to: "poetry/agent/index.js"
|
|
9
|
+
pin_all_from File.expand_path("../app/javascript/poetry/agent", __dir__),
|
|
10
|
+
under: "@poetry/agent", to: "poetry/agent"
|
data/exe/poetry-agent
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# poetry-agent: the poetry MCP server - newline-delimited JSON-RPC 2.0
|
|
5
|
+
# over stdio, serving list_components / describe_component / check /
|
|
6
|
+
# compose / build_page / get_skill from the committed registry. Boot-free:
|
|
7
|
+
# the gems load once per agent session (~0.4s); the Rails app never boots,
|
|
8
|
+
# so every call after that is milliseconds.
|
|
9
|
+
#
|
|
10
|
+
# Usage: poetry-agent [registry-root]
|
|
11
|
+
# registry-root defaults to the bundled poetry-ui gem. In .mcp.json:
|
|
12
|
+
# {"mcpServers": {"poetry": {"command": "bundle", "args": ["exec", "poetry-agent"]}}}
|
|
13
|
+
#
|
|
14
|
+
# One gem ships this exe (poetry-agent), so the surface is the same
|
|
15
|
+
# wherever the binstub resolves - the old core/ui exe-parity doctrine is
|
|
16
|
+
# retired by construction.
|
|
17
|
+
|
|
18
|
+
require "poetry/agent"
|
|
19
|
+
|
|
20
|
+
# One assembly for the exe and the HTTP mount (Poetry::Agent::MCP::Bundled):
|
|
21
|
+
# the registry root defaults to the bundled poetry-ui gem; skills, live
|
|
22
|
+
# helper names, recipes, and icon names ride along when their gems are
|
|
23
|
+
# bundled; app_root (Dir.pwd) is the app the agent runs in.
|
|
24
|
+
begin
|
|
25
|
+
Poetry::Agent::MCP::Bundled.server(root: ARGV[0], app_root: Dir.pwd).serve
|
|
26
|
+
rescue ArgumentError => e
|
|
27
|
+
abort "poetry-agent: #{e.message}"
|
|
28
|
+
end
|