pinmark 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/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/app/controllers/concerns/pinmark/session.rb +32 -0
- data/app/controllers/pinmark/annotations_controller.rb +102 -0
- data/app/controllers/pinmark/application_controller.rb +19 -0
- data/app/javascript/pinmark/pinmark_controller.js +857 -0
- data/app/views/pinmark/_activator.html.erb +14 -0
- data/app/views/pinmark/_overlay.html.erb +24 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +11 -0
- data/lib/generators/pinmark/install/install_generator.rb +82 -0
- data/lib/pinmark/engine.rb +38 -0
- data/lib/pinmark/hooks/erb_partial.rb +39 -0
- data/lib/pinmark/hooks/view_component.rb +21 -0
- data/lib/pinmark/mcp/queue.rb +90 -0
- data/lib/pinmark/mcp/rack_app.rb +36 -0
- data/lib/pinmark/mcp/server.rb +29 -0
- data/lib/pinmark/mcp/tools/clear_addressed.rb +28 -0
- data/lib/pinmark/mcp/tools/list_pending.rb +44 -0
- data/lib/pinmark/mcp/tools/list_resolved.rb +41 -0
- data/lib/pinmark/mcp/tools/mark_addressed.rb +41 -0
- data/lib/pinmark/phlex.rb +44 -0
- data/lib/pinmark/source_locator.rb +17 -0
- data/lib/pinmark/stylesheets.rb +83 -0
- data/lib/pinmark/tracker.rb +41 -0
- data/lib/pinmark/version.rb +5 -0
- data/lib/pinmark/wrapper.rb +40 -0
- data/lib/pinmark.rb +36 -0
- data/package.json +14 -0
- metadata +147 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f0f6c70a62337fbe77a78acba0686b18ef4ab1dce911fd1246ed1a8153aad218
|
|
4
|
+
data.tar.gz: 0cf5f8fdd502f1ed69c11789afb8f548c8ec796a772b149c3b0c7d02dffa31d5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5599acbbba97ab14da41f49babc53776a5b8f498ce2d6d624ed9677c9865764bbc4884fc98e0b6e6e044236b9bc47bc7203992959a5c6205d4bc86512d0d82f1
|
|
7
|
+
data.tar.gz: 22a79cd9a9be439f1360f1f95494fadaa5a02132a9781c0f218307e52ab90a1e88db926c3776757037053945048488063955d21a10456ebc6db3b94ee5829097
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — Unreleased
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Phlex `around_template` hook via `Pinmark::Phlex` concern.
|
|
8
|
+
- ViewComponent `render_in` prepend hook (auto-applied when ViewComponent is loaded).
|
|
9
|
+
- ERB partial `render_partial_template` prepend hook.
|
|
10
|
+
- Activator + overlay ERB partials renderable in any Rails layout.
|
|
11
|
+
- Stimulus controller for hover targeting, popover comments, side panel, on-page pin markers, marquee select.
|
|
12
|
+
- File-backed atomic queue at `tmp/pinmark/queue.json`.
|
|
13
|
+
- Rack-mountable MCP HTTP server exposing `list_pending_annotations`, `list_resolved_annotations`, `mark_addressed`, `clear_addressed`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Przemyslaw Lusar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Pinmark
|
|
2
|
+
|
|
3
|
+
Pin-style UI annotations that flow into Claude Code via MCP.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Why Pinmark?
|
|
8
|
+
|
|
9
|
+
You spot something off in the browser — a misaligned card, a copy bug, the wrong
|
|
10
|
+
shade of orange. Normally you'd switch contexts: open the editor, hunt for the
|
|
11
|
+
component, type the prompt, paste a screenshot, describe the spot.
|
|
12
|
+
|
|
13
|
+
Pinmark closes that loop. Click the element on the page, type the comment,
|
|
14
|
+
keep working. Claude Code reads the queue over MCP and edits the right file —
|
|
15
|
+
because each annotation already carries its source `file:line`, the component
|
|
16
|
+
class, and the DOM selector.
|
|
17
|
+
|
|
18
|
+
## What you get
|
|
19
|
+
|
|
20
|
+
- Floating "Enable annotations" activator that survives Turbo navigation.
|
|
21
|
+
- Click-to-pin overlay with popover, side panel, on-page pin markers, marquee
|
|
22
|
+
select, hover label, and a dual-highlight context for component vs. element.
|
|
23
|
+
- `<!-- pinmark:begin/end -->` HTML markers around every Phlex / ViewComponent /
|
|
24
|
+
ERB partial render — automatically.
|
|
25
|
+
- File-backed atomic queue. Survives reloads, no database needed.
|
|
26
|
+
- Rack-mountable MCP HTTP server, in-process with your Rails app.
|
|
27
|
+
- Four MCP tools so Claude Code can list, resolve, and clear annotations.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
In the host app's `Gemfile`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
group :development do
|
|
35
|
+
gem "pinmark"
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bundle install
|
|
43
|
+
bin/rails generate pinmark:install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The generator mounts `Pinmark::Engine` at `/dev/pinmark` (only when
|
|
47
|
+
`Rails.env.local?`) and pins the engine's Stimulus controller into your
|
|
48
|
+
importmap.
|
|
49
|
+
|
|
50
|
+
For webpack / esbuild hosts, the same controller is published as an exports map
|
|
51
|
+
in `package.json` — add `"pinmark": "*"` (or a `file:` path during local
|
|
52
|
+
development) to your `package.json` and import it from your Stimulus entry
|
|
53
|
+
point:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import PinmarkController from "pinmark"
|
|
57
|
+
application.register("pinmark", PinmarkController)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configure
|
|
61
|
+
|
|
62
|
+
A few host touch-points stay manual because they live in host-owned classes.
|
|
63
|
+
|
|
64
|
+
1. **Current attribute** — pinmark stores the per-request tracker on
|
|
65
|
+
`ActiveSupport::CurrentAttributes`:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
69
|
+
attribute :pinmark
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. **Controller** — include the session concern in any controller whose
|
|
74
|
+
responses should support annotations:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class ApplicationController < ActionController::Base
|
|
78
|
+
include Pinmark::Session
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. **Layout** — render the activator + overlay near the bottom of `<body>` in
|
|
83
|
+
your dev layout:
|
|
84
|
+
|
|
85
|
+
```erb
|
|
86
|
+
<% if Rails.env.development? && Current.pinmark.present? %>
|
|
87
|
+
<%= render "pinmark/activator" %>
|
|
88
|
+
<%= render "pinmark/overlay" %>
|
|
89
|
+
<% end %>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
4. **Phlex base class (optional)** — only if your host uses Phlex:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class Components::Base < Phlex::HTML
|
|
96
|
+
include Pinmark::Phlex if Rails.env.development?
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
ViewComponent and ERB partial wrapping are auto-applied at engine boot — no
|
|
101
|
+
per-host wiring needed.
|
|
102
|
+
|
|
103
|
+
5. **Mount** — the install generator adds this, but for reference:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# config/routes.rb
|
|
107
|
+
mount Pinmark::Engine, at: "/dev/pinmark" if Rails.env.local?
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Use
|
|
111
|
+
|
|
112
|
+
Visit any page in development. Click the floating "Enable annotations" button.
|
|
113
|
+
Hover any component or element, click to drop a pin, leave a comment, save.
|
|
114
|
+
Pins persist on the page and across navigation until they're addressed.
|
|
115
|
+
|
|
116
|
+
## Connect to Claude Code
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
claude mcp add pinmark --transport http \
|
|
120
|
+
http://localhost:PORT/dev/pinmark/annotations/mcp
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Tools exposed:
|
|
124
|
+
|
|
125
|
+
- `list_pending_annotations` — every open annotation, with `file:line`,
|
|
126
|
+
component class, DOM selector, comment, and page path.
|
|
127
|
+
- `list_resolved_annotations` — annotations that were already addressed.
|
|
128
|
+
- `mark_addressed` — flip an annotation by `id`.
|
|
129
|
+
- `clear_addressed` — purge the resolved bucket.
|
|
130
|
+
|
|
131
|
+
After Claude Code makes the change, it calls `mark_addressed` and the pin
|
|
132
|
+
disappears from the page on the next reload.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
- Render hooks emit HTML comment markers (`<!-- pinmark:begin id=... -->` …
|
|
137
|
+
`<!-- pinmark:end id=... -->`) around every component render.
|
|
138
|
+
- A per-request `Pinmark::Tracker` collects the parent/child hierarchy plus the
|
|
139
|
+
source location for each marker.
|
|
140
|
+
- The Stimulus controller in the browser walks the DOM, resolves which
|
|
141
|
+
component is under the cursor, draws highlights, and POSTs annotations to the
|
|
142
|
+
engine's controller.
|
|
143
|
+
- Annotations are appended atomically to `tmp/pinmark/queue.json`.
|
|
144
|
+
- The MCP server reads the same JSON file and exposes the four tools listed
|
|
145
|
+
above. Claude Code talks to your local Rails app directly — no separate
|
|
146
|
+
process.
|
|
147
|
+
|
|
148
|
+
## Renderer support
|
|
149
|
+
|
|
150
|
+
| Renderer | Wiring |
|
|
151
|
+
| -------------- | ----------------------------------------------------------- |
|
|
152
|
+
| Phlex | `include Pinmark::Phlex` in your component base class |
|
|
153
|
+
| ViewComponent | Auto — `render_in` is prepended at engine boot |
|
|
154
|
+
| ERB partial | Auto — `render_partial_template` is prepended at engine boot |
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
git clone https://github.com/lluzak/pinmark.git
|
|
160
|
+
cd pinmark
|
|
161
|
+
bundle install
|
|
162
|
+
bundle exec rspec
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Pull requests welcome. Please add specs for any new behavior and keep the diff
|
|
166
|
+
focused — Pinmark stays intentionally small.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pinmark
|
|
4
|
+
# Wraps each request in a tracker so the Phlex/ViewComponent/ERB hooks can
|
|
5
|
+
# push/pop nodes. Activated by the `?annotate=1` query string or the
|
|
6
|
+
# `pinmark=1` cookie set by the activator partial.
|
|
7
|
+
#
|
|
8
|
+
# Include this in any host controller whose responses should support
|
|
9
|
+
# annotations (typically the storefront base controller).
|
|
10
|
+
module Session
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
around_action :setup_pinmark, if: :pinmark_enabled?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def pinmark_enabled?
|
|
20
|
+
return false unless Rails.env.development?
|
|
21
|
+
|
|
22
|
+
params[:annotate] == "1" || cookies[:pinmark] == "1"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def setup_pinmark
|
|
26
|
+
Current.pinmark = Pinmark::Tracker.new
|
|
27
|
+
yield
|
|
28
|
+
ensure
|
|
29
|
+
Current.pinmark = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Pinmark
|
|
7
|
+
class AnnotationsController < ApplicationController
|
|
8
|
+
def index
|
|
9
|
+
queue = Pinmark::Mcp::Queue.new
|
|
10
|
+
render json: { annotations: queue.read.fetch("annotations", []) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
payload = JSON.parse(request.body.read)
|
|
15
|
+
comments = Array(payload["comments"])
|
|
16
|
+
path = payload["path"]
|
|
17
|
+
tree = payload["tree"]
|
|
18
|
+
replace_match = payload["replace_match"] == true
|
|
19
|
+
|
|
20
|
+
queue = Pinmark::Mcp::Queue.new
|
|
21
|
+
data = queue.read
|
|
22
|
+
existing = data.fetch("annotations", [])
|
|
23
|
+
|
|
24
|
+
tree_lookup = flatten_tree(tree).index_by { |n| n["id"] }
|
|
25
|
+
|
|
26
|
+
new_entries = comments.map do |c|
|
|
27
|
+
annotation_id = c["node_id"] || c["pm_id"]
|
|
28
|
+
node = tree_lookup[annotation_id]
|
|
29
|
+
{
|
|
30
|
+
"id" => SecureRandom.uuid,
|
|
31
|
+
"status" => "pending",
|
|
32
|
+
"received_at" => Time.current.iso8601,
|
|
33
|
+
"page_path" => path,
|
|
34
|
+
"component" => c["component"] || node&.dig("component"),
|
|
35
|
+
"source" => c["source"] || node&.dig("source"),
|
|
36
|
+
"node_id" => annotation_id,
|
|
37
|
+
"selector" => c["selector"],
|
|
38
|
+
"text_excerpt" => c["text_excerpt"],
|
|
39
|
+
"comment" => c["comment"],
|
|
40
|
+
"captured_at" => c["captured_at"],
|
|
41
|
+
"ancestry" => component_ancestry(annotation_id, tree_lookup)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
remaining = if replace_match
|
|
46
|
+
drop_matching(existing, new_entries, path)
|
|
47
|
+
else
|
|
48
|
+
existing
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
queue.write({ "annotations" => remaining + new_entries })
|
|
52
|
+
|
|
53
|
+
render json: {
|
|
54
|
+
ok: true,
|
|
55
|
+
count: new_entries.size,
|
|
56
|
+
queue: queue.path.relative_path_from(Rails.root).to_s
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def destroy
|
|
61
|
+
queue = Pinmark::Mcp::Queue.new
|
|
62
|
+
data = queue.read
|
|
63
|
+
before = data.fetch("annotations", []).size
|
|
64
|
+
data["annotations"] = data.fetch("annotations", []).reject { |entry| entry["id"] == params[:id] }
|
|
65
|
+
removed = before - data["annotations"].size
|
|
66
|
+
queue.write(data)
|
|
67
|
+
|
|
68
|
+
render json: { ok: true, removed: }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def flatten_tree(nodes, acc = [])
|
|
74
|
+
Array(nodes).each do |node|
|
|
75
|
+
acc << node
|
|
76
|
+
flatten_tree(node["children"], acc)
|
|
77
|
+
end
|
|
78
|
+
acc
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def component_ancestry(annotation_id, lookup)
|
|
82
|
+
chain = []
|
|
83
|
+
current = lookup[annotation_id]
|
|
84
|
+
while current
|
|
85
|
+
chain.unshift(
|
|
86
|
+
"id" => current["id"],
|
|
87
|
+
"component" => current["component"],
|
|
88
|
+
"source" => current["source"]
|
|
89
|
+
)
|
|
90
|
+
current = current["parent_id"] ? lookup[current["parent_id"]] : nil
|
|
91
|
+
end
|
|
92
|
+
chain
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def drop_matching(existing, new_entries, path)
|
|
96
|
+
keys = new_entries.to_set { |e| [e["node_id"], e["selector"], path] }
|
|
97
|
+
existing.reject do |entry|
|
|
98
|
+
keys.include?([entry["node_id"], entry["selector"], entry["page_path"]])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pinmark
|
|
4
|
+
# Base controller for the engine. Inherits from `ActionController::Base`
|
|
5
|
+
# directly (rather than the host's `ApplicationController`) to keep the
|
|
6
|
+
# engine self-contained — host concerns like authentication / multi-tenancy
|
|
7
|
+
# never run for the dev-only annotation endpoints.
|
|
8
|
+
class ApplicationController < ::ActionController::Base
|
|
9
|
+
skip_forgery_protection
|
|
10
|
+
|
|
11
|
+
before_action :ensure_development!
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def ensure_development!
|
|
16
|
+
head(:not_found) unless Rails.env.development?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|