@brmbh/tack 0.0.0-stage → 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.
- package/README.md +182 -2
- package/bin/tack.cjs +23 -0
- package/package.json +34 -5
package/README.md
CHANGED
|
@@ -1,3 +1,183 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Point at your running site, say what should change, and hand it to your coding agent.
|
|
4
|
+
|
|
5
|
+
`tack` puts a local proxy in front of your dev server and adds a small toolbar
|
|
6
|
+
to every page. Click an element to comment on it, or click any text and retype
|
|
7
|
+
it in place. Each note stores the page, a verified CSS selector, the element's
|
|
8
|
+
text and HTML, and the **source file:line it most likely comes from**. Your
|
|
9
|
+
agent (Claude Code, Cursor, Codex…) reads the notes, changes the code, and
|
|
10
|
+
resolves them. The pin leaves the page once it's done; the note and the agent's answer stay in the list (`L` → All).
|
|
11
|
+
|
|
12
|
+
Nothing gets installed in your project, and it works with any framework: static
|
|
13
|
+
HTML, Vite, Next, Astro, WordPress, PHP. It runs only on your machine.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
**With your agent** (Claude Code, Codex, Cursor, Gemini CLI, Copilot…): paste this into it, in your project.
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
Set me up to use Tack in this project. Run `npx -y @brmbh/tack@latest init` in the project root;
|
|
21
|
+
it installs the tack skill for this agent. Read the installed skill (.claude/skills/tack/SKILL.md
|
|
22
|
+
or .agents/skills/tack/SKILL.md), then start a tack session on my dev server and give me the URL.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**From the command line**, like any other tool:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @brmbh/tack init # in your project: installs the agent skill
|
|
29
|
+
npx @brmbh/tack --target http://localhost:3000
|
|
30
|
+
npm i -g @brmbh/tack # optional: a `tack` command everywhere
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Needs Node 18+ for `npx`, nothing else: tack ships as a standalone binary for macOS (arm64, x64),
|
|
34
|
+
Linux (x64, arm64) and Windows (x64). `tack init` writes the skill to `.claude/skills/tack/`
|
|
35
|
+
(Claude Code) and `.agents/skills/tack/` (Codex, Cursor, Gemini CLI, GitHub Copilot, Amp, Cline
|
|
36
|
+
and others). `--global` puts it in your home folder instead. Then tell your agent
|
|
37
|
+
"start tack", and later "do the tack notes".
|
|
38
|
+
|
|
39
|
+
## Use
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
tack --target http://localhost:3000 # → http://localhost:7777
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open the printed URL and browse as usual.
|
|
46
|
+
|
|
47
|
+
| Key | |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `C` | Comment mode: click an element and type. Select text first to quote it. |
|
|
50
|
+
| `E` | Edit mode: click text, retype it, `Enter` saves (`Shift+Enter` = newline), `Esc` cancels. |
|
|
51
|
+
| `L` | Feedback list (all pages). Copy button = Markdown for any agent. |
|
|
52
|
+
| `H` | Show/hide pins. |
|
|
53
|
+
| `Esc` | Leave the current mode. |
|
|
54
|
+
|
|
55
|
+
Pins show open notes only (black). A dot on a pin means the agent asked a question. Done notes leave the canvas and stay in the list (`L` → All), with the agent's answer.
|
|
56
|
+
|
|
57
|
+
## Hand it to your agent
|
|
58
|
+
|
|
59
|
+
Feedback is written to `<project>/.tack/`, as `notes.json` (the source of truth) and `FEEDBACK.md`
|
|
60
|
+
(readable by anything). Three ways for an agent to pick it up:
|
|
61
|
+
|
|
62
|
+
1. **Skill** (Claude Code, Codex, Cursor and every agent that reads `.agents/skills`): run
|
|
63
|
+
`tack init` in the project, or `tack init --global`, then tell the agent "do the tack notes".
|
|
64
|
+
2. **MCP**: `claude mcp add tack -- tack mcp`. Tools: `tack_list`, `tack_get`,
|
|
65
|
+
`tack_resolve`, `tack_reply`.
|
|
66
|
+
3. **CLI** (any agent that can run shell commands):
|
|
67
|
+
```bash
|
|
68
|
+
tack list --md
|
|
69
|
+
tack resolve 3 -m "Shortened the lead, index.html:1370"
|
|
70
|
+
tack reply 4 -m "Bolder as in weight 700, or bigger?"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Commit `.tack/` if you want the review history in git, or add it to `.gitignore`.
|
|
74
|
+
|
|
75
|
+
## Console bridge
|
|
76
|
+
|
|
77
|
+
The proxy also sees what the page does, so the agent doesn't have to ask you to open DevTools.
|
|
78
|
+
A tiny script at the top of `<head>` captures `console.*`, uncaught errors (with
|
|
79
|
+
`file:line:col`), unhandled rejections, failed `fetch` calls and broken images/scripts. The
|
|
80
|
+
proxy adds upstream 4xx/5xx responses and **missing files that the dev server answered with
|
|
81
|
+
its HTML fallback** (Vite serves `index.html` with status 200 for any unknown path, so a
|
|
82
|
+
missing `.json` or `.png` looks like success). Everything goes to `.tack/console.jsonl`.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
tack logs --md # last 50 entries
|
|
86
|
+
tack logs --level problems --since 5m # errors, warnings, network, proxy
|
|
87
|
+
tack logs --page /pricing/ --md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
MCP: `tack_console`. Every new note also carries the problems from **its own page load**,
|
|
91
|
+
with repeats collapsed. `tack --verbose` prints every proxied request.
|
|
92
|
+
|
|
93
|
+
## AI suggestions (optional)
|
|
94
|
+
|
|
95
|
+
In edit mode (`E`), the popover has **Suggest**: say what the new versions should do (or leave it
|
|
96
|
+
empty) and tack streams a few versions of the text. Pick one, it appears on the page, `Enter`
|
|
97
|
+
saves it as a normal edit note for your agent.
|
|
98
|
+
|
|
99
|
+
Any model, your own key, via the [Vercel AI SDK](https://ai-sdk.dev). Set it up in the
|
|
100
|
+
overlay: the **gear icon** opens AI settings. Pick where the model runs (any OpenAI-compatible
|
|
101
|
+
endpoint such as Ollama Cloud, OpenRouter, LM Studio or a local Ollama, or the Vercel AI
|
|
102
|
+
Gateway), paste your key, name the model, **Save & test connection**.
|
|
103
|
+
|
|
104
|
+
The key is stored on your computer in `~/.config/tack/settings.json` (mode 600), for all
|
|
105
|
+
projects. Never in the project, never in the browser; the overlay can write it but only ever
|
|
106
|
+
reads back `••••` plus the last four characters. Environment variables win over the file:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
TACK_BASE_URL=https://ollama.com/v1 TACK_API_KEY=… TACK_MODEL=minimax-m3:cloud # OpenAI-compatible
|
|
110
|
+
AI_GATEWAY_API_KEY=… TACK_MODEL=anthropic/claude-sonnet-5.5 # AI Gateway
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Models without JSON-schema output answer in plain lines, which tack parses as they stream.
|
|
114
|
+
|
|
115
|
+
The model gets the element, its page, the source around the likely file:line and your
|
|
116
|
+
`AGENTS.md` / `CLAUDE.md` / `README.md`, and can **read** more itself (list, search, read files
|
|
117
|
+
in the project). Point it at notes outside the repo with `.tack/config.json`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{ "knowledge": ["~/notes/my-project"] }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
It never writes files, and refuses `.env*`, keys and `.git/`. What it reads is sent to the
|
|
124
|
+
model provider you chose.
|
|
125
|
+
|
|
126
|
+
## Options
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
--target <url> dev server to proxy (required)
|
|
130
|
+
--port <n> proxy port (default 7777)
|
|
131
|
+
--dir <path> project root: where .tack/ lives and source hints are searched (default cwd)
|
|
132
|
+
--store <path> put notes somewhere else
|
|
133
|
+
--host <addr> bind address (default 127.0.0.1)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## How it works
|
|
137
|
+
|
|
138
|
+
- **Proxy** (`src/server.ts`): forwards every request to the target, injects
|
|
139
|
+
`/__tack/overlay.js` into HTML responses, rewrites absolute links to the dev server so
|
|
140
|
+
you stay inside the proxy, strips CSP headers, and pipes websockets through so hot
|
|
141
|
+
reload keeps working.
|
|
142
|
+
- **Same origin**: the proxy serves the page from its own address, so the overlay can
|
|
143
|
+
read the real DOM, which a screenshot tool can't. Selectors come from the actual
|
|
144
|
+
element and are checked for uniqueness when the note is captured.
|
|
145
|
+
- **Overlay** (`overlay/overlay.ts`): vanilla TypeScript in a shadow root, bundled by
|
|
146
|
+
Bun when requested (prebuilt into the release binary). The site's CSS can't reach it.
|
|
147
|
+
- **Source hints** (`src/hints.ts`): a deterministic text search over the project
|
|
148
|
+
(git-tracked files, minus build output). It still matches when text is wrapped across
|
|
149
|
+
lines, contains HTML entities or smart quotes, or is split by inline tags. It falls
|
|
150
|
+
back to `id`, then class names.
|
|
151
|
+
- **MCP** (`src/mcp.ts`): about 100 lines of JSON-RPC over stdio, no SDK.
|
|
152
|
+
- **Stable targets**: clicks on anonymous wrappers inside a heading or paragraph (split-text
|
|
153
|
+
lines and words, styling spans) snap to the heading/paragraph. Runtime classes
|
|
154
|
+
(`is-*`, `split-*`, `line`, `word`…) never go into selectors, and a pin is only drawn if the
|
|
155
|
+
element's tag and text still match. Otherwise tack looks for the element by text.
|
|
156
|
+
- **Redirects** pass through with their `Location` (rewritten when it points back at the
|
|
157
|
+
target). A redirect to another host (apex → `www`, locale subdomains) leaves the proxy: use
|
|
158
|
+
the final URL as `--target`.
|
|
159
|
+
- **Revalidation**: conditional request headers are stripped, so every request gets a full
|
|
160
|
+
response. A passed-through 304 had turned into an empty 200 (a broken web manifest).
|
|
161
|
+
|
|
162
|
+
## Develop
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
bun install
|
|
166
|
+
bun test
|
|
167
|
+
bunx tsc --noEmit
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Requires [Bun](https://bun.sh) ≥ 1.2 to develop; users get a compiled binary.
|
|
171
|
+
|
|
172
|
+
## Release
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
bun run build # dist/npm/tack + dist/npm/tack-<platform> (5 binaries)
|
|
176
|
+
npm login # once, account with publish rights on @brmbh
|
|
177
|
+
for d in dist/npm/tack-*; do (cd "$d" && npm publish --access public); done
|
|
178
|
+
(cd dist/npm/tack && npm publish --access public) # last: it depends on the binaries
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Bump `version` in `package.json` first; every package gets the same version.
|
|
182
|
+
Known limit: the darwin-x64 binary crashes under Rosetta (x64 Node on Apple Silicon), real
|
|
183
|
+
Intel Macs are fine. Use an arm64 Node on Apple Silicon.
|
package/bin/tack.cjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the tack binary that npm installed for this platform (@brmbh/tack-<os>-<arch>).
|
|
3
|
+
const { spawn } = require('node:child_process')
|
|
4
|
+
|
|
5
|
+
const os = process.platform === 'win32' ? 'windows' : process.platform
|
|
6
|
+
const name = `@brmbh/tack-${os}-${process.arch}`
|
|
7
|
+
let bin
|
|
8
|
+
try {
|
|
9
|
+
bin = require.resolve(`${name}/bin/tack${process.platform === 'win32' ? '.exe' : ''}`)
|
|
10
|
+
} catch {
|
|
11
|
+
console.error(`tack: no binary for ${process.platform}-${process.arch} (${name} is not installed).`)
|
|
12
|
+
console.error('Supported: macOS arm64/x64, Linux x64/arm64, Windows x64. If yours is one, reinstall without --no-optional.')
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit' })
|
|
17
|
+
// The proxy runs until stopped: hand stop signals to it instead of orphaning it.
|
|
18
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => child.kill(sig))
|
|
19
|
+
child.on('error', (err) => {
|
|
20
|
+
console.error(`tack: could not start ${bin}: ${err.message}`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
})
|
|
23
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@brmbh/tack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Point at your running site, comment or edit text in place, and hand the feedback to your coding agent.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://github.com/brmbh/tack",
|
|
7
|
+
"author": "Jan Brombach",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"feedback",
|
|
10
|
+
"coding-agent",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"cursor",
|
|
13
|
+
"codex",
|
|
14
|
+
"proxy",
|
|
15
|
+
"review",
|
|
16
|
+
"skills"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"tack": "bin/tack.cjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"@brmbh/tack-darwin-arm64": "0.1.0",
|
|
30
|
+
"@brmbh/tack-darwin-x64": "0.1.0",
|
|
31
|
+
"@brmbh/tack-linux-x64": "0.1.0",
|
|
32
|
+
"@brmbh/tack-linux-arm64": "0.1.0",
|
|
33
|
+
"@brmbh/tack-windows-x64": "0.1.0"
|
|
34
|
+
}
|
|
35
|
+
}
|