@neuralmux/omp-superwhisper 1.0.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 ADDED
@@ -0,0 +1,205 @@
1
+ # @neuralmux/omp-superwhisper
2
+
3
+ Superwhisper voice integration extension for [Oh My Pi](https://github.com/neuralmux/oh-my-pi).
4
+
5
+ Get voice notifications when your AI coding tasks complete, and respond with your voice. Your voice response is sent back to OMP as the next prompt, creating a hands-free coding loop.
6
+
7
+ ## Requirements
8
+
9
+ - [Oh My Pi](https://github.com/neuralmux/oh-my-pi) (`@oh-my-pi/pi-coding-agent`) installed
10
+ - [Superwhisper](https://superwhisper.com) app for macOS
11
+
12
+ ## Installation
13
+
14
+ ### Via `omp plugin` (recommended)
15
+
16
+ ```bash
17
+ # Local path — install directly from a local clone
18
+ omp plugin install /workspaces/superwhisper-omp
19
+
20
+ # Local development — symlink so edits are picked up live
21
+ omp plugin link /workspaces/superwhisper-omp
22
+
23
+ # From npm (once published)
24
+ omp plugin install @neuralmux/omp-superwhisper
25
+
26
+ # From a git repository
27
+ omp plugin install github.com/neuralmux/pi-superwhisper
28
+ ```
29
+
30
+ OMP installs the package into `~/.omp/plugins/node_modules/`, discovers
31
+ `omp.extensions` from `package.json`, and wires extension modules into the
32
+ runtime. Restart OMP to activate.
33
+
34
+ ### Manual (user-level)
35
+
36
+ ```bash
37
+ mkdir -p ~/.omp/agent/extensions
38
+ cp extensions/*.ts ~/.omp/agent/extensions/
39
+ ```
40
+
41
+ ### Manual (project-level)
42
+
43
+ ```bash
44
+ mkdir -p .omp/extensions
45
+ cp extensions/*.ts .omp/extensions/
46
+ ```
47
+
48
+ ## How It Works
49
+
50
+ ```
51
+ You speak → OMP works → Extension notifies Superwhisper → You speak back → loop
52
+ ```
53
+
54
+ 1. **Task completes** → OMP fires `agent_end` with `stopReason: "stop"`
55
+ 2. **Extension extracts the response** → reads the last assistant text content
56
+ 3. **Extension notifies Superwhisper** → writes message to temp file, opens deeplink
57
+ 4. **Superwhisper shows notification** → displays summary with voice recording UI
58
+ 5. **You speak your response** → Superwhisper transcribes and writes to response file
59
+ 6. **Extension reads response** → polls the response file, sends back to OMP via `pi.sendUserMessage`
60
+ 7. **OMP continues** → processes your voice input as the next instruction
61
+
62
+ ## Events
63
+
64
+ | OMP Event | Superwhisper Status | Description |
65
+ |-------------------|---------------------|------------------------------|
66
+ | `agent_end` (stop)| `completed` | Task finished |
67
+
68
+ OMP has no built-in permission popups or elicitation system, so only end-of-turn completions are surfaced today.
69
+
70
+ ## Using from Devcontainers / Docker
71
+
72
+ When running OMP inside a devcontainer or Docker container, the extension cannot directly access the host's Superwhisper app. You need to run the **bridge daemon** on the macOS host to proxy communication.
73
+
74
+ ### 1. Start the bridge daemon on your macOS host
75
+
76
+ **Quick start (foreground):**
77
+
78
+ ```bash
79
+ bun run bin/superwhisper-bridge.ts
80
+ ```
81
+
82
+ **Install as a background service (starts on login):**
83
+
84
+ ```bash
85
+ bun run bin/install-bridge-service.ts
86
+ ```
87
+
88
+ You'll see:
89
+
90
+ ```
91
+ ✓ Wrote ~/Library/LaunchAgents/com.superwhisper.bridge.plist
92
+ ✓ Service loaded: com.superwhisper.bridge.plist
93
+ ```
94
+
95
+ The daemon is now running and will restart automatically on login.
96
+
97
+ Check it's running:
98
+
99
+ ```bash
100
+ launchctl list | grep superwhisper
101
+ ```
102
+
103
+ View logs:
104
+
105
+ ```bash
106
+ tail -f /tmp/superwhisper-bridge.log
107
+ ```
108
+
109
+ **Uninstall the service:**
110
+
111
+ ```bash
112
+ bun run bin/install-bridge-service.ts --uninstall
113
+ ```
114
+
115
+ **Enable debug logging:**
116
+
117
+ ```bash
118
+ bun run bin/install-bridge-service.ts --debug
119
+ ```
120
+
121
+ ### 2. Configure your devcontainer
122
+
123
+ In your `.devcontainer/devcontainer.json` or `docker-compose.yml`, set the environment variable inside the container:
124
+
125
+ ```json
126
+ {
127
+ "containerEnv": {
128
+ "SUPERWHISPER_BRIDGE_URL": "http://host.docker.internal:19550"
129
+ }
130
+ }
131
+ ```
132
+
133
+ Or in a `docker-compose.yml`:
134
+
135
+ ```yaml
136
+ services:
137
+ dev:
138
+ environment:
139
+ - SUPERWHISPER_BRIDGE_URL=http://host.docker.internal:19550
140
+ extra_hosts:
141
+ - "host.docker.internal:host-gateway"
142
+ ```
143
+
144
+ ### 3. That's it
145
+
146
+ When the extension detects `SUPERWHISPER_BRIDGE_URL`, it automatically switches to bridge mode. All Superwhisper interactions (inbox delivery, message/response file I/O, deeplink wakes) are proxied through the host daemon.
147
+
148
+ > **Note:** You need one bridge daemon per Mac. Multiple devcontainers can share the same daemon.
149
+
150
+ ## Controlling Superwhisper During a Session
151
+
152
+ You can ask the agent to enable or disable Superwhisper voice notifications at any time during a session. The extension exposes a `superwhisper_toggle` tool the agent will use automatically when instructed.
153
+
154
+ **Disable Superwhisper for the current session:**
155
+ > "Disable Superwhisper" / "Turn off voice notifications" / "Stop Superwhisper"
156
+
157
+ **Re-enable Superwhisper for the current session:**
158
+ > "Enable Superwhisper" / "Turn voice notifications back on" / "Re-enable Superwhisper"
159
+
160
+ The toggle is session-scoped — it only affects the current OMP session and resets when you start a new one.
161
+
162
+ ## Slash Commands
163
+
164
+ ```
165
+ /superwhisper on — enable voice notifications
166
+ /superwhisper off — disable voice notifications
167
+ /superwhisper test — send a test notification
168
+ /superwhisper status — show current state
169
+ ```
170
+
171
+ ## Environment Variables
172
+
173
+ | Variable | Default | Description |
174
+ |-----------------------------|---------|----------------------------------------------------------------------|
175
+ | `SUPERWHISPER_DEBUG` | unset | Set to `1` to write debug logs to `/tmp/superwhisper-agent/debug.log`|
176
+ | `SUPERWHISPER_SCHEME` | auto | Override deeplink scheme (`superwhisper` vs `superwhisper-debug`) |
177
+ | `SUPERWHISPER_BRIDGE_URL` | unset | Bridge daemon URL for devcontainer support (e.g. `http://host.docker.internal:19550`) |
178
+
179
+ **Bridge daemon env vars** (set on the macOS host, not in the container):
180
+
181
+ | Variable | Default | Description |
182
+ |-----------------------------|---------|----------------------------------------------------------------------|
183
+ | `SUPERWHISPER_BRIDGE_PORT` | `19550` | Port for the bridge daemon to listen on |
184
+ | `SUPERWHISPER_BRIDGE_HOST` | `127.0.0.1` | Bind address for the bridge daemon |
185
+ | `SUPERWHISPER_DEBUG` | unset | Set to `1` for verbose bridge daemon logging to stderr |
186
+
187
+ ## Project Structure
188
+
189
+ ```
190
+ extensions/
191
+ superwhisper.ts # Extension entry — OMP loads this directly
192
+ host.ts # HostOps abstraction (direct vs bridge mode)
193
+ constants.ts # Constants and shared types
194
+ inbox.ts # Inbox payload writes
195
+ message.ts # AgentMessage helpers (extract text, summary, end-turn)
196
+ poll.ts # Response file polling
197
+ bin/
198
+ superwhisper-bridge.ts # Host bridge daemon for devcontainer support
199
+ install-bridge-service.ts # Launchd service installer
200
+ com.superwhisper.bridge.plist # Launchd plist template
201
+ ```
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
+ <plist version="1.0">
5
+ <dict>
6
+ <key>Label</key>
7
+ <string>com.superwhisper.bridge</string>
8
+ <key>ProgramArguments</key>
9
+ <array>
10
+ <string>__BUN_PATH__</string>
11
+ <string>run</string>
12
+ <string>__SCRIPT_PATH__</string>
13
+ </array>
14
+ <key>RunAtLoad</key>
15
+ <true/>
16
+ <key>KeepAlive</key>
17
+ <true/>
18
+ <key>StandardOutPath</key>
19
+ <string>/tmp/superwhisper-bridge.log</string>
20
+ <key>StandardErrorPath</key>
21
+ <string>/tmp/superwhisper-bridge.err</string>
22
+ <key>EnvironmentVariables</key>
23
+ <dict>
24
+ <key>SUPERWHISPER_DEBUG</key>
25
+ <string>__DEBUG__</string>
26
+ </dict>
27
+ </dict>
28
+ </plist>
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * install-bridge-service — installs the superwhisper-bridge launchd service.
4
+ *
5
+ * Usage:
6
+ * bun run bin/install-bridge-service.ts [--uninstall] [--debug]
7
+ *
8
+ * Installs ~/Library/LaunchAgents/com.superwhisper.bridge.plist and
9
+ * loads it with launchctl so the bridge daemon starts on login and
10
+ * stays running.
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs"
14
+ import { homedir } from "node:os"
15
+ import { join } from "node:path"
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Paths
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents")
22
+ const PLIST_NAME = "com.superwhisper.bridge.plist"
23
+ const PLIST_DEST = join(LAUNCH_AGENTS_DIR, PLIST_NAME)
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Args
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const args = process.argv.slice(2)
30
+ const uninstall = args.includes("--uninstall")
31
+ const debug = args.includes("--debug")
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function findBunPath(): string {
38
+ return process.execPath
39
+ }
40
+
41
+ function findScriptPath(): string {
42
+ return join(import.meta.dir, "superwhisper-bridge.ts")
43
+ }
44
+
45
+ function loadService(): void {
46
+ try {
47
+ Bun.spawnSync(["launchctl", "load", PLIST_DEST], { stdio: ["inherit", "inherit", "inherit"] })
48
+ } catch {
49
+ // launchctl load may fail if already loaded; try bootstrap
50
+ try {
51
+ const uid = (process as any).getuid?.() ?? 501
52
+ Bun.spawnSync(["launchctl", "bootstrap", `gui/${uid}`, PLIST_DEST], {
53
+ stdio: ["inherit", "inherit", "inherit"],
54
+ })
55
+ } catch {
56
+ console.log("⚠ Could not load the service — it may already be loaded, or you may need to log out and back in.")
57
+ }
58
+ }
59
+ }
60
+
61
+ function unloadService(): void {
62
+ try {
63
+ Bun.spawnSync(["launchctl", "unload", PLIST_DEST], { stdio: ["inherit", "inherit", "inherit"] })
64
+ } catch {
65
+ try {
66
+ const uid = (process as any).getuid?.() ?? 501
67
+ Bun.spawnSync(["launchctl", "bootout", `gui/${uid}`, PLIST_DEST], {
68
+ stdio: ["inherit", "inherit", "inherit"],
69
+ })
70
+ } catch {
71
+ // fine
72
+ }
73
+ }
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Main
78
+ // ---------------------------------------------------------------------------
79
+
80
+ if (uninstall) {
81
+ console.log("Uninstalling superwhisper-bridge launchd service...")
82
+
83
+ if (existsSync(PLIST_DEST)) {
84
+ unloadService()
85
+ unlinkSync(PLIST_DEST)
86
+ console.log(`✓ Removed ${PLIST_DEST}`)
87
+ } else {
88
+ console.log("Service plist not found — nothing to uninstall.")
89
+ }
90
+
91
+ console.log("Done.")
92
+ process.exit(0)
93
+ }
94
+
95
+ // --- Install ---
96
+
97
+ const bunPath = findBunPath()
98
+ const scriptPath = findScriptPath()
99
+ const templatePath = join(import.meta.dir, "com.superwhisper.bridge.plist")
100
+
101
+ if (!existsSync(scriptPath)) {
102
+ console.error(`✗ Bridge script not found at: ${scriptPath}`)
103
+ console.error(" Make sure bin/superwhisper-bridge.ts exists in the package.")
104
+ process.exit(1)
105
+ }
106
+
107
+ if (!existsSync(templatePath)) {
108
+ console.error(`✗ Plist template not found at: ${templatePath}`)
109
+ process.exit(1)
110
+ }
111
+
112
+ // Read template and substitute
113
+ const template = readFileSync(templatePath, "utf8")
114
+ const plist = template
115
+ .replace(/__BUN_PATH__/g, bunPath)
116
+ .replace(/__SCRIPT_PATH__/g, scriptPath)
117
+ .replace(/__DEBUG__/g, debug ? "1" : "")
118
+
119
+ // Write plist
120
+ mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true })
121
+ writeFileSync(PLIST_DEST, plist)
122
+ console.log(`✓ Wrote ${PLIST_DEST}`)
123
+
124
+ // Load service
125
+ console.log("Loading service...")
126
+ loadService()
127
+ console.log(`✓ Service loaded: ${PLIST_NAME}`)
128
+
129
+ // Done
130
+ console.log()
131
+ console.log("Bridge daemon is now running and will start automatically on login.")
132
+ console.log()
133
+ console.log("Check status:")
134
+ console.log(` launchctl list | grep superwhisper`)
135
+ console.log()
136
+ console.log("View logs:")
137
+ console.log(` tail -f /tmp/superwhisper-bridge.log`)
138
+ console.log()
139
+ console.log("Uninstall:")
140
+ console.log(` bun run ${join(import.meta.dir, "install-bridge-service.ts")} --uninstall`)
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * superwhisper-bridge — HTTP daemon that runs on the macOS host.
4
+ *
5
+ * It acts as a proxy between OMP extensions running inside devcontainers (or
6
+ * any other isolated environment) and the Superwhisper macOS app. The daemon
7
+ * manages the inbox, message/response files, and deeplink wakes on the host
8
+ * filesystem so the containerized extension doesn't need host access.
9
+ *
10
+ * Usage:
11
+ * bun run bin/superwhisper-bridge.ts [--port PORT]
12
+ *
13
+ * Environment:
14
+ * SUPERWHISPER_BRIDGE_PORT – port to listen on (default: 19550)
15
+ * SUPERWHISPER_BRIDGE_HOST – bind address (default: 127.0.0.1)
16
+ * SUPERWHISPER_SCHEME – override scheme (default: auto-detect)
17
+ * SUPERWHISPER_DEBUG – enable debug logging
18
+ *
19
+ * API:
20
+ * GET /health → { running, scheme, version }
21
+ * POST /inbox body: InboxPayload (JSON) → { ok: bool }
22
+ * GET /session/:id/message → 200 text/plain | 404
23
+ * PUT /session/:id/message body: text → 204
24
+ * DELETE /session/:id/message → 204
25
+ * GET /session/:id/response?timeout=MS → 200 { kind, text? } | 408
26
+ * DELETE /session/:id/response → 204
27
+ * GET /session/:id/disabled → { disabled: bool }
28
+ * PUT /session/:id/disabled → 204 (set disabled)
29
+ * DELETE /session/:id/disabled → 204 (clear disabled)
30
+ */
31
+
32
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs"
33
+ import { $ } from "bun"
34
+
35
+ // Re-use the existing extension modules for inbox and polling logic.
36
+ import type { InboxPayload } from "../extensions/inbox"
37
+ import { deliverAgentPayload } from "../extensions/inbox"
38
+ import { waitForResponse } from "../extensions/poll"
39
+ import { MESSAGE_DIR } from "../extensions/constants"
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Config
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const PORT = parseInt(process.env.SUPERWHISPER_BRIDGE_PORT || "19550", 10)
46
+ const HOST = process.env.SUPERWHISPER_BRIDGE_HOST || "127.0.0.1"
47
+ const DEBUG = !!process.env.SUPERWHISPER_DEBUG
48
+ const VERSION = "1.0.0"
49
+
50
+ function debugLog(level: string, msg: string) {
51
+ if (!DEBUG) return
52
+ const ts = new Date().toISOString()
53
+ process.stderr.write(`[${ts}] [${level}] [superwhisper-bridge] ${msg}\n`)
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Scheme detection
58
+ // ---------------------------------------------------------------------------
59
+
60
+ let cachedScheme: string | null = process.env.SUPERWHISPER_SCHEME || null
61
+
62
+ async function detectScheme(): Promise<string> {
63
+ if (cachedScheme) return cachedScheme
64
+ try {
65
+ await $`pgrep -f DerivedData.*superwhisper.app`.quiet()
66
+ cachedScheme = "superwhisper-debug"
67
+ } catch {
68
+ cachedScheme = "superwhisper"
69
+ }
70
+ return cachedScheme
71
+ }
72
+
73
+ async function checkRunning(): Promise<boolean> {
74
+ try {
75
+ await $`pgrep -x superwhisper`.quiet()
76
+ return true
77
+ } catch {
78
+ return false
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Path helpers
84
+ // ---------------------------------------------------------------------------
85
+
86
+ function messagePath(sessionId: string) {
87
+ return `${MESSAGE_DIR}/${sessionId}-message.txt`
88
+ }
89
+ function responsePath(sessionId: string) {
90
+ return `${MESSAGE_DIR}/${sessionId}-response.txt`
91
+ }
92
+ function disabledPath(sessionId: string) {
93
+ return `${MESSAGE_DIR}/disabled-${sessionId}`
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Response helpers
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function jsonResponse(data: unknown, status = 200): Response {
101
+ return new Response(JSON.stringify(data), {
102
+ status,
103
+ headers: { "content-type": "application/json" },
104
+ })
105
+ }
106
+
107
+ function textResponse(body: string, status = 200): Response {
108
+ return new Response(body, {
109
+ status,
110
+ headers: { "content-type": "text/plain; charset=utf-8" },
111
+ })
112
+ }
113
+
114
+ function emptyResponse(status = 204): Response {
115
+ return new Response(null, { status })
116
+ }
117
+
118
+ function corsHeaders(): Record<string, string> {
119
+ return {
120
+ "access-control-allow-origin": "*",
121
+ "access-control-allow-methods": "GET,PUT,POST,DELETE,OPTIONS",
122
+ "access-control-allow-headers": "content-type",
123
+ }
124
+ }
125
+
126
+ function extractSessionId(url: URL): string | null {
127
+ // /session/<id>/(message|response|disabled)
128
+ const m = url.pathname.match(/^\/session\/([^/]+)\/(message|response|disabled)/)
129
+ return m ? decodeURIComponent(m[1]) : null
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Router
134
+ // ---------------------------------------------------------------------------
135
+
136
+ async function handleRequest(req: Request): Promise<Response> {
137
+ const url = new URL(req.url)
138
+ const method = req.method.toUpperCase()
139
+
140
+ debugLog("info", `${method} ${url.pathname}${url.search}`)
141
+
142
+ if (method === "OPTIONS") {
143
+ return new Response(null, { status: 204, headers: corsHeaders() })
144
+ }
145
+
146
+ try {
147
+ // ---- /health ----
148
+ if (url.pathname === "/health" && method === "GET") {
149
+ const scheme = await detectScheme()
150
+ const running = await checkRunning()
151
+ return jsonResponse({ running, scheme, version: VERSION })
152
+ }
153
+
154
+ // ---- /inbox ----
155
+ if (url.pathname === "/inbox" && method === "POST") {
156
+ let payload: InboxPayload
157
+ try {
158
+ payload = await req.json()
159
+ } catch {
160
+ return jsonResponse({ error: "invalid JSON" }, 400)
161
+ }
162
+ const scheme = await detectScheme()
163
+ // Override hookPid with the bridge's own PID so Superwhisper can
164
+ // validate it against running host processes. Container PIDs would
165
+ // otherwise be silently rejected.
166
+ payload.hookPid = process.pid
167
+ const ok = await deliverAgentPayload(payload, scheme)
168
+ return jsonResponse({ ok }, ok ? 200 : 500)
169
+ }
170
+
171
+ // ---- /session/:id/message ----
172
+ const sessionId = extractSessionId(url)
173
+ if (sessionId && url.pathname.endsWith("/message")) {
174
+ mkdirSync(MESSAGE_DIR, { recursive: true })
175
+
176
+ if (method === "GET") {
177
+ try {
178
+ const content = readFileSync(messagePath(sessionId), "utf8")
179
+ return textResponse(content)
180
+ } catch {
181
+ return jsonResponse({ error: "not found" }, 404)
182
+ }
183
+ }
184
+
185
+ if (method === "PUT") {
186
+ const body = await req.text()
187
+ writeFileSync(messagePath(sessionId), body)
188
+ return emptyResponse()
189
+ }
190
+
191
+ if (method === "DELETE") {
192
+ try { unlinkSync(messagePath(sessionId)) } catch {}
193
+ return emptyResponse()
194
+ }
195
+ }
196
+
197
+ // ---- /session/:id/response ----
198
+ if (sessionId && url.pathname.endsWith("/response")) {
199
+ mkdirSync(MESSAGE_DIR, { recursive: true })
200
+
201
+ if (method === "GET") {
202
+ const timeoutParam = url.searchParams.get("timeout")
203
+ const timeoutMs = timeoutParam ? parseInt(timeoutParam, 10) : 1_800_000
204
+
205
+ const rp = responsePath(sessionId)
206
+
207
+ // Long-poll: waitForResponse handles fs.watch + interval internally.
208
+ const result = await waitForResponse(rp, {
209
+ timeoutMs,
210
+ signal: undefined,
211
+ })
212
+
213
+ return jsonResponse(result)
214
+ }
215
+
216
+ if (method === "DELETE") {
217
+ try { unlinkSync(responsePath(sessionId)) } catch {}
218
+ return emptyResponse()
219
+ }
220
+ }
221
+
222
+ // ---- /session/:id/disabled ----
223
+ if (sessionId && url.pathname.endsWith("/disabled")) {
224
+ mkdirSync(MESSAGE_DIR, { recursive: true })
225
+
226
+ if (method === "GET") {
227
+ return jsonResponse({ disabled: existsSync(disabledPath(sessionId)) })
228
+ }
229
+
230
+ if (method === "PUT") {
231
+ writeFileSync(disabledPath(sessionId), "")
232
+ return emptyResponse()
233
+ }
234
+
235
+ if (method === "DELETE") {
236
+ try { unlinkSync(disabledPath(sessionId)) } catch {}
237
+ return emptyResponse()
238
+ }
239
+ }
240
+
241
+ // ---- 404 ----
242
+ return jsonResponse({ error: "not found" }, 404)
243
+ } catch (err) {
244
+ debugLog("error", `Request failed: ${err}`)
245
+ return jsonResponse({ error: "internal server error" }, 500)
246
+ }
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Start
251
+ // ---------------------------------------------------------------------------
252
+
253
+ mkdirSync(MESSAGE_DIR, { recursive: true })
254
+
255
+ const server = Bun.serve({
256
+ port: PORT,
257
+ hostname: HOST,
258
+ fetch(req) {
259
+ const res = handleRequest(req)
260
+ // Attach CORS headers to every response
261
+ return res.then((r) => {
262
+ for (const [k, v] of Object.entries(corsHeaders())) {
263
+ r.headers.set(k, v)
264
+ }
265
+ return r
266
+ })
267
+ },
268
+ })
269
+
270
+ console.log(`superwhisper-bridge v${VERSION} listening on http://${HOST}:${PORT}`)
271
+ console.log(`Health check: http://${HOST}:${PORT}/health`)
272
+ console.log(`Message dir: ${MESSAGE_DIR}`)
273
+ if (process.env.SUPERWHISPER_BRIDGE_URL) {
274
+ console.log(`⚠ SUPERWHISPER_BRIDGE_URL is set — this daemon should NOT be run with that variable.`)
275
+ }
276
+
277
+ // Graceful shutdown
278
+ process.on("SIGINT", () => {
279
+ debugLog("info", "Shutting down...")
280
+ server.stop()
281
+ process.exit(0)
282
+ })
283
+ process.on("SIGTERM", () => {
284
+ debugLog("info", "Shutting down...")
285
+ server.stop()
286
+ process.exit(0)
287
+ })
@@ -0,0 +1,4 @@
1
+ export const LOG_PREFIX = "[omp-superwhisper]"
2
+ export const MESSAGE_DIR = "/tmp/superwhisper-agent"
3
+ export const POLL_INTERVAL_MS = 1_000
4
+ export const POLL_TIMEOUT_MS = 30 * 60 * 1_000