rethocker 0.0.2 → 0.1.1
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 +379 -3
- package/bin/rethocker-native +0 -0
- package/package.json +22 -2
- package/src/actions.ts +282 -0
- package/src/daemon.ts +244 -0
- package/src/index.test.ts +63 -0
- package/src/index.ts +8 -1
- package/src/keys.ts +346 -0
- package/src/parse-key.ts +169 -0
- package/src/register-rule.ts +186 -0
- package/src/rethocker.ts +125 -0
- package/src/rule-engine.ts +101 -0
- package/src/rule-types.ts +169 -0
- package/src/scripts/debug-keys.ts +45 -0
- package/src/scripts/example.ts +74 -0
- package/src/types.ts +298 -0
- package/AGENTS.md +0 -106
- package/bun.lock +0 -26
- package/tsconfig.json +0 -29
package/README.md
CHANGED
|
@@ -1,15 +1,391 @@
|
|
|
1
1
|
# rethocker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Global key interception and remapping for macOS. Intercept any key or combo system-wide, remap keys, fire shell commands, react to key sequences, and scope rules to specific apps — all from TypeScript.
|
|
4
|
+
|
|
5
|
+
Requires **macOS 13+** and **Accessibility permission** (prompted automatically on first run).
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
|
|
10
|
+
bun add rethocker
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
## Usage
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
|
-
import rethocker from "rethocker"
|
|
16
|
+
import { actions, Key, rethocker } from "rethocker"
|
|
17
|
+
|
|
18
|
+
const rk = rethocker([
|
|
19
|
+
// Remap Caps Lock → Escape
|
|
20
|
+
// Caps Lock is handled transparently — no extra setup needed
|
|
21
|
+
{
|
|
22
|
+
key: Key.capsLock,
|
|
23
|
+
remap: Key.escape,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Remap key to key, chord to key, key to chord, or even chord to chord (type any sequence you want)
|
|
27
|
+
// Use Key.* constants for autocomplete and safe string interpolation
|
|
28
|
+
{
|
|
29
|
+
key: "Ctrl+H E",
|
|
30
|
+
remap: `h e l l o Shift+n1 n1 ${Key.delete}`,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Media / system keys are fully interceptable
|
|
34
|
+
// Use spaces for key sequences (steps pressed in order)
|
|
35
|
+
{
|
|
36
|
+
key: `${Key.brightnessDown} ${Key.brightnessUp}`,
|
|
37
|
+
execute: "open -a 'My App'",
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Sequence with app filter, consume, and a TypeScript handler
|
|
41
|
+
{
|
|
42
|
+
key: "Cmd+R T",
|
|
43
|
+
sequenceTimeoutMs: 10_000,
|
|
44
|
+
// exclude specific apps by bundle ID (prefix with ! to negate)
|
|
45
|
+
app: ["!com.google.Chrome", "!com.apple.Safari"],
|
|
46
|
+
// consume: swallow the keys so they don't reach the app
|
|
47
|
+
consume: true,
|
|
48
|
+
handler: async () => {
|
|
49
|
+
// handlers allow async/await and full access to the rk instance
|
|
50
|
+
await rk.execute(actions.window.halfTop())
|
|
51
|
+
// actions.* provide quick access to common macOS tasks
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
// Handle lifecycle events
|
|
57
|
+
rk.on("accessibilityDenied", () => {
|
|
58
|
+
console.error("Go to System Settings → Privacy & Security → Accessibility")
|
|
59
|
+
})
|
|
60
|
+
rk.on("error", (code, message) => console.error(`[${code}] ${message}`))
|
|
61
|
+
rk.on("exit", (code) => {
|
|
62
|
+
console.error(`daemon exited (${code}), restarting...`)
|
|
63
|
+
rk.start().catch(console.error)
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The daemon starts automatically in the background. Rules take effect as soon as it's ready — no `await` needed. Call `await rk.start()` only if you want to explicitly catch startup errors.
|
|
68
|
+
|
|
69
|
+
## Key syntax
|
|
70
|
+
|
|
71
|
+
Rules use a readable string syntax for keys and combos:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
"escape" // single key by name
|
|
75
|
+
"Cmd+A" // modifier + key
|
|
76
|
+
"Cmd+Shift+K" // multiple modifiers
|
|
77
|
+
"Cmd+R T" // sequence: Cmd+R then T (space-separated steps)
|
|
78
|
+
"Ctrl+J Ctrl+K" // sequence with modifiers on each step
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Modifier names are case-insensitive: `Cmd`, `Shift`, `Alt` / `Opt` / `Option`, `Ctrl` / `Control`, `Fn`.
|
|
82
|
+
|
|
83
|
+
### `Key` constants
|
|
84
|
+
|
|
85
|
+
Import `Key` for autocomplete and safe string interpolation. Values are key name strings, so they compose naturally:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { Key } from "rethocker"
|
|
89
|
+
|
|
90
|
+
Key.capsLock // "capsLock"
|
|
91
|
+
Key.escape // "escape"
|
|
92
|
+
Key.brightnessDown // "brightnessDown"
|
|
93
|
+
|
|
94
|
+
// Interpolation always produces valid key strings
|
|
95
|
+
`Cmd+${Key.v}` // "Cmd+v"
|
|
96
|
+
`${Key.brightnessDown} ${Key.brightnessUp}` // "brightnessDown brightnessUp"
|
|
97
|
+
`Cmd+${Key.r} ${Key.t}` // "Cmd+r t"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Key names are case-insensitive. Common aliases:
|
|
101
|
+
|
|
102
|
+
| Name | Aliases |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `escape` | `esc` |
|
|
105
|
+
| `return` | `enter` |
|
|
106
|
+
| `delete` | `backspace`, `back` |
|
|
107
|
+
| `forwardDelete` | `del` |
|
|
108
|
+
| `capsLock` | `caps` |
|
|
109
|
+
| `left` / `right` / `up` / `down` | `arrowLeft` etc. |
|
|
110
|
+
| `numpadEnter` | `numenter` |
|
|
111
|
+
| `numpadAdd` | `numpadplus`, `numadd` |
|
|
112
|
+
| `numpadSubtract` | `numpadminus`, `numsubtract` |
|
|
113
|
+
| `numpadDecimal` | `numpadperiod`, `numdecimal` |
|
|
114
|
+
| `numpad0`–`numpad9` | `num0`–`num9` |
|
|
115
|
+
|
|
116
|
+
### Media / system keys
|
|
117
|
+
|
|
118
|
+
Top-row physical keys (volume, brightness, media control, keyboard backlight) are fully interceptable:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
{ key: Key.volumeUp, execute: "..." }
|
|
122
|
+
{ key: Key.playPause, handler: () => {} }
|
|
123
|
+
{ key: Key.brightnessDown, remap: Key.brightnessUp }
|
|
124
|
+
{ key: `${Key.mediaNext} ${Key.mediaPrevious}`, handler: () => {} }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Available: `volumeUp`, `volumeDown`, `mute`, `brightnessUp`, `brightnessDown`, `playPause`, `mediaNext`, `mediaPrevious`, `mediaFastForward`, `mediaRewind`, `eject`, `illuminationUp`, `illuminationDown`, `illuminationToggle`.
|
|
128
|
+
|
|
129
|
+
## Rules
|
|
130
|
+
|
|
131
|
+
All rules share these common fields:
|
|
132
|
+
|
|
133
|
+
| Field | Type | Description |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `key` | `string` | Key or sequence that triggers the rule |
|
|
136
|
+
| `id` | `string?` | Stable ID for later enable/disable/remove. Auto-generated if omitted. |
|
|
137
|
+
| `app` | `string \| string[]?` | Only fire when this app is frontmost (see [App filter](#app-filter)) |
|
|
138
|
+
| `conditions` | `RuleConditions?` | Advanced condition control |
|
|
139
|
+
| `disabled` | `boolean?` | Start the rule disabled |
|
|
140
|
+
|
|
141
|
+
### Remap
|
|
142
|
+
|
|
143
|
+
Replace a key with a different key — or a whole sequence of keys:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
{ key: Key.capsLock, remap: Key.escape }
|
|
147
|
+
{ key: "NumpadEnter", remap: "Cmd+return" }
|
|
148
|
+
{ key: "Ctrl+h", remap: "left" }
|
|
149
|
+
|
|
150
|
+
// Remap to a sequence: original key is suppressed, steps are posted in order
|
|
151
|
+
{ key: "Ctrl+H E", remap: `h e l l o Shift+n1 n1 ${Key.delete}` }
|
|
152
|
+
{ key: Key.eject, remap: "Cmd+Ctrl+q" } // single target, no sequence
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Execute
|
|
156
|
+
|
|
157
|
+
Run a shell command when a key fires. The key is consumed:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
{ key: "Cmd+Shift+Space", execute: "open -a 'Alfred 5'" }
|
|
161
|
+
|
|
162
|
+
// Multiple commands run sequentially
|
|
163
|
+
{ key: Key.playPause, execute: [actions.media.playPause(), actions.app.focus("Spotify")] }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Handler
|
|
167
|
+
|
|
168
|
+
Call a TypeScript function when a key fires. The key is consumed:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
{ key: "Ctrl+J", handler: (e) => console.log("fired", e.keyCode) }
|
|
172
|
+
|
|
173
|
+
// Async handlers work too
|
|
174
|
+
{
|
|
175
|
+
key: "Ctrl+Shift+L",
|
|
176
|
+
handler: async () => {
|
|
177
|
+
await rk.execute(actions.window.halfLeft())
|
|
178
|
+
},
|
|
179
|
+
}
|
|
15
180
|
```
|
|
181
|
+
|
|
182
|
+
### Sequences
|
|
183
|
+
|
|
184
|
+
Space-separated steps in `key` make a sequence. Steps must be pressed within `sequenceTimeoutMs` (default: 5000ms):
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
{
|
|
188
|
+
key: "Cmd+R T",
|
|
189
|
+
execute: `osascript -e 'display notification "done"'`,
|
|
190
|
+
consume: true, // swallow Cmd+R and T so they don't reach the app
|
|
191
|
+
sequenceTimeoutMs: 10_000,
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`consume` is available on `execute` and `handler` rules. When true, all intermediate key events in the sequence are suppressed.
|
|
196
|
+
|
|
197
|
+
## App filter
|
|
198
|
+
|
|
199
|
+
Scope any rule to specific frontmost apps with the `app` field. Use a **bundle ID** (contains a dot) or a **display name** (prefix match, case-insensitive). Prefix with `!` to negate. Multiple values are OR-ed (negations are AND-ed).
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// Only in Figma
|
|
203
|
+
{ key: "Cmd+W", execute: "...", app: "com.figma.Desktop" }
|
|
204
|
+
|
|
205
|
+
// Only in Terminal (display name prefix match)
|
|
206
|
+
{ key: "Ctrl+L", remap: "escape", app: "Terminal" }
|
|
207
|
+
|
|
208
|
+
// In any browser
|
|
209
|
+
{ key: "Cmd+L", handler: () => {}, app: ["Safari", "Chrome", "Firefox"] }
|
|
210
|
+
|
|
211
|
+
// Everywhere except VSCode
|
|
212
|
+
{ key: "Ctrl+P", handler: () => {}, app: "!com.microsoft.VSCode" }
|
|
213
|
+
|
|
214
|
+
// Everywhere except Chrome and Safari
|
|
215
|
+
{ key: "Cmd+R T", execute: "...", app: ["!com.google.Chrome", "!com.apple.Safari"] }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Actions
|
|
219
|
+
|
|
220
|
+
Built-in helpers for common macOS tasks. Each returns a shell command string for use in `execute`, or can be run directly with `rk.execute()`.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { actions } from "rethocker"
|
|
224
|
+
|
|
225
|
+
// Window layout (targets frontmost app, or pass app name/bundle ID)
|
|
226
|
+
actions.window.halfLeft()
|
|
227
|
+
actions.window.halfRight()
|
|
228
|
+
actions.window.halfTop()
|
|
229
|
+
actions.window.halfBottom()
|
|
230
|
+
actions.window.thirdLeft()
|
|
231
|
+
actions.window.thirdCenter()
|
|
232
|
+
actions.window.thirdRight()
|
|
233
|
+
actions.window.quarterTopLeft()
|
|
234
|
+
actions.window.maximize()
|
|
235
|
+
actions.window.halfLeft("Figma") // move specific app
|
|
236
|
+
|
|
237
|
+
// App management
|
|
238
|
+
actions.app.focus("Slack") // open if not running, bring to front
|
|
239
|
+
actions.app.focus("com.tinyspeck.slackmacgap") // by bundle ID
|
|
240
|
+
actions.app.quit("Slack")
|
|
241
|
+
|
|
242
|
+
// Media
|
|
243
|
+
actions.media.playPause()
|
|
244
|
+
actions.media.next()
|
|
245
|
+
actions.media.previous()
|
|
246
|
+
actions.media.mute()
|
|
247
|
+
actions.media.setVolume(50)
|
|
248
|
+
actions.media.volumeUp(10)
|
|
249
|
+
actions.media.volumeDown(10)
|
|
250
|
+
|
|
251
|
+
// System
|
|
252
|
+
actions.system.sleep()
|
|
253
|
+
actions.system.lockScreen()
|
|
254
|
+
actions.system.missionControl()
|
|
255
|
+
actions.system.emptyTrash()
|
|
256
|
+
|
|
257
|
+
// Run a Shortcut from the macOS Shortcuts app
|
|
258
|
+
actions.shortcut("My Shortcut Name")
|
|
259
|
+
|
|
260
|
+
// Use in rules
|
|
261
|
+
{ key: "Ctrl+Left", execute: actions.window.halfLeft() }
|
|
262
|
+
{ key: "Ctrl+Shift+S", execute: actions.app.focus("Slack") }
|
|
263
|
+
{ key: "F8", execute: actions.media.playPause() }
|
|
264
|
+
|
|
265
|
+
// Multiple actions at once
|
|
266
|
+
{ key: "Ctrl+Alt+W", execute: [actions.window.halfLeft("Figma"), actions.app.focus("Slack")] }
|
|
267
|
+
|
|
268
|
+
// Run imperatively from a handler
|
|
269
|
+
{
|
|
270
|
+
key: "Ctrl+Shift+L",
|
|
271
|
+
handler: async () => {
|
|
272
|
+
await rk.execute(actions.window.halfLeft())
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Managing rules
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const rk = rethocker([
|
|
281
|
+
{ key: Key.capsLock, remap: Key.escape, id: "caps-remap" },
|
|
282
|
+
])
|
|
283
|
+
|
|
284
|
+
// Add more rules later
|
|
285
|
+
rk.add({ key: "Ctrl+J", handler: () => {} })
|
|
286
|
+
rk.add([
|
|
287
|
+
{ key: "Ctrl+K", handler: () => {} },
|
|
288
|
+
{ key: "Ctrl+L", remap: "right" },
|
|
289
|
+
])
|
|
290
|
+
|
|
291
|
+
// Enable / disable by ID
|
|
292
|
+
rk.disable("caps-remap")
|
|
293
|
+
rk.enable("caps-remap")
|
|
294
|
+
|
|
295
|
+
// Disable / enable ALL rules on this instance
|
|
296
|
+
rk.disable()
|
|
297
|
+
rk.enable()
|
|
298
|
+
|
|
299
|
+
// Remove permanently
|
|
300
|
+
rk.remove("caps-remap")
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Discover key codes
|
|
304
|
+
|
|
305
|
+
Run the included debug script to see what keys produce:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
bun node_modules/rethocker/src/scripts/debug-keys.ts
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Output shows keyCode, modifiers, and active app for every keypress — useful for finding the right key name or checking that app filters work correctly.
|
|
312
|
+
|
|
313
|
+
## Events and lifecycle
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const rk = rethocker([...])
|
|
317
|
+
|
|
318
|
+
// Daemon lifecycle
|
|
319
|
+
rk.on("ready", () => console.log("daemon ready"))
|
|
320
|
+
rk.on("exit", (code) => { console.error(`exited (${code}), restarting...`); rk.start() })
|
|
321
|
+
|
|
322
|
+
// Permissions
|
|
323
|
+
rk.on("accessibilityDenied", () => {
|
|
324
|
+
console.error("Go to System Settings → Privacy & Security → Accessibility")
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Errors
|
|
328
|
+
rk.on("error", (code, message) => console.error(`[${code}] ${message}`))
|
|
329
|
+
|
|
330
|
+
// Listen to all key events (key recorder / debugging)
|
|
331
|
+
// Stream activates automatically when subscribed, stops when unsubscribed.
|
|
332
|
+
const off = rk.on("key", (e) => {
|
|
333
|
+
console.log(e.type, e.keyCode, e.modifiers, e.app, e.appBundleID)
|
|
334
|
+
})
|
|
335
|
+
off() // unsubscribe — stream deactivates automatically
|
|
336
|
+
|
|
337
|
+
// Optionally await startup to catch errors explicitly
|
|
338
|
+
await rk.start()
|
|
339
|
+
|
|
340
|
+
// Stop the daemon
|
|
341
|
+
await rk.stop()
|
|
342
|
+
|
|
343
|
+
// Let the process exit even while the daemon is running
|
|
344
|
+
// (by default rethocker keeps the event loop alive)
|
|
345
|
+
rk.unref()
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## API reference
|
|
349
|
+
|
|
350
|
+
### `rethocker(rules?, options?)` → `RethockerHandle`
|
|
351
|
+
|
|
352
|
+
| Option | Type | Description |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| `binaryPath` | `string?` | Override the native binary path |
|
|
355
|
+
|
|
356
|
+
### `RethockerHandle`
|
|
357
|
+
|
|
358
|
+
| Method | Returns | Description |
|
|
359
|
+
|---|---|---|
|
|
360
|
+
| `add(rule \| rule[])` | `void` | Add one or more rules |
|
|
361
|
+
| `remove(id)` | `void` | Remove a rule permanently |
|
|
362
|
+
| `enable(id?)` | `void` | Enable a rule by ID, or all rules if no ID |
|
|
363
|
+
| `disable(id?)` | `void` | Disable a rule by ID, or all rules if no ID |
|
|
364
|
+
| `on(event, listener)` | `() => void` | Subscribe to an event; returns an unsubscribe function |
|
|
365
|
+
| `execute(command)` | `Promise<void>` | Run a shell command immediately (accepts `string \| string[]`) |
|
|
366
|
+
| `start()` | `Promise<void>` | Await daemon readiness (optional) |
|
|
367
|
+
| `stop()` | `Promise<void>` | Stop the daemon |
|
|
368
|
+
| `unref()` | `void` | Allow the process to exit while the daemon runs |
|
|
369
|
+
| `ready` | `boolean` | Whether the daemon is ready |
|
|
370
|
+
|
|
371
|
+
### Events
|
|
372
|
+
|
|
373
|
+
| Event | Arguments | Description |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| `"ready"` | — | Daemon ready |
|
|
376
|
+
| `"key"` | `KeyEvent` | Every key event (stream auto-activates on subscribe) |
|
|
377
|
+
| `"accessibilityDenied"` | — | Accessibility permission not granted |
|
|
378
|
+
| `"error"` | `code, message` | Native daemon error |
|
|
379
|
+
| `"exit"` | `code` | Native process exited unexpectedly |
|
|
380
|
+
|
|
381
|
+
### `KeyEvent`
|
|
382
|
+
|
|
383
|
+
| Field | Type | Description |
|
|
384
|
+
|---|---|---|
|
|
385
|
+
| `type` | `"keydown" \| "keyup" \| "flags"` | Event type |
|
|
386
|
+
| `keyCode` | `number` | macOS virtual key code |
|
|
387
|
+
| `modifiers` | `Modifier[]` | Active modifiers |
|
|
388
|
+
| `app` | `string?` | Frontmost app display name |
|
|
389
|
+
| `appBundleID` | `string?` | Frontmost app bundle ID |
|
|
390
|
+
| `suppressed` | `boolean` | Whether the key was consumed |
|
|
391
|
+
| `ruleID` | `string?` | ID of the matched rule |
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rethocker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Intercept and remap global keys on macOS — with per-app, per-device, and key-sequence support",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"module": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"os": [
|
|
10
|
+
"darwin"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
"src/",
|
|
14
|
+
"bin/rethocker-native"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build:native": "cd swift && swift build -c release && cp .build/release/rethocker-native ../bin/rethocker-native && codesign --sign - ../bin/rethocker-native",
|
|
18
|
+
"build:native:universal": "cd swift && swift build -c release --arch arm64 --arch x86_64 && lipo -create .build/arm64-apple-macosx/release/rethocker-native .build/x86_64-apple-macosx/release/rethocker-native -output ../bin/rethocker-native && codesign --sign - ../bin/rethocker-native",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "biome lint --error-on-warnings .",
|
|
21
|
+
"format": "biome format --write .",
|
|
22
|
+
"check": "biome check --error-on-warnings .",
|
|
23
|
+
"test": "bun test"
|
|
24
|
+
},
|
|
6
25
|
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.4.9",
|
|
7
27
|
"@types/bun": "latest"
|
|
8
28
|
},
|
|
9
29
|
"peerDependencies": {
|