| 130 | * etc.). For anything not exposed as a method, use `command(cmd, args)`. |
| 131 | */ |
| 132 | export class BrowseClient { |
| 133 | readonly port: number; |
| 134 | readonly token: string; |
| 135 | readonly tabId?: number; |
| 136 | readonly timeoutMs: number; |
| 137 | |
| 138 | constructor(opts: BrowseClientOptions = {}) { |
| 139 | const auth = resolveBrowseAuth(opts); |
| 140 | this.port = auth.port; |
| 141 | this.token = auth.token; |
| 142 | this.tabId = opts.tabId ?? parseIntegerEnvValue(process.env.BROWSE_TAB); |
| 143 | this.timeoutMs = opts.timeoutMs ?? 30_000; |
| 144 | } |
| 145 | |
| 146 | // ─── Low-level dispatch ───────────────────────────────────────── |
| 147 | |
| 148 | /** Send an arbitrary command; returns raw response text. Throws on non-2xx. */ |
| 149 | async command(cmd: string, args: string[] = []): Promise<string> { |
| 150 | const body = JSON.stringify({ |
| 151 | command: cmd, |
| 152 | args, |
| 153 | ...(this.tabId !== undefined && !isNaN(this.tabId) ? { tabId: this.tabId } : {}), |
| 154 | }); |
| 155 | |
| 156 | let resp: Response; |
| 157 | try { |
| 158 | resp = await fetch(`http://127.0.0.1:${this.port}/command`, { |
| 159 | method: 'POST', |
| 160 | headers: { |
| 161 | 'Content-Type': 'application/json', |
| 162 | 'Authorization': `Bearer ${this.token}`, |
| 163 | }, |
| 164 | body, |
| 165 | signal: AbortSignal.timeout(this.timeoutMs), |
| 166 | }); |
| 167 | } catch (err: any) { |
| 168 | if (err.name === 'TimeoutError' || err.name === 'AbortError') { |
| 169 | throw new BrowseClientError(`browse-client: command "${cmd}" timed out after ${this.timeoutMs}ms`); |
| 170 | } |
| 171 | if (err.code === 'ECONNREFUSED') { |
| 172 | throw new BrowseClientError(`browse-client: daemon not running on port ${this.port}`); |
| 173 | } |
| 174 | throw new BrowseClientError(`browse-client: ${err.message ?? err}`); |
| 175 | } |
| 176 | |
| 177 | const text = await resp.text(); |
| 178 | if (!resp.ok) { |
| 179 | let message = `browse-client: command "${cmd}" failed with status ${resp.status}`; |
| 180 | try { |
| 181 | const parsed = JSON.parse(text); |
| 182 | if (parsed.error) message += `: ${parsed.error}`; |
| 183 | } catch { |
| 184 | if (text) message += `: ${text.slice(0, 200)}`; |
| 185 | } |
| 186 | throw new BrowseClientError(message, resp.status, text); |
| 187 | } |
| 188 | return text; |
| 189 | } |
nothing calls this directly
no outgoing calls
no test coverage detected