notemend 0.1.6 → 0.1.8

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.
@@ -13,5 +13,5 @@
13
13
  "design-review",
14
14
  "mcp"
15
15
  ],
16
- "version": "0.1.6"
16
+ "version": "0.1.8"
17
17
  }
package/README.md CHANGED
@@ -1,11 +1,162 @@
1
- # notemend — the bar
1
+ # notemend
2
2
 
3
- Note it. Mend it. A dev-only toolbar for Astro and Next.js sites, for reviewing a site the way you
4
- review a design file: leave pinned comments on any element, check spacing and type in
5
- design pixels, and look at every breakpoint without leaving the page. Comments live in
6
- the repo as JSON, so Claude Code (or anyone) can read them and answer from the terminal.
3
+ Note it. Mend it. Pin a comment on any element of a live site, at the screen width it
4
+ breaks; your coding agent — Claude Code, Codex, Cursor, Gemini CLI, Copilot or any other —
5
+ takes the comment, fixes the code and says what it changed; a person confirms the fix with
6
+ a tick. For design studios, their teams and their clients.
7
7
 
8
- It exists only under `astro dev` or `next dev`. Nothing of it reaches a build.
8
+ This package is the bar people comment with and the `notemend` CLI and MCP server agents
9
+ work the queue with. The threads live in the Notemend cabinet at
10
+ [app.notemend.com](https://app.notemend.com), shared by everyone in a project — or, with
11
+ no account, in `feedback/comments.json` in your repo. Docs:
12
+ [app.notemend.com/docs](https://app.notemend.com/docs).
13
+
14
+ ## Start
15
+
16
+ 1. Make a project at [app.notemend.com](https://app.notemend.com).
17
+ 2. Put the bar on the site — one line, any site (below) — open it with Notemend, and pin
18
+ comments. Teammates and clients do the same.
19
+ 3. In the site's code folder, connect your agent:
20
+
21
+ ```sh
22
+ npx notemend init # asks which agents you use and wires each one
23
+ npx notemend login # shows a code; allow it in the cabinet
24
+ npx notemend link <project-id>
25
+ ```
26
+
27
+ Then ask your agent for the open comments. It takes one (`notemend start`), fixes it and
28
+ resolves it with a note (`notemend done`); the comment waits, marked, until a person
29
+ confirms it on the site or in the cabinet.
30
+
31
+ ## On any site — one line, no build
32
+
33
+ Webflow, Tilda, WordPress, Framer, a live Next or Astro site: the cabinet's **Install**
34
+ tab hands out one line for the site's custom code,
35
+
36
+ ```html
37
+ <script src="https://app.notemend.com/bar.js" data-project="<project-id>" defer></script>
38
+ ```
39
+
40
+ and the whole bar — comments, grid, spacing, size and type inspectors, breakpoints,
41
+ devices — runs on the live site. A visitor sees nothing: the bar wakes only for people in
42
+ the project, once they open the site with `?notemend` (the cabinet's "Open site", the
43
+ link you send a client) and sign in once on that browser. Their session stays with the
44
+ cabinet, in a hidden frame of its own — the site's scripts never hold a token. The
45
+ cabinet's **Preview** tab shows the site at desktop, tablet or mobile width with the bar
46
+ in it. The pages menu comes from the site's `sitemap.xml` and the pages that already have
47
+ threads.
48
+
49
+ `bar.js` is `src/hosted.ts` bundled by the cabinet's build (`web/scripts/build-bar.mjs`);
50
+ its endpoints are the dev server's, served by the cabinet (`src/hosting.mjs`) as whoever
51
+ is signed in, so the database's rules decide what they may read and write.
52
+
53
+ ## Your coding agent
54
+
55
+ The queue is a CLI any agent that runs commands can work, an MCP server for agents that
56
+ take tools, and a Claude Code plugin. Whichever you use, a comment reaches the agent with
57
+ its page, element, width, device, thread, screenshots and what the page logged (console
58
+ errors and failed requests, secrets masked), quoted as what people wrote — never as
59
+ instructions — and only the comments meant for the agent.
60
+
61
+ ```
62
+ npx notemend open comments for the agent
63
+ npx notemend all including resolved ones and those given to people
64
+ npx notemend start <id> take it before fixing: the thread shows the agent is on it,
65
+ and another agent is refused
66
+ npx notemend reply <id> "..." answer in the thread
67
+ npx notemend done <id> "..." resolve as the agent, with a note (stays marked until a person confirms)
68
+ npx notemend reopen <id>
69
+ npx notemend rm <id> delete the thread and its screenshots
70
+ npx notemend shot <id> [--before] a picture of the element at the comment's width (Playwright)
71
+ npx notemend prune [--dry] local store: move resolved threads to feedback/archive/
72
+ npx notemend digest | count short forms for the agents' hooks
73
+ npx notemend mcp the same queue as an MCP server (stdio)
74
+ npx notemend init [--yes] set a project up
75
+ ```
76
+
77
+ The root is the nearest directory with a `package.json` above the working directory, or
78
+ `--root <dir>`. The panel numbers threads per page and breakpoint (`#4`); the CLI prints
79
+ both that and the file-wide id: `#4 (id 10)`. `prune` keeps a local store readable:
80
+ resolved threads and their screenshots move to `feedback/archive/`, and the numbering
81
+ carries on. Nothing is deleted.
82
+
83
+ ### Signing in
84
+
85
+ ```sh
86
+ npx notemend login # shows a code and opens the cabinet — allow the same code there
87
+ npx notemend link # a new project named after the site (package.json name and homepage)
88
+ npx notemend link <project-id> # or an existing one — the id is under Install in the cabinet
89
+ ```
90
+
91
+ `login` works over SSH and in containers too — nothing listens on your computer; open the
92
+ link on any machine where you are signed in. `login --code` signs in with the six-digit
93
+ code the cabinet emails instead. The terminal gets a session of its own, so signing out in
94
+ one never signs out the other. `link` writes `.notemend.json`; commit it and the whole
95
+ team files into the same project. `whoami` says who you are and where comments go;
96
+ `unlink` and `logout` undo.
97
+
98
+ ### Wiring each agent
99
+
100
+ `init` asks which agents you use (or `--agents=claude,codex,cursor`) and wires each the
101
+ way it reads context:
102
+
103
+ | Agent | What `init` writes | What it reads |
104
+ |---|---|---|
105
+ | Claude Code | `.claude/settings.json` hooks | `digest` at session start, `count` on each prompt |
106
+ | Codex | `.codex/hooks.json` (+ `codex_hooks = true` in `.codex/config.toml`) | the same, as plain text |
107
+ | Gemini CLI | `.gemini/settings.json` hooks, `GEMINI.md` | the same, as `hookSpecificOutput.additionalContext` JSON |
108
+ | Cursor | `.cursor/hooks.json` (`sessionStart`) | `digest` as `additional_context` |
109
+ | Copilot, Windsurf, Amp, … | a block in `AGENTS.md` | how to list, take, answer and close threads |
110
+
111
+ An agent signs what it writes: threads show Codex, Gemini or Cursor rather than Claude.
112
+ Gemini CLI, Codex and Cursor are recognised from their environment; any other passes
113
+ `--as <agent>` (`npx notemend reply 3 "…" --as copilot`).
114
+
115
+ ### MCP
116
+
117
+ `npx notemend mcp` serves the queue over stdio: `list_comments`, `start`, `reply`,
118
+ `resolve`, `reopen` and `screenshot`. Each runs the CLI above, so the cloud, agent keys
119
+ and sign-in work as they do there; replies are signed with the client's own name. It is in
120
+ the MCP Registry as `io.github.zharkodv-cmd/notemend`.
121
+
122
+ One click: **Add to Cursor**, **Add to VS Code** and the **Claude Desktop** bundle are on
123
+ [app.notemend.com/docs/mcp](https://app.notemend.com/docs/mcp). By hand:
124
+
125
+ ```sh
126
+ codex mcp add notemend -- npx notemend mcp
127
+ claude mcp add notemend -- npx notemend mcp
128
+ ```
129
+
130
+ ```json
131
+ { "mcpServers": { "notemend": { "command": "npx", "args": ["notemend", "mcp", "--root", "/path/to/site"] } } }
132
+ ```
133
+
134
+ ### Claude Code plugin
135
+
136
+ The hooks and the tools in one, no `init` needed for Claude Code:
137
+
138
+ ```
139
+ /plugin marketplace add https://app.notemend.com/claude/marketplace.json
140
+ /plugin install notemend@notemend
141
+ ```
142
+
143
+ Its files are in `plugin/`; `npm run pack` puts them at the package root, and
144
+ `npm run mcpb` rebuilds the Claude Desktop bundle (`web/public/notemend.mcpb`) after a pack.
145
+
146
+ ### An agent that works on its own
147
+
148
+ A project's settings make agent keys (shown once, kept only as a hash). With one as
149
+ `NOTEMEND_KEY`, the CLI needs no sign-in: it lists the project's threads and takes,
150
+ answers and closes them as the agent — nothing else. `npx notemend init --ci` writes
151
+ `.github/workflows/notemend.yml`: every hour (or by hand) it asks for the agent's threads
152
+ and, when there are some, runs Claude Code's GitHub Action to fix them on a branch, close
153
+ each with what it changed, and open a pull request. It needs the repository secrets
154
+ `NOTEMEND_KEY` and `ANTHROPIC_API_KEY`, and `.notemend.json` committed. A person still
155
+ merges, and confirms each fix on the live site.
156
+
157
+ ## The bar
158
+
159
+ Review a site the way you review a design file:
9
160
 
10
161
  - **Pages** — every route in one menu, grouped by folder (or your own groups), with the
11
162
  open-comment count per page and per breakpoint.
@@ -34,7 +185,35 @@ It exists only under `astro dev` or `next dev`. Nothing of it reaches a build.
34
185
  size, rotatable. Both are working surfaces: comments go to the breakpoint the width
35
186
  falls in, and remember the device ("iPhone SE · bars folded").
36
187
 
37
- ## Install
188
+ ### Using the bar
189
+
190
+ | Control | |
191
+ |---|---|
192
+ | **Pages** | jump to any page. Chips show comment counts per breakpoint: blue open, clay resolved by an agent, green resolved |
193
+ | **Grid** | the layout grid overlay |
194
+ | **Spacing / Size / Type** | inspectors, one at a time. Hold **Alt** to measure the parent |
195
+ | **Sanity** | hides Sanity's visual-editing overlay (only with `sanity: true`) |
196
+ | **Comment** | comment mode: click anything to pin a note. The badge counts open threads |
197
+ | **Panel** | the list of this page's threads at this breakpoint |
198
+ | **Resolved** | shows or hides resolved threads, on the page and in the menu |
199
+ | **Breakpoints** | open the canvas at a band. The widest band is your own window |
200
+ | **Device** | *Preview*: a real screen with its browser. *DevTools*: the canvas at a device's size. Comments work in both |
201
+ | **Tab** on top of the bar | folds the bar below the window edge and back |
202
+
203
+ Keys: **Esc** closes a menu, then the note you are writing, then the open thread, then
204
+ comment mode. **Enter** sends, **Shift+Enter** is a new line. Double-click a canvas
205
+ handle to snap to the band's edge.
206
+
207
+ Everything the bar remembers (modes, canvas width, a half-typed comment) is in
208
+ `localStorage`, per browser.
209
+
210
+ ## The bar in your dev server
211
+
212
+ For Astro and Next.js projects the same bar runs under `astro dev` / `next dev` — with no
213
+ account, comments stay in `feedback/comments.json` and screenshots in `feedback/images/`;
214
+ linked, they go to the project. Nothing of it reaches a build.
215
+
216
+ ### Install
38
217
 
39
218
  ```sh
40
219
  npm i -D notemend
@@ -49,7 +228,7 @@ npx notemend init
49
228
  `<title>` or `metadata.title`, grouped by folder; dynamic routes are listed as examples
50
229
  for you to fill in;
51
230
  - **grid** — the container and grid classes the overlay should borrow;
52
- - **Claude Code** — whether to add the hooks that show Claude the open comments;
231
+ - **agents** — which coding agents you use, to wire each (above);
53
232
  - **git** — whether to keep pasted screenshots out of it.
54
233
 
55
234
  It writes `notemend.config.mjs`, wires the bar in (Astro: `notemend()` in `astro.config`;
@@ -63,7 +242,7 @@ After upgrading, stop and start `astro dev`. Astro's own restart (on a config ch
63
242
  runs in the same Node process, which keeps the old server half of the bar loaded while
64
243
  the browser half is already new.
65
244
 
66
- ### Next.js
245
+ #### Next.js
67
246
 
68
247
  App Router, Next 15.3 or later, Turbopack or webpack. Next has no integrations, so
69
248
  `init` writes three small pieces instead:
@@ -99,171 +278,7 @@ export const config = { matcher: ['/((?!__devbar|_next/static|_next/image|favico
99
278
  There is no `enabled` switch under Next: to keep the bar out of a Playwright run, add
100
279
  your own condition to the `instrumentation-client` line.
101
280
 
102
- ## On any site — one line, no build
103
-
104
- Webflow, Tilda, WordPress, Framer, a live Next or Astro site: the cabinet's **Install**
105
- tab hands out one line for the site's custom code,
106
-
107
- ```html
108
- <script src="https://app.notemend.com/bar.js" data-project="<project-id>" defer></script>
109
- ```
110
-
111
- and the whole bar — comments, grid, spacing, size and type inspectors, breakpoints,
112
- devices — runs on the live site. A visitor sees nothing: the bar wakes only for people in
113
- the project, once they open the site with `?notemend` (the cabinet's "Open site", the
114
- link you send a client) and sign in in the small window it opens. The cabinet's
115
- **Preview** tab shows the site at desktop, tablet or mobile width with the bar in it, no
116
- second sign-in. The pages menu comes from the site's `sitemap.xml` and the pages that
117
- already have threads.
118
-
119
- `bar.js` is `src/hosted.ts` bundled by the cabinet's build (`web/scripts/build-bar.mjs`);
120
- its endpoints are the dev server's, served by the cabinet (`src/hosting.mjs`) as whoever
121
- is signed in, so the database's rules decide what they may read and write.
122
-
123
- ## With your team
124
-
125
- On its own the bar files comments into `feedback/comments.json` on your computer. Linked
126
- to a project in the Notemend cabinet, it files them into the project instead, so everyone
127
- in it — teammates and clients — sees the same threads, each with its writer's name and
128
- face, and the cabinet keeps the history:
129
-
130
- ```sh
131
- npx notemend login # opens the cabinet in your browser — allow, and this computer is signed in
132
- npx notemend link # a new project named after the site (package.json name and homepage)
133
- npx notemend link <project-id> # or an existing one — the id is under "Connect" in the cabinet
134
- ```
135
-
136
- `login --code` signs in with the six-digit code the cabinet emails instead, without a
137
- browser. The browser way works for every account, Google ones included: the cabinet
138
- hands the terminal a session of its own, so signing out in one never signs out the other.
139
-
140
- `link` writes `.notemend.json`; commit it and the whole team files into the same project.
141
- Claude works the same as before — `npx notemend` lists the open threads and fetches their
142
- screenshots into `feedback/images/`, `npx notemend done` marks a fix — and a person
143
- confirms it with the tick, here or in the cabinet, which records who and when.
144
- `npx notemend whoami` says who you are and where comments go; `unlink` and `logout` undo.
145
-
146
- ## Using the bar
147
-
148
- | Control | |
149
- |---|---|
150
- | **Pages** | jump to any page. Chips show comment counts per breakpoint: blue open, clay resolved by Claude, green resolved |
151
- | **Grid** | the layout grid overlay |
152
- | **Spacing / Size / Type** | inspectors, one at a time. Hold **Alt** to measure the parent |
153
- | **Sanity** | hides Sanity's visual-editing overlay (only with `sanity: true`) |
154
- | **Comment** | comment mode: click anything to pin a note. The badge counts open threads |
155
- | **Panel** | the list of this page's threads at this breakpoint |
156
- | **Resolved** | shows or hides resolved threads, on the page and in the menu |
157
- | **Breakpoints** | open the canvas at a band. The widest band is your own window |
158
- | **Device** | *Preview*: a real screen with its browser. *DevTools*: the canvas at a device's size. Comments work in both |
159
- | **Tab** on top of the bar | folds the bar below the window edge and back |
160
-
161
- Keys: **Esc** closes a menu, then the note you are writing, then the open thread, then
162
- comment mode. **Enter** sends, **Shift+Enter** is a new line. Double-click a canvas
163
- handle to snap to the band's edge.
164
-
165
- Everything the bar remembers (modes, canvas width, a half-typed comment) is in
166
- `localStorage`, per browser.
167
-
168
- ## Comments and your coding agent
169
-
170
- Threads are stored in `feedback/comments.json`, screenshots in `feedback/images/`, both
171
- at the project root. The CLI reads and answers them:
172
-
173
- ```
174
- npx notemend open comments, newest last
175
- npx notemend all including resolved
176
- npx notemend start <id> take it before fixing: the thread shows the agent is on it
177
- npx notemend reply <id> "..." answer in the thread as Claude
178
- npx notemend done <id> "..." resolve as Claude + note (stays marked until you confirm)
179
- npx notemend note <id> "..." reply without changing status
180
- npx notemend reopen <id>
181
- npx notemend rm <id> delete the thread and its screenshots
182
- npx notemend prune [--dry] move resolved threads to feedback/archive/
183
- npx notemend digest compact open list (SessionStart hook)
184
- npx notemend count one line (UserPromptSubmit hook)
185
- npx notemend init [--yes] set a project up
186
- ```
187
-
188
- The root is the nearest directory with a `package.json` above the working directory, or
189
- `--root <dir>`.
190
-
191
- A thread Claude resolves is marked in clay until you confirm it with the tick, so "done"
192
- and "checked" stay apart. The panel numbers threads per page and breakpoint (`#4`); the
193
- CLI prints both that and the file-wide id: `#4 (id 10)`.
194
-
195
- `prune` keeps the store readable: resolved threads and their screenshots move to
196
- `feedback/archive/`, and the next comment carries on the numbering. Nothing is deleted.
197
-
198
- The Claude Code hooks `init` adds to `.claude/settings.json` (both print nothing while the
199
- queue is empty, and nothing at all where the package is not installed):
200
-
201
- ```json
202
- "SessionStart": [{ "hooks": [{ "type": "command",
203
- "command": "if [ -f \"$CLAUDE_PROJECT_DIR/node_modules/notemend/bin/notemend.mjs\" ]; then node \"$CLAUDE_PROJECT_DIR/node_modules/notemend/bin/notemend.mjs\" --root \"$CLAUDE_PROJECT_DIR\" digest; fi" }] }],
204
- "UserPromptSubmit": [{ "hooks": [{ "type": "command",
205
- "command": "if [ -f \"$CLAUDE_PROJECT_DIR/node_modules/notemend/bin/notemend.mjs\" ]; then node \"$CLAUDE_PROJECT_DIR/node_modules/notemend/bin/notemend.mjs\" --root \"$CLAUDE_PROJECT_DIR\" count; fi" }] }]
206
- ```
207
-
208
- ### Other coding agents
209
-
210
- The queue is plain CLI, so any agent that runs commands can work it. `init` asks which
211
- ones you use and wires each the way it reads context:
212
-
213
- | Agent | What `init` writes | What it reads |
214
- |---|---|---|
215
- | Claude Code | `.claude/settings.json` hooks | `digest` at session start, `count` on each prompt |
216
- | Codex | `.codex/hooks.json` (+ `codex_hooks = true` in `.codex/config.toml`) | the same, as plain text |
217
- | Gemini CLI | `.gemini/settings.json` hooks, `GEMINI.md` | the same, as `hookSpecificOutput.additionalContext` JSON |
218
- | Cursor | `.cursor/hooks.json` (`sessionStart`) | `digest` as `additional_context` |
219
- | Copilot, Windsurf, Amp, … | a block in `AGENTS.md` | how to list, answer and close threads |
220
-
221
- Before and after: with Playwright in the project, `npx notemend shot 3 --before` files a
222
- picture of the element at the comment's width, and `npx notemend done 3 "…" --shot` files
223
- one after the fix (`--url` points at the dev server; localhost:4321, or :3000 for Next).
224
-
225
- ### An agent that works on its own
226
-
227
- A project's settings make agent keys (shown once, kept only as a hash). With one as
228
- `NOTEMEND_KEY`, the CLI needs no sign-in: it lists the project's threads and answers and
229
- closes them as the agent — nothing else. `npx notemend init --ci` writes
230
- `.github/workflows/notemend.yml`: every hour (or by hand) it asks for the agent's threads
231
- and, when there are some, runs Claude Code's GitHub Action to fix them on a branch, close
232
- each with what it changed, and open a pull request. It needs the repository secrets
233
- `NOTEMEND_KEY` and `ANTHROPIC_API_KEY`, and `.notemend.json` committed. A person still
234
- merges, and confirms each fix on the live site.
235
-
236
- `npx notemend init --agents=claude,codex,cursor` skips the question. An agent signs what
237
- it writes with `--as`: `npx notemend reply 3 "…" --as codex` — threads in the bar and the
238
- cabinet then show Codex rather than Claude. Gemini CLI and Codex are recognised from their
239
- environment without the flag; anything unnamed is Claude, as before.
240
-
241
- ### MCP
242
-
243
- `npx notemend mcp` serves the same queue as an MCP server over stdio, for agents that
244
- take tools rather than a shell: `list_comments`, `reply`, `resolve`, `reopen` and
245
- `screenshot`. Each runs the CLI above, so the cloud, agent keys and sign-in work as they
246
- do there; replies are signed by the client's own name (Cursor, Codex…) unless `--as`
247
- says otherwise. Run it from the project, or pass `--root <dir>`.
248
-
249
- One click: Add to Cursor, Add to VS Code and the Claude Desktop bundle are on
250
- https://app.notemend.com/docs/mcp. Claude Code takes the plugin — the hooks and the tools
251
- in one — with `/plugin marketplace add https://app.notemend.com/claude/marketplace.json`
252
- and `/plugin install notemend@notemend` (its files are in `plugin/`; `npm run pack` puts
253
- them at the package root). `npm run mcpb` rebuilds `web/public/notemend.mcpb` after a pack.
254
-
255
- ```sh
256
- claude mcp add notemend -- npx notemend mcp # Claude Code (the hooks above already cover it)
257
- codex mcp add notemend -- npx notemend mcp # Codex
258
- ```
259
-
260
- Cursor, `.cursor/mcp.json`; Claude Desktop and Zed take the same command:
261
-
262
- ```json
263
- { "mcpServers": { "notemend": { "command": "npx", "args": ["notemend", "mcp", "--root", "/path/to/site"] } } }
264
- ```
265
-
266
- ## Configuration
281
+ ### Configuration
267
282
 
268
283
  Everything lives in `notemend.config.mjs` at the project root — named exports or one
269
284
  default object. It is re-read on every page load: edit, refresh, no restart.
@@ -308,7 +323,7 @@ Under Astro the same keys can be passed to `notemend({ … })` in `astro.config`
308
323
  win over the file. `enabled` only works there: `notemend({ enabled: process.env.CI !== 'true' })` keeps
309
324
  the bar out of, say, a Playwright run.
310
325
 
311
- ### A private install
326
+ #### A private install
312
327
 
313
328
  Installed from a repo the build machine cannot reach (an `optionalDependency` on a
314
329
  private GitHub repo), import it behind a catch so a missing package never breaks the
@@ -332,7 +347,7 @@ export default defineConfig({ integrations: [...(notemend ? [notemend()] : [])]
332
347
  - The bar stays above the page's modal `<dialog>`s: while one is open, the bar moves into
333
348
  a popover host inside it (top layer, not inert) and back out when it closes.
334
349
  - Writes to `/__devbar/comments` from another origin are refused — any site open in the
335
- same browser could otherwise post a "comment" that Claude would read as a task.
350
+ same browser could otherwise post a "comment" an agent would read as a task.
336
351
  - The bar is sized in px on purpose: a site with a fluid root font-size would otherwise
337
352
  scale the dev UI along with the design.
338
353
  - A preview is an iframe, and some things only a real device or DevTools emulation shows:
package/bin/init.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import{existsSync as m,readFileSync as x,writeFileSync as $,readdirSync as K,mkdirSync as v,appendFileSync as I}from"node:fs";import{join as d,relative as E,extname as U,dirname as P}from"node:path";import{createInterface as H}from"node:readline/promises";import{titleOf as Y,titled as N,scanApp as V}from"../src/server/middleware.mjs";const z=new Set(["node_modules","dist",".astro",".next",".git",".vercel",".netlify","public"]);function*A(e){let s=[];try{s=K(e,{withFileTypes:!0})}catch{return}for(const a of s)a.isDirectory()?z.has(a.name)||(yield*A(d(e,a.name))):yield d(e,a.name)}function Q(e){const s=new Map,a=o=>{const i=Math.round(o);i>=320&&i<=2560&&s.set(i,(s.get(i)||0)+1)},t=(o,i)=>Number(o)*(i==="px"?1:16),r=["src","app","components","styles"].flatMap(o=>[...A(d(e,o))]);for(const o of r)if(/\.(css|scss|sass|less|astro|svelte|vue|jsx|tsx)$/.test(o)){for(const i of x(o,"utf8").split(`
2
- `))if(i.includes("@media")){for(const n of i.matchAll(/(min|max)-width\s*:\s*([\d.]+)(px|r?em)/g)){const c=t(n[2],n[3]);a(n[1]==="max"?Math.floor(c)+1:c)}for(const n of i.matchAll(/width\s*([<>]=?)\s*([\d.]+)(px|r?em)/g)){const c=t(n[2],n[3]);a(n[1]==="<="||n[1]===">"?Math.floor(c)+1:c)}}}return[...s].filter(([,o])=>o>1).sort((o,i)=>i[1]-o[1]).slice(0,4).map(([o])=>o).sort((o,i)=>i-o)}const X={2:["desktop","mobile"],3:["desktop","tablet","mobile"],4:["desktop","tablet","landscape","portrait"],5:["desktop","laptop","tablet","landscape","portrait"]},Z={desktop:"Desktop",laptop:"Laptop",tablet:"Tablet",landscape:"Mobile landscape",portrait:"Mobile portrait",mobile:"Mobile"},ee={desktop:1440,laptop:1280,tablet:834,landscape:667,portrait:390,mobile:390};function te(e){const s=[...new Set(e)].sort((t,r)=>r-t).slice(0,4);return(X[s.length+1]??["desktop"]).map((t,r)=>{const o=s[r]??0,i=r===0?1/0:s[r-1]-1;return{id:t,label:Z[t],min:o,max:i,ideal:Math.min(Math.max(ee[t],o),i)}})}function ne(e){const s=d(e,"src","pages"),a=[];for(const t of A(s)){const r=E(s,t).replace(/\\/g,"/");!/\.(astro|md|mdx|html)$/.test(r)||r.split("/").some(o=>o.startsWith("_"))||a.push({route:("/"+r.slice(0,-U(r).length)).replace(/\/index$/,"")||"/",file:t})}return a}function se(e,s="astro"){const a=s==="next"?V(e).map(n=>({route:n.pattern,file:n.entrypoint})):ne(e),t=[],r=[];for(const{route:n,file:c}of a)n.includes("[")?r.push({route:n,file:E(e,c).replace(/\\/g,"/")}):/^\/(404|500)$/.test(n)||t.push({route:n,name:Y(e,c)??N(n.split("/").pop()||"home")});const o={};for(const{route:n}of t){const c=n.split("/")[1];c&&(o[c]=(o[c]||0)+1)}for(const n of t){const c=n.route.split("/")[1];n.group=c&&o[c]>1?N(c):"Pages"}const i=n=>n==="Pages"?"":n;return t.sort((n,c)=>i(n.group).localeCompare(i(c.group))||n.route.localeCompare(c.route)),{pages:t,dynamic:r}}const w=e=>`'${String(e).replace(/\\/g,"\\\\").replace(/'/g,"\\'")}'`;function oe({bands:e,pages:s,dynamic:a,grid:t,sanity:r}){const o=n=>` { id: ${w(n.id)}, label: ${w(n.label)}, min: ${n.min}, max: ${Number.isFinite(n.max)?n.max:"Infinity"}, ideal: ${n.ideal} },`,i=["// Notemend \u2014 the dev bar. Re-read on every page load: edit and refresh, no restart.","// Every export is optional; delete one to fall back to the default.","","// Where the layout switches, widest first. The widest band is your own window; the","// others open as a canvas at `ideal` and can be dragged anywhere inside the band.","export const breakpoints = [",...e.map(o),"];","","// The pages menu: route \u2192 [name, group]. A page left out still shows up, named by","// its <title> and grouped by folder. A number in front of a name only orders the",'// rows inside a group ("1.2. About") \u2014 the menu does not show it.',"export const pages = {",...s.map(n=>` ${w(n.route)}: [${w(n.name)}, ${w(n.group)}],`)];if(a.length){i.push(""," // Dynamic routes cannot be listed for you \u2014 name the addresses they serve:");for(const n of a){const c=n.route.replace(/\[\.{3}[^\]]+\]|\[[^\]]+\]/g,"example"),u=n.route.split("/")[1];i.push(` // ${w(c)}: [${w(N(u||"page"))}, ${w(u?N(u):"Pages")}], // ${n.file}`)}}return i.push("};",""),i.push("// The layout grid overlay borrows these classes from your own CSS."),i.push(`export const grid = { container: ${w(t.container)}, grid: ${w(t.grid)}, columns: ${t.columns} };`),r&&i.push("","// Adds a toggle that hides Sanity's visual-editing overlay.","export const sanity = true;"),i.join(`
1
+ import{existsSync as m,readFileSync as x,writeFileSync as $,readdirSync as K,mkdirSync as v,appendFileSync as I}from"node:fs";import{join as d,relative as E,extname as U,dirname as P}from"node:path";import{createInterface as H}from"node:readline/promises";import{titleOf as Y,titled as N,scanApp as V}from"../src/server/middleware.mjs";const z=new Set(["node_modules","dist",".astro",".next",".git",".vercel",".netlify","public"]);function*C(e){let s=[];try{s=K(e,{withFileTypes:!0})}catch{return}for(const a of s)a.isDirectory()?z.has(a.name)||(yield*C(d(e,a.name))):yield d(e,a.name)}function Q(e){const s=new Map,a=o=>{const i=Math.round(o);i>=320&&i<=2560&&s.set(i,(s.get(i)||0)+1)},t=(o,i)=>Number(o)*(i==="px"?1:16),r=["src","app","components","styles"].flatMap(o=>[...C(d(e,o))]);for(const o of r)if(/\.(css|scss|sass|less|astro|svelte|vue|jsx|tsx)$/.test(o)){for(const i of x(o,"utf8").split(`
2
+ `))if(i.includes("@media")){for(const n of i.matchAll(/(min|max)-width\s*:\s*([\d.]+)(px|r?em)/g)){const c=t(n[2],n[3]);a(n[1]==="max"?Math.floor(c)+1:c)}for(const n of i.matchAll(/width\s*([<>]=?)\s*([\d.]+)(px|r?em)/g)){const c=t(n[2],n[3]);a(n[1]==="<="||n[1]===">"?Math.floor(c)+1:c)}}}return[...s].filter(([,o])=>o>1).sort((o,i)=>i[1]-o[1]).slice(0,4).map(([o])=>o).sort((o,i)=>i-o)}const X={2:["desktop","mobile"],3:["desktop","tablet","mobile"],4:["desktop","tablet","landscape","portrait"],5:["desktop","laptop","tablet","landscape","portrait"]},Z={desktop:"Desktop",laptop:"Laptop",tablet:"Tablet",landscape:"Mobile landscape",portrait:"Mobile portrait",mobile:"Mobile"},ee={desktop:1440,laptop:1280,tablet:834,landscape:667,portrait:390,mobile:390};function te(e){const s=[...new Set(e)].sort((t,r)=>r-t).slice(0,4);return(X[s.length+1]??["desktop"]).map((t,r)=>{const o=s[r]??0,i=r===0?1/0:s[r-1]-1;return{id:t,label:Z[t],min:o,max:i,ideal:Math.min(Math.max(ee[t],o),i)}})}function ne(e){const s=d(e,"src","pages"),a=[];for(const t of C(s)){const r=E(s,t).replace(/\\/g,"/");!/\.(astro|md|mdx|html)$/.test(r)||r.split("/").some(o=>o.startsWith("_"))||a.push({route:("/"+r.slice(0,-U(r).length)).replace(/\/index$/,"")||"/",file:t})}return a}function se(e,s="astro"){const a=s==="next"?V(e).map(n=>({route:n.pattern,file:n.entrypoint})):ne(e),t=[],r=[];for(const{route:n,file:c}of a)n.includes("[")?r.push({route:n,file:E(e,c).replace(/\\/g,"/")}):/^\/(404|500)$/.test(n)||t.push({route:n,name:Y(e,c)??N(n.split("/").pop()||"home")});const o={};for(const{route:n}of t){const c=n.split("/")[1];c&&(o[c]=(o[c]||0)+1)}for(const n of t){const c=n.route.split("/")[1];n.group=c&&o[c]>1?N(c):"Pages"}const i=n=>n==="Pages"?"":n;return t.sort((n,c)=>i(n.group).localeCompare(i(c.group))||n.route.localeCompare(c.route)),{pages:t,dynamic:r}}const w=e=>`'${String(e).replace(/\\/g,"\\\\").replace(/'/g,"\\'")}'`;function oe({bands:e,pages:s,dynamic:a,grid:t,sanity:r}){const o=n=>` { id: ${w(n.id)}, label: ${w(n.label)}, min: ${n.min}, max: ${Number.isFinite(n.max)?n.max:"Infinity"}, ideal: ${n.ideal} },`,i=["// Notemend \u2014 the dev bar. Re-read on every page load: edit and refresh, no restart.","// Every export is optional; delete one to fall back to the default.","","// Where the layout switches, widest first. The widest band is your own window; the","// others open as a canvas at `ideal` and can be dragged anywhere inside the band.","export const breakpoints = [",...e.map(o),"];","","// The pages menu: route \u2192 [name, group]. A page left out still shows up, named by","// its <title> and grouped by folder. A number in front of a name only orders the",'// rows inside a group ("1.2. About") \u2014 the menu does not show it.',"export const pages = {",...s.map(n=>` ${w(n.route)}: [${w(n.name)}, ${w(n.group)}],`)];if(a.length){i.push(""," // Dynamic routes cannot be listed for you \u2014 name the addresses they serve:");for(const n of a){const c=n.route.replace(/\[\.{3}[^\]]+\]|\[[^\]]+\]/g,"example"),u=n.route.split("/")[1];i.push(` // ${w(c)}: [${w(N(u||"page"))}, ${w(u?N(u):"Pages")}], // ${n.file}`)}}return i.push("};",""),i.push("// The layout grid overlay borrows these classes from your own CSS."),i.push(`export const grid = { container: ${w(t.container)}, grid: ${w(t.grid)}, columns: ${t.columns} };`),r&&i.push("","// Adds a toggle that hides Sanity's visual-editing overlay.","export const sanity = true;"),i.join(`
3
3
  `)+`
4
4
  `}const R=e=>`if [ -f "$CLAUDE_PROJECT_DIR/node_modules/notemend/bin/notemend.mjs" ]; then node "$CLAUDE_PROJECT_DIR/node_modules/notemend/bin/notemend.mjs" --root "$CLAUDE_PROJECT_DIR" ${e}; fi`;function ie(e){const s=d(e,".claude","settings.json");let a={};try{a=m(s)?JSON.parse(x(s,"utf8")):{}}catch{return console.warn(`[notemend] ${s} is not valid JSON \u2014 the Claude Code hooks were not added. Fix it and run npx notemend init again.`),0}a.hooks??={};let t=0;for(const[r,o]of[["SessionStart","digest"],["UserPromptSubmit","count"]]){const i=a.hooks[r]??=[];for(const n of i.flatMap(c=>c.hooks??[]))typeof n.command=="string"&&n.command.startsWith("[ -f")&&n.command.includes("notemend")&&(n.command=R(o),t++);JSON.stringify(i).includes("notemend")||(i.push({hooks:[{type:"command",command:R(o)}]}),t++)}return t&&(v(P(s),{recursive:!0}),$(s,JSON.stringify(a,null,2)+`
5
5
  `)),t}const M=(e,s)=>`D="\${CLAUDE_PROJECT_DIR:-\${GEMINI_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-$PWD}}}"; if [ -f "$D/node_modules/notemend/bin/notemend.mjs" ]; then node "$D/node_modules/notemend/bin/notemend.mjs" --root "$D" ${s} --format ${e}; fi`,ae=e=>{try{return m(e)?JSON.parse(x(e,"utf8")):{}}catch{return null}},re=(e,s)=>{v(P(e),{recursive:!0}),$(e,JSON.stringify(s,null,2)+`
@@ -12,7 +12,7 @@ codex_hooks = true
12
12
  `)?`
13
13
  `:""}${t?`
14
14
  `:""}${de}`),1)}const le=`# Notemend: the agent works the page-comment queue on its own (npx notemend init --ci).
15
- # Secrets: NOTEMEND_KEY (project settings \u2192 Cloud agent \u2192 Make a key), ANTHROPIC_API_KEY.
15
+ # Secrets: NOTEMEND_KEY (project settings \u2192 Agent on GitHub \u2192 Make a key), ANTHROPIC_API_KEY.
16
16
  name: Notemend
17
17
  on:
18
18
  schedule: [{ cron: "0 * * * *" }]
@@ -56,9 +56,9 @@ jobs:
56
56
  transpilePackages: ['notemend'],`):o.push(`add to ${u}: transpilePackages: ['notemend'],`)),u&&f!==k&&($(d(e,u),f),r.push(`${u} (transpilePackages)`)),{written:r,manual:o}}async function we(e,s){const a=s.includes("--yes")||s.includes("-y")||!process.stdin.isTTY,t=s.includes("--force"),r=d(e,"package.json"),o=m(r)?JSON.parse(x(r,"utf8")):{},i={...o.dependencies,...o.devDependencies,...o.optionalDependencies},n=i.astro?"astro":i.next?"next":null;n||(console.error(`Neither astro nor next in ${E(process.cwd(),r)||"package.json"} \u2014 notemend is a dev bar for Astro and Next.js.`),process.exit(1));const c=a?null:H({input:process.stdin,output:process.stdout}),u=async(l,g)=>{if(!c)return g;let b="";try{b=(await c.question(`${l} [${g}] `)).trim()}catch{console.log(`
57
57
  Stopped.`),process.exit(130)}return b||g},k=async l=>!/^n/i.test(await u(l,"Y"));console.log(`Setting up notemend in ${e}
58
58
  `);const f=d(e,"notemend.config.mjs"),h=[];if(m(f)&&!t)console.log(`notemend.config.mjs is already here \u2014 leaving it alone (--force to start over).
59
- `);else{let l=Q(e),g="your CSS";!l.length&&i.tailwindcss&&(l=[1024,768,640],g="Tailwind's lg / md / sm"),l.length||(l=[992,768,480],g="the defaults");const S=((await u(`Breakpoints \u2014 the widths where your layout switches, from ${g}:`,l.join(" "))).match(/\d+/g)||[]).map(Number).filter(y=>y>=200&&y<=4e3),O=te(S.length?S:l);for(const y of O)console.log(` ${y.label.padEnd(17)} ${y.min}\u2013${Number.isFinite(y.max)?y.max:"\u221E"}, opens at ${y.ideal}`);const{pages:_,dynamic:C}=se(e,n);console.log(`
60
- Pages: ${_.length} found in ${n==="next"?"app/":"src/pages"}${C.length?`, ${C.length} dynamic (listed as examples to fill in)`:""}.`);const D=[...new Set(_.map(y=>y.group))];D.length>1&&console.log(` grouped by folder: ${D.join(", ")}`);const W=await u(`
61
- Grid overlay \u2014 container class, grid class, columns:`,"container grid 12"),[G="container",L="grid",q="12"]=W.split(/[\s,]+/),B=!!(i["@sanity/astro"]||i["next-sanity"]||i.sanity);$(f,oe({bands:O,pages:_,dynamic:C,grid:{container:G,grid:L,columns:Number(q)||12},sanity:B})),h.push("notemend.config.mjs")}const p=n==="astro"?pe(e,!!o.optionalDependencies?.notemend):{};p.status==="added"&&h.push(`${E(e,p.file)} (added notemend())`);const T=n==="next"?me(e):{written:[],manual:[]};h.push(...T.written);const F=["claude","codex","gemini","cursor"].filter(l=>l==="claude"||m(d(e,`.${l}`))),j=(s.find(l=>l.startsWith("--agents="))?.slice(9)??await u(`
59
+ `);else{let l=Q(e),g="your CSS";!l.length&&i.tailwindcss&&(l=[1024,768,640],g="Tailwind's lg / md / sm"),l.length||(l=[992,768,480],g="the defaults");const S=((await u(`Breakpoints \u2014 the widths where your layout switches, from ${g}:`,l.join(" "))).match(/\d+/g)||[]).map(Number).filter(y=>y>=200&&y<=4e3),O=te(S.length?S:l);for(const y of O)console.log(` ${y.label.padEnd(17)} ${y.min}\u2013${Number.isFinite(y.max)?y.max:"\u221E"}, opens at ${y.ideal}`);const{pages:_,dynamic:A}=se(e,n);console.log(`
60
+ Pages: ${_.length} found in ${n==="next"?"app/":"src/pages"}${A.length?`, ${A.length} dynamic (listed as examples to fill in)`:""}.`);const D=[...new Set(_.map(y=>y.group))];D.length>1&&console.log(` grouped by folder: ${D.join(", ")}`);const G=await u(`
61
+ Grid overlay \u2014 container class, grid class, columns:`,"container grid 12"),[W="container",L="grid",q="12"]=G.split(/[\s,]+/),B=!!(i["@sanity/astro"]||i["next-sanity"]||i.sanity);$(f,oe({bands:O,pages:_,dynamic:A,grid:{container:W,grid:L,columns:Number(q)||12},sanity:B})),h.push("notemend.config.mjs")}const p=n==="astro"?pe(e,!!o.optionalDependencies?.notemend):{};p.status==="added"&&h.push(`${E(e,p.file)} (added notemend())`);const T=n==="next"?me(e):{written:[],manual:[]};h.push(...T.written);const F=["claude","codex","gemini","cursor"].filter(l=>l==="claude"||m(d(e,`.${l}`))),j=(s.find(l=>l.startsWith("--agents="))?.slice(9)??await u(`
62
62
  Which coding agents should see open comments? (claude, codex, gemini, cursor)`,F.join(","))).toLowerCase().split(/[\s,]+/).filter(l=>["claude","codex","gemini","cursor"].includes(l));j.includes("claude")&&ie(e)&&h.push(".claude/settings.json (hooks)");for(const l of j.filter(g=>g!=="claude"))ce(e,l)&&h.push({codex:".codex/hooks.json",gemini:".gemini/settings.json",cursor:".cursor/hooks.json"}[l]+" (hooks)");if(J(e)&&h.push("AGENTS.md (how to answer comments)"),j.includes("gemini")&&J(e,"GEMINI.md")&&h.push("GEMINI.md"),await k("Keep pasted screenshots out of git?")){const l=d(e,".gitignore"),g=m(l)?x(l,"utf8"):"",b=ue.filter(S=>!g.split(`
63
63
  `).includes(S));b.length&&(I(l,`${g&&!g.endsWith(`
64
64
  `)?`