{"_id":"@benjypng/logseq-request","_rev":"3-af1ad02f4f71c88969315d954b151add","name":"@benjypng/logseq-request","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@benjypng/logseq-request","version":"1.0.0","keywords":["logseq","logseq-plugin","cors","http","request","fetch"],"author":{"name":"benjypng"},"license":"MIT","_id":"@benjypng/logseq-request@1.0.0","maintainers":[{"name":"hkgnp","email":"ben@pngs.cc"}],"homepage":"https://github.com/benjypng/logseq-request#readme","bugs":{"url":"https://github.com/benjypng/logseq-request/issues"},"dist":{"shasum":"2649919b035eaa90d8a14089a0191794b6d5b004","tarball":"https://registry.npmjs.org/@benjypng/logseq-request/-/logseq-request-1.0.0.tgz","fileCount":7,"integrity":"sha512-wuXMKXMwaqrynazG0nuMhGyXMKNM+7N7gNNbDcaMk2EYlXi1N+rgHQNumizuJbDmZNpZk3hTItrcImrxcqevVg==","signatures":[{"sig":"MEQCIDTs/++Fk9fPFnH/Js7N5EBDSChIvOrDb8rTSzGEMKU+AiBSi74q3aW+cqozrXCXrh5xkhLQ7YFz1QrBE3sYgBH7Mw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":24723},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.mts","module":"./dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"23a5228e360f7b151ed2a2c816aea59bf0d1cc3a","release":{"branches":["main"]},"scripts":{"lint":"biome check --write .","test":"vitest run","build":"tsdown src/index.ts --format esm,cjs --dts --clean","prepare":"husky","typecheck":"tsc --noEmit"},"_npmUser":{"name":"hkgnp","email":"ben@pngs.cc"},"repository":{"url":"git+https://github.com/benjypng/logseq-request.git","type":"git"},"_npmVersion":"11.17.0","description":"CORS-free HTTP requests for Logseq plugins, via Logseq's exper_request proxy","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","tsdown":"^0.22.2","vitest":"^3.2.4","typescript":"^5.5.4","@biomejs/biome":"^2.3.11"},"_npmOperationalInternal":{"tmp":"tmp/logseq-request_1.0.0_1781259097201_0.9682815712966697","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@benjypng/logseq-request","version":"1.0.1","keywords":["logseq","logseq-plugin","cors","http","request","fetch"],"author":{"name":"benjypng"},"license":"MIT","_id":"@benjypng/logseq-request@1.0.1","maintainers":[{"name":"hkgnp","email":"ben@pngs.cc"}],"homepage":"https://github.com/benjypng/logseq-request#readme","bugs":{"url":"https://github.com/benjypng/logseq-request/issues"},"dist":{"shasum":"4c016cb67715778640bbaa370bce92aad40ccc31","tarball":"https://registry.npmjs.org/@benjypng/logseq-request/-/logseq-request-1.0.1.tgz","fileCount":7,"integrity":"sha512-OprbbPZwgLxpk5zZpw98yqDJSdJrA/3Ific4agZ9ymXsBZHFxFlgwDvMkBPVASA8NXXi9ZQoUwV5Ik70lDfY6w==","signatures":[{"sig":"MEUCIQDr6acqVC8a5KXKhSGA/GqCxsVP169eCwheto+ziWDvswIgFAPDlJApK9sDkxJXYu0YJUam0flvmwkGgVWOx/Tv4yo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjypng%2flogseq-request@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":29064},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.mts","module":"./dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"2df0d3ce6418c0da54f832afc8f10c2d3bcbf75b","release":{"branches":["main"]},"scripts":{"lint":"biome check --write .","test":"vitest run","build":"tsdown src/index.ts --format esm,cjs --dts --clean","prepare":"husky","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4057914d-e090-4f97-802e-e07e87eb3342"}},"repository":{"url":"git+https://github.com/benjypng/logseq-request.git","type":"git"},"_npmVersion":"11.17.0","description":"CORS-free HTTP requests for Logseq plugins, via Logseq's exper_request proxy","directories":{},"_nodeVersion":"24.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","tsdown":"^0.22.2","vitest":"^3.2.4","typescript":"^5.5.4","@biomejs/biome":"^2.3.11"},"_npmOperationalInternal":{"tmp":"tmp/logseq-request_1.0.1_1782610320080_0.9940265243468884","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@benjypng/logseq-request","version":"1.0.2","description":"CORS-free HTTP requests for Logseq plugins, via Logseq's exper_request proxy","author":{"name":"benjypng"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/benjypng/logseq-request.git"},"keywords":["logseq","logseq-plugin","cors","http","request","fetch"],"type":"module","main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.mts","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"publishConfig":{"access":"public"},"scripts":{"build":"tsdown src/index.ts --format esm,cjs --dts --clean","test":"vitest run","typecheck":"tsc --noEmit","lint":"biome check --write .","prepare":"husky"},"release":{"branches":["main"]},"devDependencies":{"@biomejs/biome":"^2.3.11","husky":"^9.1.7","tsdown":"^0.22.2","typescript":"^5.5.4","vitest":"^3.2.4"},"gitHead":"0793554f634451a6fb40ea960ccada034997dc9d","_id":"@benjypng/logseq-request@1.0.2","bugs":{"url":"https://github.com/benjypng/logseq-request/issues"},"homepage":"https://github.com/benjypng/logseq-request#readme","_nodeVersion":"24.18.0","_npmVersion":"11.18.0","dist":{"integrity":"sha512-6oUJ3FDYePJ/7l04Us6m+EEVF8n9zWPgqzouMhWpQ5RI8pLqXIHQNsbhTxIXC2yZo9LsJyNhcRfOKwlRpEk+Yw==","shasum":"4fc927bb22fc62548f7bedb160f6c31a19ba58a0","tarball":"https://registry.npmjs.org/@benjypng/logseq-request/-/logseq-request-1.0.2.tgz","fileCount":7,"unpackedSize":25466,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjypng%2flogseq-request@1.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIChTg9G2zyKOSSL1MhQ7658T7nH2KPjmYaU8CXnHlGdoAiBPLGEsq+Xvz8beLqXA2RQl8Tg/k2imE/9FyiYVr7fbXw=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4057914d-e090-4f97-802e-e07e87eb3342"}},"directories":{},"maintainers":[{"name":"hkgnp","email":"ben@pngs.cc"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/logseq-request_1.0.2_1783743229421_0.8210371918018613"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-12T10:11:36.916Z","modified":"2026-07-11T04:13:49.849Z","1.0.0":"2026-06-12T10:11:37.424Z","1.0.1":"2026-06-28T01:32:00.221Z","1.0.2":"2026-07-11T04:13:49.556Z"},"bugs":{"url":"https://github.com/benjypng/logseq-request/issues"},"author":{"name":"benjypng"},"license":"MIT","homepage":"https://github.com/benjypng/logseq-request#readme","keywords":["logseq","logseq-plugin","cors","http","request","fetch"],"repository":{"type":"git","url":"git+https://github.com/benjypng/logseq-request.git"},"description":"CORS-free HTTP requests for Logseq plugins, via Logseq's exper_request proxy","maintainers":[{"name":"hkgnp","email":"ben@pngs.cc"}],"readme":"# @benjypng/logseq-request\n\n![Version](https://img.shields.io/npm/v/%40benjypng%2Flogseq-request?style=flat-square&color=0969da) ![Downloads](https://img.shields.io/npm/dm/%40benjypng%2Flogseq-request?style=flat-square&color=orange) ![License](https://img.shields.io/github/license/benjypng/logseq-request?style=flat-square)\n\n> 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.\n\n---\n\n## ✨ Features\n\n- **Wretch-like chainable builder:** `proxy(url).headers(h).post(body).json<T>()` — if you've used wretch, you already know the API.\n- **Full HTTP method set:** `get`, `post`, `put`, `patch`, `delete`, plus `.json<T>()` and `.text()` response handles.\n- **Typed errors:** `HttpError` (status, statusText, body, lazy `json()`) and `ProxyUnavailableError` (unreachable endpoint / dead IPC / timeout) instead of stringly-typed failures.\n- **Opt-in timeout:** `.timeout(ms)` guards against a proxy that never answers. No default timeout — LLM calls can run for minutes.\n- **Zero dependencies:** no runtime deps, no peer deps, no `@logseq/libs` version-clash friction.\n- **Works across Logseq builds:** normalizes the differing response shapes of DB builds (2.x) and markdown builds (0.10.x).\n- **Dual ESM + CJS** with bundled type declarations.\n\n### Why not just `fetch` (or wretch/axios)?\n\nLogseq 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.\n\n### How it works\n\n`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`.\n\nThe 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.\n\n## ⚙️ Installation\n\n```bash\nnpm install @benjypng/logseq-request\n# or\nbun add @benjypng/logseq-request\n```\n\nRequires 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.\n\n## 🛠 Usage\n\n### Quick start\n\n```ts\nimport { proxy } from '@benjypng/logseq-request'\n\nconst data = await proxy('https://api.anthropic.com/v1/messages')\n  .headers({\n    'x-api-key': apiKey,\n    'anthropic-version': '2023-06-01',\n    'content-type': 'application/json',\n  })\n  .post({ model, max_tokens: 1024, messages })\n  .json<ChatResponse>()\n```\n\n### Full plugin example\n\n```ts\n// main.ts — your plugin entry\nimport '@logseq/libs'\nimport {\n  HttpError,\n  proxy,\n  ProxyUnavailableError,\n} from '@benjypng/logseq-request'\n\nconst main = async () => {\n  logseq.Editor.registerSlashCommand('Ask Claude', async () => {\n    try {\n      const data = await proxy('https://api.anthropic.com/v1/messages')\n        .headers({\n          'x-api-key': logseq.settings?.apiKey as string,\n          'anthropic-version': '2023-06-01',\n          'content-type': 'application/json',\n        })\n        .post({\n          model: 'claude-sonnet-4-6',\n          max_tokens: 1024,\n          messages: [{ role: 'user', content: 'Hello' }],\n        })\n        .json<{ content: { text: string }[] }>()\n\n      await logseq.Editor.insertAtEditingCursor(data.content[0].text)\n    } catch (e) {\n      if (e instanceof HttpError) {\n        // Real HTTP failure (4xx/5xx) — status and body available on DB builds\n        logseq.UI.showMsg(`API error ${e.status}: ${e.body}`, 'error')\n      } else if (e instanceof ProxyUnavailableError) {\n        // Endpoint unreachable / IPC dead — message starts \"Failed to fetch:\"\n        logseq.UI.showMsg('Could not reach the API. Network up?', 'error')\n      }\n    }\n  })\n}\n\nlogseq.ready(main).catch(console.error)\n```\n\n### API\n\nAll methods:\n\n```ts\nproxy(url).get().json<T>()\nproxy(url).headers(h).post(body).json<T>()\nproxy(url).headers(h).put(body).text()\nproxy(url).headers(h).patch(body).json<T>()\nproxy(url).headers(h).delete().json<T>()\n```\n\n- `.headers()` merges across calls (later wins). Builders are immutable — each call returns a new one, so you can branch safely:\n\n  ```ts\n  const api = proxy('https://api.example.com/v1').headers(authHeaders)\n  await api.get().json<Status>() // base unchanged\n  await api.headers({ 'x-extra': '1' }).post(body).json<Result>()\n  ```\n\n- Nothing is sent until you call `.json<T>()` or `.text()` — a method call returns a lazy response handle.\n- `.json<T>()` parses the response body as JSON; `.text()` returns the raw body string.\n- `.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.\n\n### Error handling\n\n```ts\nimport { HttpError, ProxyUnavailableError } from '@benjypng/logseq-request'\n\ntry {\n  await proxy(url).post(body).json()\n} catch (e) {\n  if (e instanceof HttpError) {\n    // HTTP-level failure (DB builds only — see Limitations)\n    console.error(e.status, e.statusText, e.body, e.json())\n  } else if (e instanceof ProxyUnavailableError) {\n    // Unreachable endpoint, dead IPC, or timeout.\n    // e.message always starts with \"Failed to fetch:\".\n  }\n}\n```\n\n- **`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.\n- **`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.\n\n### Low-level escape hatch\n\n```ts\nimport { proxyRequest } from '@benjypng/logseq-request'\n\nconst data = await proxyRequest<MyResponse>({\n  url,\n  method: 'POST',\n  headers,\n  body,\n  timeoutMs: 30_000,\n})\n```\n\n`proxyRequestRaw()` goes one level lower still: it returns the normalized response body without JSON parsing (a string in the typical case).\n\n## 🚧 Limitations\n\n- **No streaming.** `exper_request` buffers the full response; SSE/streaming APIs are not supported. Request non-streaming variants from providers.\n- **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.\n- **No abort/cancellation.** The underlying IPC offers none; `.timeout(ms)` abandons the wait but cannot cancel the in-flight request.\n\n## 🧑‍💻 Local development\n\nTo consume the package from another project without going through npm:\n\n```bash\n# in this repo\nbun install && bun run build\n\n# in your plugin\nbun add file:../logseq-request\n# or: `bun link` here, then `bun link @benjypng/logseq-request` in the plugin\n```\n\nScripts: `bun run test` (vitest), `bun run typecheck`, `bun run build` (tsdown → ESM + CJS + d.ts), `bun run lint` (Biome).\n\nReleases are automated: pushes to `main` run semantic-release, which versions from conventional commits and publishes to npm via trusted publishing (OIDC).\n\n## ☕️ Support\n\nIf this package saves you from a CORS rabbit hole, please consider supporting the development.\n\n<div align=\"center\">\n  <a href=\"https://github.com/sponsors/benjypng\"><img src=\"https://img.shields.io/badge/Sponsor-GitHub-ea4aaa?style=for-the-badge&logo=github\" alt=\"Sponsor on Github\" /></a>&nbsp;<a href=\"https://buymeacoffee.com/hkgnp.dev\"><img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\" /></a>\n</div>\n\n## 🤝 Contributing\n\nIssues 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.\n","readmeFilename":"README.md"}