@dorsk/yubisashi 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dorsk
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,66 @@
1
+ # yubisashi 指差し
2
+
3
+ Point at your running app, say what's wrong, and the coding agent working on it gets the
4
+ element's source `file:line`, the components that rendered it, and your comment. It answers,
5
+ edits, and hot-reload shows you the result. The agent's conversation streams in a panel next to
6
+ the app.
7
+
8
+ ![pick mode](docs/pick.png)
9
+
10
+ ## How it works
11
+
12
+ `yubi up` puts a small proxy in front of your dev server. Everything passes through to the app,
13
+ including HMR websockets. Frame-blocking headers are stripped, so the app can be shown inside
14
+ the review shell at `/__yubi/`:
15
+
16
+ - **Comment**: press `C` or the ⌖ button, then click an element. Shift-click adds more
17
+ elements; `Esc` cancels. A glass layer takes the clicks, so the app doesn't react, and
18
+ disabled controls can be picked too.
19
+ - **Source location** comes from Svelte 5's dev-mode `__svelte_meta`, which gives exact
20
+ `file:line:col` plus the component chain. For other frameworks you still get the selector,
21
+ text and HTML.
22
+ - **Conversation tab**: follows the agent's Claude Code transcript live
23
+ (`~/.claude/projects/*/<session>.jsonl`).
24
+ - **Comments tab**: one thread per comment, with status and a pin on the element.
25
+
26
+ Your comments reach the agent through `yubi wait`. The agent runs it as a background command,
27
+ it blocks until you send something, then exits with your messages. Claude Code wakes the agent
28
+ when a background command exits, so it needs no MCP server, no special launch flags, and no
29
+ polling. The same works in any Claude Code session, including cctui ones.
30
+
31
+ ![comments](docs/comments.png)
32
+
33
+ ## Use
34
+
35
+ ```sh
36
+ # from the app directory, as a background command in the agent's session
37
+ yubi up --target http://localhost:5173 -- npm run dev
38
+ yubi url # the link to open (carries the access token)
39
+ yubi wait # background: exits with the user's next messages
40
+ yubi reply 3 "…" # answer in thread #3
41
+ yubi resolve 3 "…" # close it with what changed
42
+ yubi dismiss 3 "…" # close it without acting
43
+ yubi list [--all]
44
+ ```
45
+
46
+ `skill/SKILL.md` teaches an agent this loop. Link it into `~/.claude/skills/yubisashi` so every
47
+ session knows it.
48
+
49
+ State lives in `.yubisashi/` in the app directory. It's added to `.git/info/exclude`
50
+ automatically and holds the comments, the token and the server address.
51
+
52
+ ## Security
53
+
54
+ The API lets anyone who can reach it inject prompts into your agent's session. It binds to
55
+ 127.0.0.1 by default, and every API call needs the random token in `.yubisashi/token`, which
56
+ also stops other sites in your browser from posting to it. Think twice before `--host 0.0.0.0`.
57
+
58
+ ## Develop
59
+
60
+ Node ≥ 22.18 runs the TypeScript sources directly; there is no build step.
61
+
62
+ ```sh
63
+ npm install
64
+ npm test # unit, proxy/API integration, and a Playwright end-to-end run
65
+ npm run check # tsc + biome
66
+ ```
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@dorsk/yubisashi",
3
+ "version": "0.1.0",
4
+ "description": "Point at your running app, comment, and hand it to the coding agent working on it.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Dorsk <dorsk.dev@gmail.com>",
8
+ "engines": {
9
+ "node": ">=22.18"
10
+ },
11
+ "bin": {
12
+ "yubi": "src/cli.ts"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "shell",
17
+ "skill"
18
+ ],
19
+ "scripts": {
20
+ "check": "tsc -p tsconfig.json && biome check .",
21
+ "format": "biome check --write .",
22
+ "test": "node --test test/*.test.ts"
23
+ },
24
+ "devDependencies": {
25
+ "@biomejs/biome": "^2.2.0",
26
+ "@types/node": "^22.18.0",
27
+ "playwright": "^1.55.0",
28
+ "typescript": "^5.9.0"
29
+ }
30
+ }
package/shell/app.css ADDED
@@ -0,0 +1,436 @@
1
+ :root {
2
+ --bg: #111214;
3
+ --panel: #18191c;
4
+ --raised: #212226;
5
+ --line: #2c2e33;
6
+ --text: #e6e6e8;
7
+ --muted: #8b8d94;
8
+ --accent: #f0883e;
9
+ --ok: #3fb950;
10
+ --warn: #d29922;
11
+ --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
12
+ --mono: ui-monospace, "JetBrains Mono", "SF Mono", monospace;
13
+ color-scheme: dark;
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ html,
21
+ body {
22
+ height: 100%;
23
+ margin: 0;
24
+ }
25
+
26
+ body {
27
+ display: flex;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ font: 13px / 1.45 var(--font);
31
+ }
32
+
33
+ button {
34
+ font: inherit;
35
+ color: inherit;
36
+ background: var(--raised);
37
+ border: 1px solid var(--line);
38
+ border-radius: 6px;
39
+ padding: 4px 10px;
40
+ cursor: pointer;
41
+ }
42
+
43
+ button:hover {
44
+ border-color: var(--muted);
45
+ }
46
+
47
+ #stage {
48
+ flex: 1;
49
+ display: flex;
50
+ flex-direction: column;
51
+ min-width: 0;
52
+ }
53
+
54
+ #bar {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 8px;
58
+ padding: 6px 10px;
59
+ border-bottom: 1px solid var(--line);
60
+ }
61
+
62
+ #bar a {
63
+ color: var(--muted);
64
+ text-decoration: none;
65
+ padding: 0 4px;
66
+ }
67
+
68
+ #pick.on {
69
+ background: var(--accent);
70
+ border-color: var(--accent);
71
+ color: #111;
72
+ }
73
+
74
+ #route {
75
+ flex: 1;
76
+ color: var(--muted);
77
+ font-family: var(--mono);
78
+ font-size: 12px;
79
+ overflow: hidden;
80
+ white-space: nowrap;
81
+ text-overflow: ellipsis;
82
+ }
83
+
84
+ #viewport {
85
+ position: relative;
86
+ flex: 1;
87
+ min-height: 0;
88
+ }
89
+
90
+ #app {
91
+ width: 100%;
92
+ height: 100%;
93
+ border: 0;
94
+ background: #fff;
95
+ display: block;
96
+ }
97
+
98
+ #glass {
99
+ position: absolute;
100
+ inset: 0;
101
+ cursor: crosshair;
102
+ }
103
+
104
+ #glass[hidden] {
105
+ display: none;
106
+ }
107
+
108
+ #overlay {
109
+ position: absolute;
110
+ inset: 0;
111
+ pointer-events: none;
112
+ overflow: hidden;
113
+ }
114
+
115
+ .box {
116
+ position: absolute;
117
+ border: 2px solid var(--accent);
118
+ border-radius: 3px;
119
+ background: rgb(240 136 62 / 0.08);
120
+ }
121
+
122
+ .box.hover {
123
+ border-style: dashed;
124
+ background: rgb(240 136 62 / 0.05);
125
+ }
126
+
127
+ .box .tag {
128
+ position: absolute;
129
+ top: -20px;
130
+ left: -2px;
131
+ padding: 1px 6px;
132
+ border-radius: 3px 3px 3px 0;
133
+ background: var(--accent);
134
+ color: #111;
135
+ font: 11px / 18px var(--mono);
136
+ white-space: nowrap;
137
+ }
138
+
139
+ .pin {
140
+ position: absolute;
141
+ width: 22px;
142
+ height: 22px;
143
+ margin: -11px 0 0 -11px;
144
+ border-radius: 11px 11px 11px 2px;
145
+ background: var(--accent);
146
+ color: #111;
147
+ font: 600 11px / 22px var(--font);
148
+ text-align: center;
149
+ pointer-events: auto;
150
+ cursor: pointer;
151
+ box-shadow: 0 1px 4px rgb(0 0 0 / 0.4);
152
+ }
153
+
154
+ #panel {
155
+ width: 420px;
156
+ display: flex;
157
+ flex-direction: column;
158
+ background: var(--panel);
159
+ border-left: 1px solid var(--line);
160
+ }
161
+
162
+ #panel > header {
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: space-between;
166
+ padding: 6px 10px;
167
+ border-bottom: 1px solid var(--line);
168
+ }
169
+
170
+ .tabs {
171
+ display: flex;
172
+ gap: 4px;
173
+ }
174
+
175
+ .tabs button {
176
+ background: none;
177
+ border-color: transparent;
178
+ color: var(--muted);
179
+ }
180
+
181
+ .tabs button.active {
182
+ color: var(--text);
183
+ background: var(--raised);
184
+ border-color: var(--line);
185
+ }
186
+
187
+ #status {
188
+ font-size: 12px;
189
+ color: var(--muted);
190
+ }
191
+
192
+ #status::before {
193
+ content: "";
194
+ display: inline-block;
195
+ width: 8px;
196
+ height: 8px;
197
+ margin-right: 6px;
198
+ border-radius: 50%;
199
+ background: var(--warn);
200
+ }
201
+
202
+ #status.on::before {
203
+ background: var(--ok);
204
+ }
205
+
206
+ .feed {
207
+ flex: 1;
208
+ overflow-y: auto;
209
+ padding: 12px;
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: 8px;
213
+ }
214
+
215
+ .feed > * {
216
+ flex-shrink: 0;
217
+ }
218
+
219
+ .feed[hidden] {
220
+ display: none;
221
+ }
222
+
223
+ .empty {
224
+ color: var(--muted);
225
+ text-align: center;
226
+ margin: auto 0;
227
+ }
228
+
229
+ .msg {
230
+ max-width: 100%;
231
+ overflow-wrap: anywhere;
232
+ }
233
+
234
+ .msg.user {
235
+ align-self: flex-end;
236
+ max-width: 88%;
237
+ background: var(--raised);
238
+ border: 1px solid var(--line);
239
+ border-radius: 10px 10px 2px 10px;
240
+ padding: 6px 10px;
241
+ white-space: pre-wrap;
242
+ }
243
+
244
+ .msg.assistant p {
245
+ margin: 0 0 6px;
246
+ }
247
+
248
+ .msg.tool {
249
+ color: var(--muted);
250
+ font: 11.5px / 1.4 var(--mono);
251
+ white-space: nowrap;
252
+ overflow: hidden;
253
+ text-overflow: ellipsis;
254
+ }
255
+
256
+ .msg.event {
257
+ align-self: center;
258
+ color: var(--muted);
259
+ font-size: 11.5px;
260
+ }
261
+
262
+ code {
263
+ font-family: var(--mono);
264
+ font-size: 12px;
265
+ background: var(--raised);
266
+ padding: 0 4px;
267
+ border-radius: 4px;
268
+ }
269
+
270
+ pre {
271
+ background: var(--bg);
272
+ border: 1px solid var(--line);
273
+ border-radius: 6px;
274
+ padding: 8px;
275
+ overflow-x: auto;
276
+ white-space: pre;
277
+ }
278
+
279
+ pre code {
280
+ background: none;
281
+ padding: 0;
282
+ }
283
+
284
+ .card {
285
+ border: 1px solid var(--line);
286
+ border-radius: 8px;
287
+ padding: 8px 10px;
288
+ background: var(--raised);
289
+ }
290
+
291
+ .card.focus {
292
+ border-color: var(--accent);
293
+ }
294
+
295
+ .card.closed {
296
+ opacity: 0.6;
297
+ }
298
+
299
+ .card header {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 8px;
303
+ margin-bottom: 4px;
304
+ }
305
+
306
+ .card .id {
307
+ font-weight: 600;
308
+ }
309
+
310
+ .badge {
311
+ font-size: 11px;
312
+ padding: 0 6px;
313
+ border-radius: 8px;
314
+ border: 1px solid var(--line);
315
+ color: var(--muted);
316
+ }
317
+
318
+ .badge.open {
319
+ color: var(--accent);
320
+ border-color: var(--accent);
321
+ }
322
+
323
+ .badge.resolved {
324
+ color: var(--ok);
325
+ border-color: var(--ok);
326
+ }
327
+
328
+ .card .where {
329
+ font: 11.5px / 1.4 var(--mono);
330
+ color: var(--muted);
331
+ overflow-wrap: anywhere;
332
+ }
333
+
334
+ .card .thread {
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 4px;
338
+ margin: 6px 0;
339
+ }
340
+
341
+ .card .thread .user {
342
+ white-space: pre-wrap;
343
+ }
344
+
345
+ .card .thread p {
346
+ margin: 0;
347
+ }
348
+
349
+ .card .thread .agent {
350
+ color: var(--muted);
351
+ border-left: 2px solid var(--line);
352
+ padding-left: 8px;
353
+ }
354
+
355
+ .card .actions {
356
+ display: flex;
357
+ gap: 6px;
358
+ justify-content: flex-end;
359
+ }
360
+
361
+ .card .actions button {
362
+ padding: 2px 8px;
363
+ font-size: 12px;
364
+ }
365
+
366
+ #composer {
367
+ border-top: 1px solid var(--line);
368
+ padding: 8px 10px 10px;
369
+ }
370
+
371
+ #context {
372
+ display: flex;
373
+ flex-wrap: wrap;
374
+ gap: 4px;
375
+ margin-bottom: 6px;
376
+ }
377
+
378
+ #context:empty {
379
+ display: none;
380
+ }
381
+
382
+ .chip {
383
+ display: inline-flex;
384
+ align-items: center;
385
+ gap: 4px;
386
+ max-width: 100%;
387
+ padding: 1px 4px 1px 8px;
388
+ border: 1px solid var(--accent);
389
+ border-radius: 10px;
390
+ font: 11.5px / 1.5 var(--mono);
391
+ overflow: hidden;
392
+ white-space: nowrap;
393
+ text-overflow: ellipsis;
394
+ }
395
+
396
+ .chip button {
397
+ border: 0;
398
+ padding: 0 4px;
399
+ background: none;
400
+ color: var(--muted);
401
+ }
402
+
403
+ textarea {
404
+ width: 100%;
405
+ resize: vertical;
406
+ font: inherit;
407
+ color: var(--text);
408
+ background: var(--bg);
409
+ border: 1px solid var(--line);
410
+ border-radius: 8px;
411
+ padding: 8px;
412
+ }
413
+
414
+ textarea:focus {
415
+ outline: none;
416
+ border-color: var(--muted);
417
+ }
418
+
419
+ #composer .row {
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: space-between;
423
+ margin-top: 6px;
424
+ }
425
+
426
+ #hint {
427
+ color: var(--muted);
428
+ font-size: 11.5px;
429
+ }
430
+
431
+ #composer button[type="submit"] {
432
+ background: var(--accent);
433
+ border-color: var(--accent);
434
+ color: #111;
435
+ font-weight: 600;
436
+ }