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 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
+ ![demo](docs/demo.gif)
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