/** HTTP fetch + HTML extract helpers for agent `web_fetch` tool (preamble for /bin/agent). */
/**
* @param {unknown} e
* @returns {string}
*/
function bareWebFmtErr(e) {
if (e === undefined)
return 'promise_rejected_with_undefined (no rejection reason)'
if (e === null) return 'promise_rejected_with_null'
if (typeof e === 'string') return e
if (typeof e !== 'object') return String(e)
const o = /** @type {Record} */ (e)
const msg = o.message
if (typeof msg === 'string' && msg.trim())
return bareWebFmtErrAugment(o, msg.trim())
if (typeof msg === 'number' || typeof msg === 'boolean')
return bareWebFmtErrAugment(o, String(msg))
const nm = o.name
const code = o.code
const errno = o.errno
/** @type {string[]} */
const bits = []
if (typeof nm === 'string' && nm.trim()) bits.push(nm)
if (code !== undefined && code !== null && String(code) !== '')
bits.push('code=' + String(code))
if (errno !== undefined && errno !== null && String(errno) !== '')
bits.push('errno=' + String(errno))
const cause = o.cause
if (cause !== undefined && cause !== null && cause !== e) {
const cs = bareWebFmtErr(cause)
if (cs && cs !== 'unknown_error')
bits.push('cause=(' + cs.slice(0, 280) + ')')
}
const errs = o.errors
if (Array.isArray(errs) && errs.length) {
errs.slice(0, 5).forEach((sub, i) => {
bits.push('agg' + i + '=' + bareWebFmtErr(sub).slice(0, 120))
})
}
if (bits.length) return bits.join(' ')
try {
const j = JSON.stringify(o)
if (j && j !== '{}' && j !== '[]') return j.slice(0, 400)
} catch {
/* ignore */
}
try {
if (
typeof /** @type {{ toString?: () => string }} */ (o).toString ===
'function'
) {
const t = /** @type {{ toString: () => string }} */ (o).toString()
if (t && t !== '[object Object]') return t.slice(0, 400)
}
} catch {
/* ignore */
}
return 'unknown_error'
}
/**
* Append errno/syscall from Node-ish errors when message alone is vague.
* @param {Record} o
* @param {string} base
*/
function bareWebFmtErrAugment(o, base) {
const syscall = o.syscall
const code = o.code
const errno = o.errno
/** @type {string[]} */
const tail = []
if (typeof syscall === 'string' && syscall.trim())
tail.push('syscall=' + syscall)
if (code !== undefined && code !== null && String(code) !== '')
tail.push(String(code))
if (errno !== undefined && errno !== null && String(errno) !== '')
tail.push('errno=' + String(errno))
const cause = o.cause
if (cause !== undefined && cause !== null) {
const cs = bareWebFmtErr(cause)
if (cs && cs !== 'unknown_error')
tail.push('cause=(' + cs.slice(0, 240) + ')')
}
return tail.length ? base + ' [' + tail.join(', ') + ']' : base
}
/**
* Tool args often omit numeric fields; `Number(undefined)` is NaN and `NaN ?? d` is still NaN.
* @param {unknown} n
* @param {number} def
*/
function bareWebFiniteOr(n, def) {
const x = Number(n)
return Number.isFinite(x) ? x : def
}
/**
* @param {Record} ctx
* @returns {typeof fetch | null}
*/
function bareWebResolveFetch(ctx) {
if (typeof ctx.httpFetch === 'function')
return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx))
const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null
let f =
bare && typeof bare.fetch === 'function'
? bare.fetch
: bare &&
bare.default &&
typeof bare.default === 'object' &&
typeof bare.default.fetch === 'function'
? bare.default.fetch
: null
if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare))
if (typeof globalThis.fetch === 'function')
return globalThis.fetch.bind(globalThis)
return null
}
/**
* bundled bare-fetch rejects the fetch promise with `signal.reason` on abort.
* `controller.abort()` with no argument sets `reason === undefined`, so callers
* see `promise_rejected_with_undefined`. Always pass an explicit reason.
* @param {number} timeoutMs
*/
function bareWebTimeoutAbortReason(timeoutMs) {
const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)'
try {
if (typeof DOMException === 'function')
return new DOMException(msg, 'TimeoutError')
} catch {
/* ignore */
}
const e = new Error(msg)
e.name = 'TimeoutError'
return e
}
/**
* @param {AbortSignal} sig
*/
function bareWebSignalAbortReason(sig) {
try {
const r = /** @type {{ reason?: unknown }} */ (sig).reason
if (r !== undefined && r !== null) return r
} catch {
/* ignore */
}
const e = new Error('web_fetch aborted (signal)')
e.name = 'AbortError'
return e
}
/**
* @param {AbortSignal | null | undefined} a
* @param {AbortSignal | null | undefined} b
*/
function bareWebUnionAbort(a, b) {
if (!a) return b || undefined
if (!b) return a
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
const c = new AbortController()
/**
* @param {AbortSignal} sig
*/
const forward = (sig) => {
try {
c.abort(bareWebSignalAbortReason(sig))
} catch {
/* ignore — second source may fire after controller already aborted */
}
}
try {
const as = /** @type {AbortSignal} */ (a)
const bs = /** @type {AbortSignal} */ (b)
if (as.aborted) forward(as)
else as.addEventListener('abort', () => forward(as), { once: true })
if (bs.aborted) forward(bs)
else bs.addEventListener('abort', () => forward(bs), { once: true })
} catch {
/* ignore */
}
return c.signal
}
/**
* @param {Uint8Array[]} parts
*/
function bareWebConcatUint8(parts) {
let n = 0
for (const p of parts) n += p.length
const out = new Uint8Array(n)
let o = 0
for (const p of parts) {
out.set(p, o)
o += p.length
}
return out
}
/**
* @param {Response} res
* @param {number} maxBytes
* @param {AbortSignal | undefined} signal
*/
async function bareWebReadBodyLimited(res, maxBytes, signal) {
if (!res.body || typeof res.body.getReader !== 'function') {
try {
const ab = await res.arrayBuffer()
const u8 = new Uint8Array(ab)
return {
bytes: u8.byteLength > maxBytes ? u8.slice(0, maxBytes) : u8,
truncated: u8.byteLength > maxBytes
}
} catch {
return { bytes: new Uint8Array(0), truncated: false }
}
}
const reader = res.body.getReader()
/** @type {Uint8Array[]} */
const chunks = []
let total = 0
try {
for (;;) {
if (signal && signal.aborted) {
try {
await reader.cancel()
} catch {
/* ignore */
}
break
}
const { done, value } = await reader.read()
if (done) break
if (!value || !value.length) continue
total += value.length
if (total > maxBytes) {
const prev = total - value.length
const take = Math.max(0, maxBytes - prev)
if (take > 0) chunks.push(value.subarray(0, take))
try {
await reader.cancel()
} catch {
/* ignore */
}
return { bytes: bareWebConcatUint8(chunks), truncated: true }
}
chunks.push(value)
}
} finally {
try {
reader.releaseLock()
} catch {
/* ignore */
}
}
return { bytes: bareWebConcatUint8(chunks), truncated: false }
}
/**
* @param {string | null | undefined} ct
*/
function bareWebCharsetFromContentType(ct) {
const m = /charset\s*=\s*["']?([^"';\s]+)/i.exec(String(ct || ''))
return (m ? m[1] : 'utf-8').trim().toLowerCase()
}
/**
* @param {Uint8Array} bytes
* @param {string} label
*/
function bareWebDecodeBytes(bytes, label) {
try {
const dec = new TextDecoder(label || 'utf-8', {
fatal: false,
ignoreBOM: true
})
return dec.decode(bytes)
} catch {
return new TextDecoder('utf-8', { fatal: false }).decode(bytes)
}
}
/**
* @param {string} s
*/
function bareWebDecodeHtmlEntities(s) {
let t = String(s || '')
t = t.replace(/ /gi, ' ')
t = t.replace(/"/gi, '"')
t = t.replace(/'/g, "'")
t = t.replace(/'/gi, "'")
t = t.replace(/&/gi, '&')
t = t.replace(/</gi, '<')
t = t.replace(/>/gi, '>')
t = t.replace(/([0-9a-f]+);/gi, (_, h) => {
const c = parseInt(h, 16)
return Number.isFinite(c) ? String.fromCodePoint(c) : _
})
t = t.replace(/(\d+);/g, (_, d) => {
const c = parseInt(d, 10)
return Number.isFinite(c) ? String.fromCodePoint(c) : _
})
return t
}
/**
* @param {string} html
*/
function bareWebExtractHtmlText(html) {
let s = String(html || '')
s = s.replace(/