npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@benjypng/logseq-request

v1.0.2

Published

CORS-free HTTP requests for Logseq plugins, via Logseq's exper_request proxy

Readme

@benjypng/logseq-request

Version Downloads License

CORS-free HTTP requests for Logseq plugins. A tiny, typed, wretch-like builder over Logseq's exper_request proxy — because plain fetch no longer works from a plugin.


✨ Features

  • Wretch-like chainable builder: proxy(url).headers(h).post(body).json<T>() — if you've used wretch, you already know the API.
  • Full HTTP method set: get, post, put, patch, delete, plus .json<T>() and .text() response handles.
  • Typed errors: HttpError (status, statusText, body, lazy json()) and ProxyUnavailableError (unreachable endpoint / dead IPC / timeout) instead of stringly-typed failures.
  • Opt-in timeout: .timeout(ms) guards against a proxy that never answers. No default timeout — LLM calls can run for minutes.
  • Zero dependencies: no runtime deps, no peer deps, no @logseq/libs version-clash friction.
  • Works across Logseq builds: normalizes the differing response shapes of DB builds (2.x) and markdown builds (0.10.x).
  • Dual ESM + CJS with bundled type declarations.

Why not just fetch (or wretch/axios)?

Logseq plugins run at the lsp://logseq.com origin. Since the Electron update that ships a stricter CORS policy, direct fetch (and any fetch-based library like wretch or axios) to external APIs — api.anthropic.com, generativelanguage.googleapis.com, even localhost Ollama — is blocked at the preflight stage. Logseq's experimental request API (exper_request) proxies the call through the main process, which is not subject to CORS. This package wraps that proxy in a small, typed API.

How it works

exper_request is invoked via Logseq's internal _execCallableAPIAsync('exper_request', pluginId, options), which returns a request id; the response arrives on a task_callback_<id> event. Responses are normalized across Logseq builds: DB (2.x) honours includeResponse and returns { status, ok, body }; markdown (0.10.x) returns the bare body; null/undefined (unreachable endpoint) becomes ProxyUnavailableError.

The package deliberately has no dependency on @logseq/libs: there is no instance to import from it (the logseq global is created as a side effect of the consumer's own import), and the IPC members used here are undocumented internals that its public types don't cover. Minimal types for exactly the members used are defined in-package instead.

⚙️ Installation

npm install @benjypng/logseq-request
# or
bun add @benjypng/logseq-request

Requires running inside a Logseq plugin: it uses the logseq global that @logseq/libs sets up in your plugin's entry, so it works anywhere after logseq.ready() with no extra setup.

🛠 Usage

Quick start

import { proxy } from '@benjypng/logseq-request'

const data = await proxy('https://api.anthropic.com/v1/messages')
  .headers({
    'x-api-key': apiKey,
    'anthropic-version': '2023-06-01',
    'content-type': 'application/json',
  })
  .post({ model, max_tokens: 1024, messages })
  .json<ChatResponse>()

Full plugin example

// main.ts — your plugin entry
import '@logseq/libs'
import {
  HttpError,
  proxy,
  ProxyUnavailableError,
} from '@benjypng/logseq-request'

const main = async () => {
  logseq.Editor.registerSlashCommand('Ask Claude', async () => {
    try {
      const data = await proxy('https://api.anthropic.com/v1/messages')
        .headers({
          'x-api-key': logseq.settings?.apiKey as string,
          'anthropic-version': '2023-06-01',
          'content-type': 'application/json',
        })
        .post({
          model: 'claude-sonnet-4-6',
          max_tokens: 1024,
          messages: [{ role: 'user', content: 'Hello' }],
        })
        .json<{ content: { text: string }[] }>()

      await logseq.Editor.insertAtEditingCursor(data.content[0].text)
    } catch (e) {
      if (e instanceof HttpError) {
        // Real HTTP failure (4xx/5xx) — status and body available on DB builds
        logseq.UI.showMsg(`API error ${e.status}: ${e.body}`, 'error')
      } else if (e instanceof ProxyUnavailableError) {
        // Endpoint unreachable / IPC dead — message starts "Failed to fetch:"
        logseq.UI.showMsg('Could not reach the API. Network up?', 'error')
      }
    }
  })
}

logseq.ready(main).catch(console.error)

API

All methods:

proxy(url).get().json<T>()
proxy(url).headers(h).post(body).json<T>()
proxy(url).headers(h).put(body).text()
proxy(url).headers(h).patch(body).json<T>()
proxy(url).headers(h).delete().json<T>()
  • .headers() merges across calls (later wins). Builders are immutable — each call returns a new one, so you can branch safely:

    const api = proxy('https://api.example.com/v1').headers(authHeaders)
    await api.get().json<Status>() // base unchanged
    await api.headers({ 'x-extra': '1' }).post(body).json<Result>()
  • Nothing is sent until you call .json<T>() or .text() — a method call returns a lazy response handle.

  • .json<T>() parses the response body as JSON; .text() returns the raw body string.

  • .timeout(ms) (opt-in, no default) fails the request if the proxy never responds — useful against dead IPC. LLM calls can run for minutes, so no timeout is applied unless you ask for one.

Error handling

import { HttpError, ProxyUnavailableError } from '@benjypng/logseq-request'

try {
  await proxy(url).post(body).json()
} catch (e) {
  if (e instanceof HttpError) {
    // HTTP-level failure (DB builds only — see Limitations)
    console.error(e.status, e.statusText, e.body, e.json())
  } else if (e instanceof ProxyUnavailableError) {
    // Unreachable endpoint, dead IPC, or timeout.
    // e.message always starts with "Failed to fetch:".
  }
}
  • HttpError — thrown when the response reports ok: false. Carries status: number, statusText?: string, body: string, and a lazy json<T>() that returns undefined if the body isn't valid JSON.
  • ProxyUnavailableError — thrown when the proxy yields no response (unreachable endpoint, dead IPC) or the opt-in timeout elapses. Its message always starts with Failed to fetch: so existing connection-error handling that pattern-matches on that prefix keeps working.

Low-level escape hatch

import { proxyRequest } from '@benjypng/logseq-request'

const data = await proxyRequest<MyResponse>({
  url,
  method: 'POST',
  headers,
  body,
  timeoutMs: 30_000,
})

proxyRequestRaw() goes one level lower still: it returns the normalized response body without JSON parsing (a string in the typical case).

🚧 Limitations

  • No streaming. exper_request buffers the full response; SSE/streaming APIs are not supported. Request non-streaming variants from providers.
  • HTTP errors are invisible on markdown builds (0.10.x). Those builds ignore includeResponse and return the bare body, so there is no status code to inspect; HttpError is only thrown on DB builds (2.x). Error bodies on markdown builds surface as JSON parse results instead.
  • No abort/cancellation. The underlying IPC offers none; .timeout(ms) abandons the wait but cannot cancel the in-flight request.

🧑‍💻 Local development

To consume the package from another project without going through npm:

# in this repo
bun install && bun run build

# in your plugin
bun add file:../logseq-request
# or: `bun link` here, then `bun link @benjypng/logseq-request` in the plugin

Scripts: bun run test (vitest), bun run typecheck, bun run build (tsdown → ESM + CJS + d.ts), bun run lint (Biome).

Releases are automated: pushes to main run semantic-release, which versions from conventional commits and publishes to npm via trusted publishing (OIDC).

☕️ Support

If this package saves you from a CORS rabbit hole, please consider supporting the development.

🤝 Contributing

Issues are welcome. If you find a bug, please open an issue. Pull requests are not accepted at the moment as I am not able to commit to reviewing them in a timely fashion.