stikfix 1.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omer Nesher
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 all
13
+ 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.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ <div align="center">
2
+
3
+ <img src="public/icon/128.png" alt="stikfix" width="96" height="96" />
4
+
5
+ # stikfix
6
+
7
+ **Stick a note on any web page. Your AI coding agent reads it and fixes it. That's the whole loop.**
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
10
+ [![npm](https://img.shields.io/npm/v/stikfix.svg)](https://www.npmjs.com/package/stikfix)
11
+ [![Chrome MV3](https://img.shields.io/badge/Chrome-MV3-blue.svg)](#load-the-extension)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## The problem you already know too well
18
+
19
+ You spot something off in your UI — a button that's 4px too low, a heading that wraps weird, a modal that scrolls when it shouldn't. So you screenshot it. Paste it into a chat. Type a paragraph explaining *which* button, on *which* page, and what "right" looks like. Your agent guesses. You screenshot again. Round and round.
20
+
21
+ That ping-pong is slow, lossy, and maddening — and it throws away everything the browser already knows about the element you're pointing at.
22
+
23
+ **stikfix kills the ping-pong.** You drop a note *directly on the thing*, and it lands on disk as a precise, context-rich markdown file in your project's `notes/` folder — which your AI agent can just read, fix, and mark done. A durable, file-based review loop instead of ephemeral chat.
24
+
25
+ No cloud. No accounts. No sign-up. Everything stays on `127.0.0.1` — **your code and your notes never leave your machine.**
26
+
27
+ ## How it feels to use
28
+
29
+ 1. Click **Enter Review Mode** on any page.
30
+ 2. Drop a **sticky note** — free-floating, or click an element to anchor it. Anchored notes auto-capture the CSS selector, computed styles, `outerHTML`, the bounding box, an auto-highlighted screenshot, and the React component name. No describing required.
31
+ 3. Tell your agent **"read my notes."** It reads the fresh `.md` files, makes the fixes, and **writes a reply back into each note** — so the pin turns **green ✓** right on the page with a one-line "here's what I changed" (no fix is lost in a chat log). Anything ambiguous turns **amber** with the agent's clarifying question.
32
+ 4. Glance at the pins (or open the **notes panel** to filter/search/jump), drop a few more, repeat. Pins update live as the agent works — no refresh. Your UI gets tighter every loop.
33
+
34
+ ## Quick start
35
+
36
+ ### 1. Install the host (one command)
37
+
38
+ ```bash
39
+ npx stikfix init --root /path/to/your/project
40
+ ```
41
+
42
+ This turnkey installer:
43
+
44
+ - registers the secure native-messaging host for **Chrome and Edge** (user-level, no admin rights),
45
+ - writes a small config pointing at your project,
46
+ - creates a **"Stikfix Host" launcher on your Desktop** so you can start the backend with a double-click — no terminal babysitting,
47
+ - prints your stable **extension ID** (you never copy-paste a token).
48
+
49
+ > **Keep it current:** re-run `npx --yes stikfix@latest init --root /path/to/your/project` anytime to update the host.
50
+
51
+ ### 2. Load the extension
52
+
53
+ For v1.0 the extension is loaded **unpacked** (a Chrome Web Store listing is coming next):
54
+
55
+ 1. Open `chrome://extensions` (or `edge://extensions`).
56
+ 2. Turn on **Developer mode**.
57
+ 3. Click **Load unpacked** and pick the **`.output/chrome-mv3/`** folder from this repo.
58
+
59
+ The loaded ID matches what the installer printed — nothing to type. (Building `.output/chrome-mv3/` from source is covered under [Development](#development).)
60
+
61
+ ### 3. Start the backend
62
+
63
+ Double-click the **Stikfix Host** launcher on your Desktop. That's it — no commands, and double-clicking again is safe (it won't launch a second copy). The host listens on a port in the `39240–39260` range; the extension discovers it automatically.
64
+
65
+ ### 4. Pair in one click
66
+
67
+ Open the stikfix popup in your toolbar and hit **Pair with host**. The token is handed over automatically through the OS native-messaging channel — you never see it, never paste it. Pair once and it stays paired, even after restarts.
68
+
69
+ ### 5. Drop a note
70
+
71
+ Open your app, click **Enter Review Mode**, and start dropping notes. The first note on a new site opens an **OS folder picker** so you can choose where that origin's notes live; it's remembered after that. When you're ready, tell your AI agent **"read my notes."**
72
+
73
+ ## Features
74
+
75
+ - **Free notes** — a draggable post-it you can drop anywhere on the page. Captures URL, title, timestamp, viewport, and a screenshot.
76
+ - **Element notes** — click any element and the note auto-captures a robust unique CSS selector ([`@medv/finder`](https://github.com/antonmedv/finder)), curated computed styles, truncated `outerHTML`, bounding box, `data-*`, accessibility role/label, the best-effort **React component name**, and an **auto element-highlight screenshot** showing exactly which element you meant.
77
+ - **Region / marquee capture** — the camera tool dims the page, gives you a crosshair, and lets you drag a rectangle. stikfix hides its own UI, captures, and crops the screenshot DPR-correctly. Stack multiple per note; each is a deletable thumbnail.
78
+ - **Persistent on-page pins** — notes stay visible as pins on the page across reloads. View, edit, and delete them in place — backed by host-side CRUD over the localhost relay. Overlapping pins fan out automatically so dense pages stay readable.
79
+ - **Two-way status, on the page** — pins reflect the loop: **unread** (yellow), **flagged** (amber — the agent needs you to clarify, with its question on hover), **resolved** (green ✓ — fixed, with the agent's reply on hover). Resolved notes stay visible so you can verify the fix; archived (`read`) notes disappear.
80
+ - **Notes panel** — a chip-toggled list of every note: counts by status, filter chips, text search, and click-to-jump that scrolls right to the pin. Flip **All pages** to browse every note across the project, not just the current page.
81
+ - **Live updates** — while Review Mode is on, pins and the panel refresh on their own (~4s, only when the tab is visible) as the agent writes replies and resolves notes. No manual reload.
82
+ - **Per-origin project routing** — the first note on a new site opens an OS folder picker; after that, every tab routes to the right project's `notes/` folder automatically. No per-note picking.
83
+ - **Cross-browser host** — one `npx stikfix init` registers Chrome and Edge in a single pass.
84
+ - **The `review-notes` AI skill** — the portable agent half of the loop (see below).
85
+
86
+ ## How notes reach your AI
87
+
88
+ Every note becomes a markdown file in your project's `notes/` folder, named `<serial>-<YYYYMMDD-HHmmss>.md` (e.g. `0007-20260531-143022.md`), with any screenshots written alongside it as `<base>+<N>.png`. Element notes embed a full **Element context** section — selector, computed styles, `outerHTML`, React component — so an agent can locate the exact code without guessing.
89
+
90
+ The **review-notes** skill is the agent half of the loop. It works through your unread notes in serial order, applies each fix, then writes a short **`reply`** into the note and marks it **`resolved`** (the pin turns green ✓ on the page). Re-running is always idempotent — resolved, flagged, and archived notes are skipped.
91
+
92
+ **Claude Code (project-local):**
93
+
94
+ ```bash
95
+ mkdir -p .claude/skills/review-notes
96
+ cp /path/to/stikfix/skill/SKILL.md .claude/skills/review-notes/SKILL.md
97
+ ```
98
+
99
+ **Any other folder-reading agent (Cursor, Codex, etc.):** point it at [`skill/SKILL.md`](skill/SKILL.md) — it's plain markdown, no Claude-specific bits. For example: *"follow the instructions in skill/SKILL.md."*
100
+
101
+ **Just say the word.** Any of these kicks it off:
102
+
103
+ - "read my notes"
104
+ - "process review notes"
105
+ - "fix sticky notes"
106
+ - "what notes do I have"
107
+
108
+ **Under the hood, it:**
109
+
110
+ 1. Finds every unread `notes/*.md` (skips `*.read.md` and already-resolved/flagged notes), oldest serial first.
111
+ 2. Reads the note plus its screenshots, then makes the code change.
112
+ 3. Marks the note **resolved** with a `reply` (and optional `fixed_in` commit ref) — only *after* the fix lands. If a fix is interrupted, the note stays unread and is retried next run. Resolved notes stay on the page (green ✓) so you can verify; archiving/dismissing (`status: read` + `*.read.md` rename, which hides the pin) is a separate step done after you've acknowledged them.
113
+ 4. Flags anything ambiguous instead of guessing — sets **flagged** with a `reply` question (amber pin), so it surfaces again next run for you to clarify.
114
+ 5. Gives you a one-line recap: N resolved, K flagged, J already done.
115
+
116
+ Run it on a clean directory and it just says "no unread notes." Safe to fire any time.
117
+
118
+ ## Demo
119
+
120
+ ![stikfix demo](docs/demo-placeholder.png)
121
+
122
+ *Recorded walkthrough coming soon — the placeholder above will be replaced with a GIF of the full drop-a-note → "read my notes" loop.*
123
+
124
+ ## Cross-browser
125
+
126
+ | Browser | v1.0 status | Notes |
127
+ |---------|-------------|-------|
128
+ | **Chrome** | Supported | Load unpacked from `.output/chrome-mv3/` |
129
+ | **Edge** | Supported | Drop-in; the same artifact loads directly. `init` registers Edge automatically |
130
+ | Firefox | Documented (next) | Different native-messaging manifest key; path documented |
131
+ | Safari | Documented (next) | App-bundled extension via Mac App Store; path documented |
132
+
133
+ Full packaging details, manifest locations, and the Firefox/Safari paths are in [docs/cross-browser.md](docs/cross-browser.md).
134
+
135
+ ## Security
136
+
137
+ stikfix is built localhost-first on purpose:
138
+
139
+ - **`127.0.0.1` only.** The host binds to localhost and nothing else — never `0.0.0.0`, not reachable from your network.
140
+ - **Authorized writes only.** Every `POST /annotation` is token-checked. The token is delivered to the extension over the OS native-messaging channel during pairing — it never travels over the web and you never handle it.
141
+ - **Stays in its lane.** The host only writes `.md` and `.png` files inside the project folder you chose (its `notes/` dir). Path-traversal attempts are rejected; no eval, no shelling out.
142
+ - **Sensible limits.** Oversized payloads are turned away (12 MB body cap → `413`).
143
+ - **Right project, every time.** Each tab's origin is mapped to its project, so notes land in the correct folder automatically.
144
+ - **MIT, clean-room.** Original implementation written from spec — contains no code from the GPL-3.0 upstream. See [CLEAN-ROOM.md](./CLEAN-ROOM.md) for the full provenance declaration and GPL grep audit.
145
+
146
+ ## Development
147
+
148
+ Build the extension and host from source:
149
+
150
+ ```bash
151
+ npm install # installs deps + runs `wxt prepare`
152
+ npm run build # builds the extension (.output/chrome-mv3) + host bundles
153
+ npm test # host test suite
154
+ npm run check # full gate: tsc, clean-room check, host smoke test, all tests
155
+ ```
156
+
157
+ Useful scripts:
158
+
159
+ - `npm run dev` — WXT dev server with HMR for the extension.
160
+ - `npm run host -- --root /path/to/project --origin http://localhost:3000` — start the host directly from a terminal (the Desktop launcher is the easy path; this is the manual one). On Windows PowerShell, use the equals form (`--root=C:\path`) or set `STIKFIX_ROOT` / `STIKFIX_ORIGINS` env vars, since npm 11.x swallows unknown `--flags`.
161
+
162
+ **Removing stikfix:**
163
+
164
+ ```bash
165
+ npx stikfix uninstall
166
+ ```
167
+
168
+ This removes the native-messaging manifest, the desktop launcher, and the local config — no leftovers.
169
+
170
+ ### Architecture (one-liner)
171
+
172
+ ```
173
+ [Chrome/Edge Extension (MV3)] --POST /annotation--> [stikfix host (localhost)] --writes--> notes/NNNN-<ts>.md
174
+ you annotate token-authed, 127.0.0.1 only your AI agent reads these
175
+ ```
176
+
177
+ Pairing rides a separate OS-level native-messaging channel so the token never touches the web; notes themselves flow over the localhost HTTP relay (`GET /status`, `POST /annotation`, plus `GET/PUT/DELETE /annotation/<serial>` for the persistent on-page pins).
178
+
179
+ ## Tech stack
180
+
181
+ TypeScript ES modules both halves. Extension built with [WXT](https://wxt.dev) (Vite, MV3) — vanilla DOM in a shadow root, [`@medv/finder`](https://github.com/antonmedv/finder) for selectors, [`interactjs`](https://interactjs.io) for drag + marquee, native `chrome.tabs.captureVisibleTab` + canvas crop for screenshots. The host is Node built-ins only plus [`yaml`](https://eemeli.org/yaml/) for safe frontmatter.
182
+
183
+ ## License & provenance
184
+
185
+ MIT © 2026 Omer Nesher. See [LICENSE](./LICENSE).
186
+
187
+ This is an **original, clean-room implementation**. See [CLEAN-ROOM.md](./CLEAN-ROOM.md) for the full MIT provenance declaration, clean-room method narrative, and the live GPL grep audit result.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Port-discovery helpers for stikfix-host.
3
+ * Extracted so tests can import bindServer without running the full index.ts boot.
4
+ * WR-06: removeAllListeners between scan attempts prevents stale-handler accumulation.
5
+ */
6
+ export const PORT_RANGE_START = 39240;
7
+ export const PORT_RANGE_END = 39260;
8
+ export const BIND_HOST = '127.0.0.1'; // T-02-bind: NEVER 0.0.0.0
9
+ /**
10
+ * Attempt to bind `server` on a single port.
11
+ * Resolves true if bound, false on EADDRINUSE, throws on other errors.
12
+ * Uses 'once' listeners so a failed attempt does not accumulate handlers.
13
+ */
14
+ export function tryListen(server, port) {
15
+ return new Promise((resolve, reject) => {
16
+ const onError = (e) => {
17
+ if (e.code === 'EADDRINUSE') {
18
+ resolve(false);
19
+ }
20
+ else {
21
+ reject(e);
22
+ }
23
+ };
24
+ const onListening = () => {
25
+ resolve(true);
26
+ };
27
+ server.once('error', onError);
28
+ server.once('listening', onListening);
29
+ server.listen(port, BIND_HOST);
30
+ });
31
+ }
32
+ /**
33
+ * Find the first free port in the range [start, end] and bind the server to it.
34
+ * Honors a caller-supplied preferred port (e.g. from --port) by trying it first.
35
+ * Throws if no port in range is free.
36
+ *
37
+ * WR-06: The comment previously described probe-server-per-attempt, but the
38
+ * implementation reused the same server in a loop. We keep single-server reuse
39
+ * (simpler) and make the code and comment agree: after each failed EADDRINUSE
40
+ * attempt, removeAllListeners('error') and removeAllListeners('listening') are
41
+ * called before the next attempt to prevent stale-handler accumulation.
42
+ */
43
+ export async function bindServer(server, preferredPort, startPort = PORT_RANGE_START, endPort = PORT_RANGE_END) {
44
+ // Try preferred port first if specified
45
+ if (preferredPort !== undefined) {
46
+ const bound = await tryListen(server, preferredPort);
47
+ if (bound) {
48
+ return server.address().port;
49
+ }
50
+ throw new Error(`stikfix-host: --port ${preferredPort} is already in use. ` +
51
+ `Remove --port to auto-scan ${PORT_RANGE_START}–${PORT_RANGE_END}.`);
52
+ }
53
+ // Scan range, reusing the real server across attempts.
54
+ // WR-06: Remove stale handlers between attempts so a prior failed listen()
55
+ // does not accumulate duplicate error/listening listeners on the server.
56
+ // startPort/endPort default to the production range; tests override them with
57
+ // an OS-assigned ephemeral range so the scan-past-occupied path can be
58
+ // exercised hermetically (never colliding with a real host on 39240).
59
+ for (let port = startPort; port <= endPort; port++) {
60
+ server.removeAllListeners('error');
61
+ server.removeAllListeners('listening');
62
+ const bound = await tryListen(server, port);
63
+ if (bound) {
64
+ return server.address().port;
65
+ }
66
+ }
67
+ throw new Error(`stikfix-host: no free port found in ${startPort}–${endPort}. ` +
68
+ `Use --port to specify a different port.`);
69
+ }