poetry-agent 0.1.0 → 0.1.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 +4 -4
- data/CHANGELOG.md +16 -1
- data/app/assets/stylesheets/poetry-agent.css +25 -0
- data/lib/poetry/agent/mcp/bundled.rb +24 -10
- data/lib/poetry/agent/mcp/server.rb +68 -10
- data/lib/poetry/agent/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdfdb5620724755c575e18ce6bfd75b48bd4237ca0f637d0b909057a52cfc46f
|
|
4
|
+
data.tar.gz: 23014ea1b745fafb3e48e3337f399df235417352650a469e07d26a52821d905b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0fed74653f7b49002c96d01a6157bca3d22c0c2fde05a01570c3e1c8cc7e22a39157a062a516f5ae263dc758180576ac56bcc2d9d9d8fbfbe83b3501c5ea1636
|
|
7
|
+
data.tar.gz: b6d84fc27e506a684f77cf6b088a545f62b826aac36cd09c2d4eb0f3dae9f29217d0f9d7f36f261005117ec3a71c853eff0fe91f23f2e33733d4c5b9515f8e03
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.1.2] - 2026-09-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- A tool called without one of its declared required arguments (or with it blank) answers an error naming the argument; `check` in particular no longer reports PASS for a call that sent no `source`. `compose` and `build_page` keep answering a blank brief with their own guidance.
|
|
8
|
+
|
|
9
|
+
- `compose`, `build_page`, `list_components` and `describe_component` name an app component's helper from the registry, and an app component without one by the class to render, never an invented `poetry_` name. Once the app committed its registry that file is the truth and the component sources are not scanned; an invalid committed registry is not a root and does not crash the server.
|
|
10
|
+
|
|
11
|
+
- The WebMCP agent-focus styling (`:tool-form-active` on the form an agent fills, `:tool-submit-active` on its submit) moves here from poetry-ui's nine theme fragments, where it was identical in every theme: `app/assets/stylesheets/poetry-agent.css`, plain CSS on the theme's tokens, vendored by `poetry:install` into `layer(base)` when this gem is bundled. Hosts without poetry-agent no longer carry the rules or the two warnings the CSS optimizer prints for the origin-trial pseudo-classes on every minified build (the Rails Tailwind task minifies by default).
|
|
12
|
+
- Controllers manifests are registered by the same convention, boot-free: every bundled gem's and the app's own (`bin/rails poetry:stimulus:manifest`), so the `check` tool validates chart, agent and host controllers like core's.
|
|
13
|
+
- The server assembles from every published registry in the bundle plus the app's own committed one (`bin/rails poetry:registry`), by convention: no gem is named, a third-party engine that commits a registry is served, and an app's components describe, check and compose with full contracts under their declared helpers. `Server.from_registries` merges roots; `from_registry` remains for one.
|
|
14
|
+
- The MCP `check` tool knows the host application's own component helpers: a boot-free scan of the app directory's component files for `helper :name` declarations adds those names to the valid set, so the tool agrees with `bin/rails poetry:check` that the helper exists. Their option contracts stay with the booted check.
|
|
15
|
+
|
|
16
|
+
## [0.1.1] - 2026-09-08
|
|
17
|
+
|
|
18
|
+
Lockstep release with the family; no changes in this gem.
|
|
4
19
|
|
|
5
20
|
## [0.1.0] - 2026-09-05
|
|
6
21
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* poetry-agent: the WebMCP agent-focus states. The browser applies
|
|
2
|
+
:tool-form-active to the form an agent is filling through a declarative
|
|
3
|
+
tool and :tool-submit-active to its submit; these rules make that a
|
|
4
|
+
designed state on the theme's tokens instead of the browser's default.
|
|
5
|
+
:where() keeps both rules forgiving (zero specificity, and a browser
|
|
6
|
+
without the pseudo-classes drops the argument, not the rule), so a host
|
|
7
|
+
restyles them with any rule of its own. Plain CSS on purpose: the file
|
|
8
|
+
is vendored into the host's Tailwind entry (`poetry:install` when this
|
|
9
|
+
gem is bundled) and imported into layer(base) beside the theme.
|
|
10
|
+
|
|
11
|
+
The CSS optimizer that runs on every minified build (the Rails
|
|
12
|
+
Tailwind task minifies by default; TAILWINDCSS_DEBUG=1 skips it) does
|
|
13
|
+
not yet know these origin-trial pseudo-classes and warns twice; that is
|
|
14
|
+
the optimizer, not the CSS - the rules compile and ship. Hosts without
|
|
15
|
+
poetry-agent never carry them. */
|
|
16
|
+
:where(form:tool-form-active) {
|
|
17
|
+
border-radius: var(--radius);
|
|
18
|
+
outline: 2px dashed color-mix(in oklab, var(--ring) 60%, transparent);
|
|
19
|
+
outline-offset: 4px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:where(:tool-submit-active) {
|
|
23
|
+
border-color: var(--ring);
|
|
24
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 50%, transparent);
|
|
25
|
+
}
|
|
@@ -12,23 +12,37 @@ module Poetry
|
|
|
12
12
|
module Bundled
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
-
# @param root [String, nil]
|
|
16
|
-
#
|
|
15
|
+
# @param root [String, nil] one more registry root to serve (the
|
|
16
|
+
# bundle's published registries and the app's committed one are
|
|
17
|
+
# found on their own)
|
|
18
|
+
# @param app_root [String] the host app (its committed registry, when
|
|
19
|
+
# `bin/rails poetry:registry` wrote one; build_page's probe/direct
|
|
17
20
|
# steps read its config/theme)
|
|
18
21
|
# @return [Server]
|
|
19
22
|
# @raise [ArgumentError] when no component registry is found
|
|
20
23
|
def server(root: nil, app_root: Dir.pwd)
|
|
21
24
|
ui = soft_require("poetry/ui")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
roots = Poetry::Core::Registry.gem_roots(app_root: app_root).map(&:to_s)
|
|
26
|
+
# Controllers manifests by the same convention, boot-free: every
|
|
27
|
+
# bundled gem's and the app's own, so the check tool validates
|
|
28
|
+
# chart, agent and host controllers like core's.
|
|
29
|
+
manifest_roots = Poetry::Core::Registry.gem_roots(app_root: app_root, registry: false)
|
|
30
|
+
Poetry::Core::Stimulus::Manifest.register_roots(manifest_roots)
|
|
31
|
+
roots.unshift(root) if root && !roots.include?(root)
|
|
32
|
+
raise ArgumentError, "no published component registry in the bundle - pass a root" if roots.empty?
|
|
27
33
|
|
|
28
34
|
skills, helpers, recipes =
|
|
29
|
-
ui
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
if ui
|
|
36
|
+
[Poetry::Ui.agent_skills(app_root: app_root), Poetry::Ui.helper_names, Poetry::Ui.recipe_items.summaries]
|
|
37
|
+
else
|
|
38
|
+
[{}, nil, []]
|
|
39
|
+
end
|
|
40
|
+
# The valid helper set: the registries' own sections carry every
|
|
41
|
+
# gem helper and the app's declared ones; poetry-ui's live names
|
|
42
|
+
# ride along when the gem is loaded, for a registry that predates
|
|
43
|
+
# the sections.
|
|
44
|
+
Server.from_registries(roots, icon_names: icon_names, helpers: helpers, skills: skills,
|
|
45
|
+
app_root: app_root, recipes: recipes)
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
# The lucide names, or nil for a host without poetry-lucide (check
|
|
@@ -27,6 +27,10 @@ module Poetry
|
|
|
27
27
|
# The serverInfo payload returned by the initialize handshake.
|
|
28
28
|
SERVER_INFO = { "name" => "poetry-agent", "version" => Poetry::Agent::VERSION }.freeze
|
|
29
29
|
|
|
30
|
+
# Tools that answer a blank brief with guidance of their own (the
|
|
31
|
+
# block catalog, what to send) rather than a pass.
|
|
32
|
+
TOLERATES_BLANK = %w[compose build_page].freeze
|
|
33
|
+
|
|
30
34
|
# The tool roster the server advertises (tools/list): MCP Tool-shaped
|
|
31
35
|
# definitions, read-only by construction.
|
|
32
36
|
TOOLS = [
|
|
@@ -196,13 +200,42 @@ module Poetry
|
|
|
196
200
|
# Lazy because the usage skill is generated from the registry on
|
|
197
201
|
# first fetch - server boot stays instant.
|
|
198
202
|
def self.from_registry(root, helpers: nil, icon_names: nil, skills: {}, app_root: nil, recipes: [])
|
|
199
|
-
|
|
203
|
+
from_registries([root], helpers: helpers, icon_names: icon_names, skills: skills,
|
|
204
|
+
app_root: app_root, recipes: recipes)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Several registry roots as one catalog (Registry.merged: later
|
|
208
|
+
# roots win a path collision, block templates resolve to absolute
|
|
209
|
+
# paths): the gems' published registries and, when the app
|
|
210
|
+
# committed its own with `bin/rails poetry:registry`, the app's -
|
|
211
|
+
# its components then describe, check and compose like a gem's.
|
|
212
|
+
def self.from_registries(roots, helpers: nil, icon_names: nil, skills: {}, app_root: nil, recipes: [])
|
|
213
|
+
committed = Poetry::Core::Registry.merged(roots, source_root: roots.first)
|
|
214
|
+
# The app's own components declare their helpers in source
|
|
215
|
+
# (`helper :name`); a boot-free scan of app_root's component files
|
|
216
|
+
# adds those names to the valid set, so the check tool agrees with
|
|
217
|
+
# `bin/rails poetry:check` that the helper exists (their option
|
|
218
|
+
# contracts are only known to the booted check).
|
|
219
|
+
helper_entries = (committed.helpers || {}).dup
|
|
220
|
+
# Only for an app that never committed its registry: once the
|
|
221
|
+
# file exists it is the truth (helpers, contracts), not the scan.
|
|
222
|
+
if app_root && !Poetry::Core::Registry.published_at?(app_root)
|
|
223
|
+
Poetry::Core::HostComponents.declared_helpers(root: app_root).each { |name| helper_entries[name] ||= {} }
|
|
224
|
+
end
|
|
225
|
+
helpers = (helpers + mapped_helpers(committed.entries)).uniq if helpers
|
|
200
226
|
catalog = Poetry::Core::Check::Catalog.new(committed.entries, helpers: helpers,
|
|
201
|
-
helper_entries:
|
|
227
|
+
helper_entries: helper_entries,
|
|
202
228
|
icon_names: icon_names,
|
|
203
229
|
helper_args: committed.helper_args)
|
|
204
230
|
new(entries: committed.entries, catalog: catalog, blocks: committed.blocks || {},
|
|
205
|
-
root:
|
|
231
|
+
root: committed.source_root.to_s, skills: skills, app_root: app_root, recipes: recipes)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# The helper each entry names, so an explicit helper list never
|
|
235
|
+
# hides another root's components (an entry without one has no
|
|
236
|
+
# helper to map).
|
|
237
|
+
def self.mapped_helpers(entries)
|
|
238
|
+
entries.filter_map { |path, entry| Poetry::Core::Registry.helper_for(path, entry) }
|
|
206
239
|
end
|
|
207
240
|
|
|
208
241
|
# app_root: the HOST app directory (where `bundle exec poetry-agent`
|
|
@@ -295,6 +328,10 @@ module Poetry
|
|
|
295
328
|
def call_tool(params)
|
|
296
329
|
name = params["name"]
|
|
297
330
|
arguments = params["arguments"] || {}
|
|
331
|
+
if !TOLERATES_BLANK.include?(name) && (missing = missing_arguments(name, arguments)).any?
|
|
332
|
+
return tool_content("#{name}: missing required argument#{"s" if missing.size > 1} " \
|
|
333
|
+
"#{missing.join(", ")} (see tools/list for the schema)", error: true)
|
|
334
|
+
end
|
|
298
335
|
text =
|
|
299
336
|
case name
|
|
300
337
|
when "compose" then compose(arguments)
|
|
@@ -312,12 +349,21 @@ module Poetry
|
|
|
312
349
|
tool_content(text)
|
|
313
350
|
end
|
|
314
351
|
|
|
352
|
+
# The tool's declared required arguments that are absent or blank -
|
|
353
|
+
# a wrong key never reaches a tool as an empty value it would pass.
|
|
354
|
+
def missing_arguments(name, arguments)
|
|
355
|
+
tool = TOOLS.find { |candidate| candidate["name"] == name }
|
|
356
|
+
return [] unless tool
|
|
357
|
+
|
|
358
|
+
Array(tool.dig("inputSchema", "required")).select { |key| arguments[key].to_s.strip.empty? }
|
|
359
|
+
end
|
|
360
|
+
|
|
315
361
|
# --- the tools (project the built surfaces) ---
|
|
316
362
|
|
|
317
363
|
def list_components
|
|
318
364
|
@entries.map do |path, entry|
|
|
319
365
|
interactive = entry["controllers"]&.any? ? " [interactive]" : ""
|
|
320
|
-
"- #{title(path)} (
|
|
366
|
+
"- #{title(path)} (#{helper_label(path)})#{interactive}: #{summary(entry)}"
|
|
321
367
|
end.join("\n")
|
|
322
368
|
end
|
|
323
369
|
|
|
@@ -327,7 +373,7 @@ module Poetry
|
|
|
327
373
|
|
|
328
374
|
entry = @entries.fetch(path)
|
|
329
375
|
detail = arguments.fetch("detail", "detailed")
|
|
330
|
-
lines = ["# #{title(path)} (
|
|
376
|
+
lines = ["# #{title(path)} (#{helper_label(path)})", summary(entry)]
|
|
331
377
|
lines.concat(surface_lines(entry)) if %w[detailed full].include?(detail)
|
|
332
378
|
lines.concat(full_lines(entry)) if detail == "full"
|
|
333
379
|
# The block back-reference: a screen containing this
|
|
@@ -434,7 +480,9 @@ module Poetry
|
|
|
434
480
|
def component_route(scored, components)
|
|
435
481
|
lines = ["No block covers this brief - component-scale work."]
|
|
436
482
|
lines << if components.any?
|
|
437
|
-
"Matched components: #{components.map
|
|
483
|
+
"Matched components: #{components.map do |name|
|
|
484
|
+
"#{name} (#{helper_label(path_for(name))})"
|
|
485
|
+
end.join(", ")} - " \
|
|
438
486
|
"describe_component for the contracts."
|
|
439
487
|
else
|
|
440
488
|
"No component name matched either - list_components for the catalog."
|
|
@@ -860,7 +908,7 @@ module Poetry
|
|
|
860
908
|
convention = args && slot["types"].all? { |type| args[type]&.zero? } ? " - options as keywords" : ""
|
|
861
909
|
facets << "types #{slot["types"].join("|")}#{convention}"
|
|
862
910
|
end
|
|
863
|
-
facets << "takes #{
|
|
911
|
+
facets << "takes #{helper_text(slot["component"])} props, not a block" if slot["component"]
|
|
864
912
|
# The render-crash seams, stated where agents read them.
|
|
865
913
|
if (yieldless = slot["yieldless"])
|
|
866
914
|
setters = yieldless.map { |name| "with_#{name}" }.join("/")
|
|
@@ -936,14 +984,24 @@ module Poetry
|
|
|
936
984
|
end
|
|
937
985
|
|
|
938
986
|
# name may be the title (button, command_dialog) or the full path.
|
|
987
|
+
# A registry path, a title, or a helper name (the poetry_ convention
|
|
988
|
+
# or an app component's declared helper).
|
|
939
989
|
def resolve(name)
|
|
940
990
|
return name if @entries.key?(name)
|
|
941
991
|
|
|
942
|
-
@entries.keys.find { |path| title(path) == name }
|
|
992
|
+
@entries.keys.find { |path| title(path) == name || helper(path) == name }
|
|
943
993
|
end
|
|
944
994
|
|
|
945
|
-
|
|
946
|
-
def
|
|
995
|
+
# A gem path drops its poetry/<gem>/ prefix; an app path is its own.
|
|
996
|
+
def title(path) = (path.start_with?("poetry/") ? path.split("/").drop(2) : path.split("/")).join("_")
|
|
997
|
+
# The helper the registry names for the entry, nil for an app
|
|
998
|
+
# component that declared none (it renders by class).
|
|
999
|
+
def helper(path) = Poetry::Core::Registry.helper_for(path, @entries[path] || {})
|
|
1000
|
+
# The helper to render the entry by, or the class when it has none.
|
|
1001
|
+
def helper_text(path) = helper(path) || "render #{@entries.dig(path, "class_name") || path}"
|
|
1002
|
+
# The same, the helper in backticks.
|
|
1003
|
+
def helper_label(path) = (name = helper(path)) ? "`#{name}`" : helper_text(path)
|
|
1004
|
+
def path_for(name) = @entries.keys.find { |path| title(path) == name }
|
|
947
1005
|
|
|
948
1006
|
def result(id, value) = { "jsonrpc" => "2.0", "id" => id, "result" => value }
|
|
949
1007
|
|
data/lib/poetry/agent/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: poetry-agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Solt
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.1.
|
|
18
|
+
version: 0.1.2
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.1.
|
|
25
|
+
version: 0.1.2
|
|
26
26
|
description: 'The agent-interop gem of Poetry, the AI-native UI component library.
|
|
27
27
|
Five surfaces: the boot-free poetry-agent MCP server (the component contract over
|
|
28
28
|
Model Context Protocol, with runtime skill delivery); the WebMCP runtime that registers
|
|
@@ -43,6 +43,7 @@ files:
|
|
|
43
43
|
- CHANGELOG.md
|
|
44
44
|
- LICENSE.txt
|
|
45
45
|
- README.md
|
|
46
|
+
- app/assets/stylesheets/poetry-agent.css
|
|
46
47
|
- app/javascript/poetry/agent/a2ui_surface_controller.js
|
|
47
48
|
- app/javascript/poetry/agent/adapter.js
|
|
48
49
|
- app/javascript/poetry/agent/agui_client_tool_controller.js
|