Add Discordjs
CI / Build & Test (push) Failing after 52s

This commit is contained in:
2026-09-04 22:10:35 -04:00
parent 998cb3521f
commit 693814f46b
85 changed files with 5945 additions and 28 deletions
+1
View File
@@ -60,6 +60,7 @@ flowchart LR
- **Net** — `BridgeSwarm.net.fetch` to public http(s) only - **Net** — `BridgeSwarm.net.fetch` to public http(s) only
- **QVAC** — on-device LLM (`BridgeSwarm.qvac.*`), off until Settings → Enable QVAC. Multimodal **image input**; live desktop stills via `pushDesktopFrame`. No image/video generation. - **QVAC** — on-device LLM (`BridgeSwarm.qvac.*`), off until Settings → Enable QVAC. Multimodal **image input**; live desktop stills via `pushDesktopFrame`. No image/video generation.
- **Agent** — grok-class coding loop (`BridgeSwarm.agent.*`) on the same engine - **Agent** — grok-class coding loop (`BridgeSwarm.agent.*`) on the same engine
- **Discord** — official discord.js 14 (`BridgeSwarm.DiscordJS`), off until Settings → Enable Discord
See [docs/CAPABILITIES.md](docs/CAPABILITIES.md). See [docs/CAPABILITIES.md](docs/CAPABILITIES.md).
+2
View File
@@ -33,6 +33,8 @@ These are the main APIs your page uses. For host request types and event payload
- **`BridgeSwarm.qvac.*`** — Local QVAC inference (enable in Settings). `chat({ history, tools }, { execute })` runs page tools. Multimodal models accept images; HTTPS pages feed a live desktop ring with `pushDesktopFrame`. See [QVAC.md](QVAC.md). - **`BridgeSwarm.qvac.*`** — Local QVAC inference (enable in Settings). `chat({ history, tools }, { execute })` runs page tools. Multimodal models accept images; HTTPS pages feed a live desktop ring with `pushDesktopFrame`. See [QVAC.md](QVAC.md).
- **`BridgeSwarm.agent.*`** — Grok-class coding sessions (requires QVAC enabled in Settings). `session.addTool({ name, description, parameters, execute })` registers page tools. See [AGENT.md](AGENT.md). - **`BridgeSwarm.agent.*`** — Grok-class coding sessions (requires QVAC enabled in Settings). `session.addTool({ name, description, parameters, execute })` registers page tools. See [AGENT.md](AGENT.md).
- **`BridgeSwarm.DiscordJS`** — Official discord.js 14 (`Client`, intents, events, builders, `REST`). Real Client on the Bare host. Enable in Settings. See [DISCORD.md](DISCORD.md).
- **`BridgeSwarm.media.*`** — Media pack: batch (`info`, `imageTransform`, `extractFrame`, `transcode`, …) and live encode (`encodeStart`, `encodePushFrame`, `encodeStop`, `liveSession`, `attachLiveReceiver`). See [CAPABILITIES.md](CAPABILITIES.md). - **`BridgeSwarm.media.*`** — Media pack: batch (`info`, `imageTransform`, `extractFrame`, `transcode`, …) and live encode (`encodeStart`, `encodePushFrame`, `encodeStop`, `liveSession`, `attachLiveReceiver`). See [CAPABILITIES.md](CAPABILITIES.md).
- **`BridgeSwarm.fs.*`** — Allowlisted files under storage `files/` (`write`, `read`, `list`, `mkdir`, `stat`, `exists`, `unlink`, `rename`). - **`BridgeSwarm.fs.*`** — Allowlisted files under storage `files/` (`write`, `read`, `list`, `mkdir`, `stat`, `exists`, `unlink`, `rename`).
+5 -5
View File
@@ -19,7 +19,7 @@ flowchart TB
SW["Hyperswarm"] SW["Hyperswarm"]
DATA["Corestore · Hypercore · Hyperbee<br/>Hyperdrive · Autobase · Hyperdb"] DATA["Corestore · Hypercore · Hyperbee<br/>Hyperdrive · Autobase · Hyperdb"]
MUX["Protomux · HRPC"] MUX["Protomux · HRPC"]
CAP["capabilities/<br/>media · fs · sqlite · net · qvac · agent"] CAP["capabilities/<br/>media · fs · sqlite · net · qvac · agent · discord"]
EX["examples-server.js<br/>127.0.0.1:4173"] EX["examples-server.js<br/>127.0.0.1:4173"]
end end
@@ -40,15 +40,15 @@ flowchart TB
### Extension ### Extension
- **Background service worker** ([extension/background.js](../extension/background.js)): Maintains the native messaging port (`chrome.runtime.connectNative('com.bridgeswarm')`). Tracks pending requests by id; routes swarm events to the tabs that own each `swarmId`; broadcasts capability events (`cap-chunk` / `cap-end` / `cap-error`) to subscribed tabs. Reconnects with backoff on disconnect. Reads **`bridgeSwarmSettings`** from local storage (autosaved from Options/Dashboard). When “Show notification when host disconnects” is enabled, shows a browser notification on `port.onDisconnect`. When **Examples server** is enabled, sends `examplesServer.start` / `stop` to the host. When **Enable QVAC** is on, sends `qvac.setEnabled`; origin-allowlisted pack commands are gated, and QVAC/agent inference is refused until that toggle is on. - **Background service worker** ([extension/background.js](../extension/background.js)): Maintains the native messaging port (`chrome.runtime.connectNative('com.bridgeswarm')`). Tracks pending requests by id; routes swarm events to the tabs that own each `swarmId`; broadcasts capability events (`cap-chunk` / `cap-end` / `cap-error`) to subscribed tabs. Reconnects with backoff on disconnect. Reads **`bridgeSwarmSettings`** from local storage (autosaved from Options/Dashboard). When “Show notification when host disconnects” is enabled, shows a browser notification on `port.onDisconnect`. When **Examples server** is enabled, sends `examplesServer.start` / `stop` to the host. When **Enable QVAC** is on, sends `qvac.setEnabled`; when **Enable Discord** is on, sends `discord.setEnabled`; origin-allowlisted pack commands are gated, and QVAC/agent inference and Discord construct/login are refused until those toggles are on.
- **Content script** ([extension/content.js](../extension/content.js)): Reads settings; if “Do not inject on file:// URLs” is on and the page is `file://`, skips injection. Otherwise injects defaults then [api.js](../extension/api.js), [framed-stream.js](../extension/framed-stream.js), and [protomux-bundle.js](../extension/protomux-bundle.js). Bridges page ↔ background via `window.postMessage` / `chrome.runtime.sendMessage` (the page cannot use `chrome.runtime` directly). - **Content script** ([extension/content.js](../extension/content.js)): Reads settings; if “Do not inject on file:// URLs” is on and the page is `file://`, skips injection. Otherwise injects defaults then [api.js](../extension/api.js), [discordjs-builders.js](../extension/discordjs-builders.js), [discordjs.js](../extension/discordjs.js), [framed-stream.js](../extension/framed-stream.js), and [protomux-bundle.js](../extension/protomux-bundle.js). Bridges page ↔ background via `window.postMessage` / `chrome.runtime.sendMessage` (the page cannot use `chrome.runtime` directly).
- **Control Center** ([extension/dashboard.html](../extension/dashboard.html)): Premium panel (Overview · Swarms · Connections · Activity · Settings). Live state over a `bridgeswarm-dashboard` Port; advanced log viewer; host health from `host.snapshot`. Changes **save on change**. - **Control Center** ([extension/dashboard.html](../extension/dashboard.html)): Premium panel (Overview · Swarms · Connections · Activity · Settings). Live state over a `bridgeswarm-dashboard` Port; advanced log viewer; host health from `host.snapshot`. Changes **save on change**.
- **Options** ([extension/options.html](../extension/options.html)): Opens Control Center Settings (`open_in_tab`). - **Options** ([extension/options.html](../extension/options.html)): Opens Control Center Settings (`open_in_tab`).
- **Injected scripts** (page context): [defaults.js](../extension/defaults.js) is injected **into the page world** (content-script `window` is isolated) with Settings as `data-defaults`. Then [api.js](../extension/api.js) exposes `window.BridgeSwarm`, `BridgeSwarm.request`, `BridgeSwarm.capabilities` / `media` / `fs` / `sqlite` / `net` / `qvac` / `agent`. Merges constructor options with `__BRIDGESWARM_DEFAULTS__`. Debug `console.log` in api.js only runs when Settings → Debug is on. - **Injected scripts** (page context): [defaults.js](../extension/defaults.js) is injected **into the page world** (content-script `window` is isolated) with Settings as `data-defaults`. Then [api.js](../extension/api.js) exposes `window.BridgeSwarm`, `BridgeSwarm.request`, `BridgeSwarm.capabilities` / `media` / `fs` / `sqlite` / `net` / `qvac` / `agent`. [discordjs.js](../extension/discordjs.js) attaches `BridgeSwarm.DiscordJS`. Merges constructor options with `__BRIDGESWARM_DEFAULTS__`. Debug `console.log` in api.js only runs when Settings → Debug is on.
### Native host ### Native host
- **Entry** ([native-host/index.mjs](../native-host/index.mjs)): Loads bare-process, registers default capability packs (media, fs, sqlite, net, qvac, agent), starts [messenger.js](../native-host/messenger.js) + [host.js](../native-host/host.js). QVAC inference stays idle until Settings → Enable QVAC. - **Entry** ([native-host/index.mjs](../native-host/index.mjs)): Loads bare-process, registers default capability packs (media, fs, sqlite, net, qvac, agent, discord), starts [messenger.js](../native-host/messenger.js) + [host.js](../native-host/host.js). QVAC inference stays idle until Settings → Enable QVAC. Discord.js stays idle until Settings → Enable Discord.
- **Messenger** ([native-host/messenger.js](../native-host/messenger.js)): Chrome/Firefox native messaging: 4-byte little-endian length + UTF-8 JSON. Max message size from host → browser is **1 MB**. - **Messenger** ([native-host/messenger.js](../native-host/messenger.js)): Chrome/Firefox native messaging: 4-byte little-endian length + UTF-8 JSON. Max message size from host → browser is **1 MB**.
- **Host** ([native-host/host.js](../native-host/host.js)): Swarm lifecycle, peer firewall/ban, auto-replicate, connection attachment (Protomux/HRPC), Hyper* data API (including list/peek/has/update/entry), capability dispatch, examples-server control (`examplesServer.start` / `stop` / `status`), and in-process QVAC (`@qvac/inference`) plus the agent loop. - **Host** ([native-host/host.js](../native-host/host.js)): Swarm lifecycle, peer firewall/ban, auto-replicate, connection attachment (Protomux/HRPC), Hyper* data API (including list/peek/has/update/entry), capability dispatch, examples-server control (`examplesServer.start` / `stop` / `status`), and in-process QVAC (`@qvac/inference`) plus the agent loop.
- **Examples server** ([native-host/examples-server.js](../native-host/examples-server.js)): Optional `bare-http1` static server on `127.0.0.1:4173` serving synced `examples/` (or `BRIDGESWARM_EXAMPLES_DIR`). Enabled from extension settings. - **Examples server** ([native-host/examples-server.js](../native-host/examples-server.js)): Optional `bare-http1` static server on `127.0.0.1:4173` serving synced `examples/` (or `BRIDGESWARM_EXAMPLES_DIR`). Enabled from extension settings.
+17 -2
View File
@@ -1,11 +1,11 @@
# Capabilities # Capabilities
BridgeSwarm exposes selected [Bare](https://github.com/holepunchto/bare) native APIs to the page as **capability packs**. Curated packs — not every `bare-*` package — is intentional. The **default host includes media, fs, sqlite, net, qvac, and agent**. Other modules are listed in [DEFAULT-MODULES.md](DEFAULT-MODULES.md). BridgeSwarm exposes selected [Bare](https://github.com/holepunchto/bare) native APIs to the page as **capability packs**. Curated packs — not every `bare-*` package — is intentional. The default host includes media, fs, sqlite, net, qvac, agent, and discord. Other modules are listed in [DEFAULT-MODULES.md](DEFAULT-MODULES.md).
## Page API ## Page API
```js ```js
await BridgeSwarm.capabilities.list() // e.g. ['media', 'fs', 'sqlite', 'net', 'qvac', 'agent'] await BridgeSwarm.capabilities.list() // e.g. ['media', 'fs', 'sqlite', 'net', 'qvac', 'agent', 'discord']
await BridgeSwarm.capabilities.has('media') await BridgeSwarm.capabilities.has('media')
// Generic dispatch // Generic dispatch
@@ -161,6 +161,21 @@ In-process `@qvac/inference` (LLM + embeddings; optional ASR/TTS/OCR/NMT/RAG). *
Demo: [`examples/qvac-chat/`](../examples/qvac-chat/). Demo: [`examples/qvac-chat/`](../examples/qvac-chat/).
## Pack — Discord (`discord`, default)
Official discord.js 14 on Bare (`bare-discord-js`). Page surface is `BridgeSwarm.DiscordJS`. **Off until Settings → Enable Discord.** Full API, fidelity gap, and packing notes: [DISCORD.md](DISCORD.md).
| Command | Behavior |
|---------|----------|
| `discord.surface` / `status` | Constants (`GatewayIntentBits`, `Events`, …) and load state. Allowed while disabled. |
| `discord.construct` | Host `new Client` / `REST` / `WebhookClient``{ handle }` (max 4 clients) |
| `discord.call` / `get` | Invoke or read a path on a handle |
| `discord.listen` | Forward Client events as `cap-chunk` `{ pack: 'discord', kind: 'event' }` |
| `discord.destroy` | Destroy a Client (or all) |
| `discord.setEnabled` | Background only (Settings toggle) |
Demo: [`examples/discord-bot/`](../examples/discord-bot/).
## Pack — Agent (`agent`, default) ## Pack — Agent (`agent`, default)
Grok-class tool loop on QVAC (`write_file`, `search_replace`, …). Pages register extra tools with `session.addTool`. Stays idle until QVAC is enabled. See [AGENT.md](AGENT.md). Grok-class tool loop on QVAC (`write_file`, `search_replace`, …). Pages register extra tools with `session.addTool`. Stays idle until QVAC is enabled. See [AGENT.md](AGENT.md).
+4 -1
View File
@@ -43,6 +43,7 @@ Curated modules shipped in the **default** BridgeSwarm native host. Not every `b
| `@qvac/inference` + `@qvac/fabric` + `@qvac/llm-llamacpp` + `@qvac/embed-llamacpp` | `qvac` pack (optional plugins: ASR/TTS/OCR/NMT/RAG). Multimodal still input via mmproj; live desktop ring. Not image/video gen | | `@qvac/inference` + `@qvac/fabric` + `@qvac/llm-llamacpp` + `@qvac/embed-llamacpp` | `qvac` pack (optional plugins: ASR/TTS/OCR/NMT/RAG). Multimodal still input via mmproj; live desktop ring. Not image/video gen |
| `@qvac/decoder-audio` / `@qvac/langdetect-text` | Required by `@qvac/inference` import graph (audio constants / translate); not image/video gen | | `@qvac/decoder-audio` / `@qvac/langdetect-text` | Required by `@qvac/inference` import graph (audio constants / translate); not image/video gen |
| `bare-os` / `bare-subprocess` / `bare-gpu-info` | Agent shell (cwd-jailed; **not** a page pack); GPU inventory for QVAC device auto | | `bare-os` / `bare-subprocess` / `bare-gpu-info` | Agent shell (cwd-jailed; **not** a page pack); GPU inventory for QVAC device auto |
| `bare-discord-js` + `discord.js` + `bare-ws` / `bare-tls` / `bare-https` / `bare-form-data` | `discord` pack — official discord.js 14 on Bare; off until Settings → Enable Discord |
## Native addons extracted on install (`--extract-addons`) ## Native addons extracted on install (`--extract-addons`)
@@ -54,6 +55,8 @@ SQLite: `bare-sqlite` (when present as a native addon)
QVAC: `@qvac/fabric`, `@qvac/llm-llamacpp`, `@qvac/embed-llamacpp`, `bare-gpu-info` (when present) QVAC: `@qvac/fabric`, `@qvac/llm-llamacpp`, `@qvac/embed-llamacpp`, `bare-gpu-info` (when present)
Discord: `bare-tls`, `bare-crypto`, `bare-zlib` (plus `bare-ws` / `bare-https` when present as addons)
## Intentionally not default ## Intentionally not default
| Area | Examples | Reason | | Area | Examples | Reason |
@@ -63,6 +66,6 @@ QVAC: `@qvac/fabric`, `@qvac/llm-llamacpp`, `@qvac/embed-llamacpp`, `bare-gpu-in
| Raw LAN sockets as page API | `bare-tcp` / `bare-dgram` listen | Already transitive for Hyperswarm; no open page API yet | | Raw LAN sockets as page API | `bare-tcp` / `bare-dgram` listen | Already transitive for Hyperswarm; no open page API yet |
| Everything else in `bare-*` (~150) | — | Size + maintenance; curated packs only | | Everything else in `bare-*` (~150) | — | Size + maintenance; curated packs only |
See [CAPABILITIES.md](CAPABILITIES.md) for the page-facing packs built on these modules, and [QVAC.md](QVAC.md) for inference (off until Settings → Enable QVAC). See [CAPABILITIES.md](CAPABILITIES.md) for the page-facing packs built on these modules, [QVAC.md](QVAC.md) for inference (off until Settings → Enable QVAC), and [DISCORD.md](DISCORD.md) for discord.js (off until Settings → Enable Discord).
Working notes and the next-build plan: [living/README.md](living/README.md). Working notes and the next-build plan: [living/README.md](living/README.md).
+45
View File
@@ -0,0 +1,45 @@
# Discord.js on BridgeSwarm
Official **discord.js 14** runs inside the Bare native host ([`bare-discord-js`](../native-host/vendor/bare-discord-js)). Pages use `window.BridgeSwarm.DiscordJS` — the same names as Node (`Client`, `GatewayIntentBits`, `Events`, `REST`, `Routes`, builders). Discord REST has no CORS for random origins and discord.js needs Node/Bare sockets, so the Client cannot live in Chrome.
**Off until Settings → Enable Discord.** Origin allowlist still applies. Bot tokens are credentials; the demo never writes them to disk.
```js
await BridgeSwarm.ready()
const { Client, GatewayIntentBits, Events, SlashCommandBuilder, REST, Routes } = BridgeSwarm.DiscordJS
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages] })
client.once(Events.ClientReady, (c) => console.log('Logged in as', c.user.tag))
client.on(Events.InteractionCreate, async (ix) => {
if (ix.isChatInputCommand() && ix.commandName === 'ping') await ix.reply('pong')
})
await client.login(token)
```
`BridgeSwarm.ready()` also loads constants from the host (`discord.surface`). `BridgeSwarm.DiscordJS.ready()` does the same if you skip `BridgeSwarm.ready()`.
## Fidelity gap
Native messaging is async JSON (~1 MB). **Collections/cache are snapshots, not live Maps.** Use `client.channels.fetch(id)` / `client.guilds.fetch(id)` (already async in discord.js) instead of `cache.get`. `client.user` after `ClientReady` is a snapshot. Event payloads (`Message`, `ChatInputCommandInteraction`, …) hydrate stubs: sync predicates (`isChatInputCommand()`) from the snapshot; async methods (`reply`, `followUp`, `editReply`, …) RPC to the host handle.
Anything not stubbed: `BridgeSwarm.DiscordJS.invoke(handle, path, args)`.
Voice (`@discordjs/voice`) is not shipped. Clients are **tab-scoped**`pagehide` and `client.destroy()` drop the host Client. Closing the tab stops the bot.
## Host pack (`discord`)
| Command | Behavior |
|---------|----------|
| `surface` / `status` | Constants + load state. Allowed while disabled. |
| `construct` | `{ className: 'Client'\|'REST'\|'WebhookClient', args }``{ handle }`. Max 4 Clients. |
| `call` / `get` | `{ handle, path, args }` generic invoke |
| `listen` | Forward Client events as `cap-chunk` `{ pack: 'discord', kind: 'event', handle, name, args }` |
| `destroy` | `client.destroy()` + drop handles |
| `setEnabled` | Background only (Settings toggle) |
Tokens are normalized (trim, strip BOM / zero-width). They are never logged.
## Bare runtime notes
Gateway WebSocket is a WHATWG facade over `bare-ws` (`process.versions.bun` so `@discordjs/ws` does not use npm `ws`). `zlib-sync` is a **null** stub — a truthy empty module hangs `Client.login()`. REST uses the `@discordjs/rest` web/fetch build and `bare-form-data` globals for uploads.
Demo: [`examples/discord-bot/`](../examples/discord-bot/).
+2 -1
View File
@@ -7,9 +7,10 @@
| [ARCHITECTURE.md](ARCHITECTURE.md) | Components and message flow | | [ARCHITECTURE.md](ARCHITECTURE.md) | Components and message flow |
| [API-REFERENCE.md](API-REFERENCE.md) | Page API + every host request/event | | [API-REFERENCE.md](API-REFERENCE.md) | Page API + every host request/event |
| [DATA-API.md](DATA-API.md) | Hypercore / Hyperbee / Hyperdrive / Autobase / Hyperdb | | [DATA-API.md](DATA-API.md) | Hypercore / Hyperbee / Hyperdrive / Autobase / Hyperdb |
| [CAPABILITIES.md](CAPABILITIES.md) | media, fs, sqlite, net, qvac, agent packs | | [CAPABILITIES.md](CAPABILITIES.md) | media, fs, sqlite, net, qvac, agent, discord packs |
| [QVAC.md](QVAC.md) | Local QVAC inference, vision input, desktop still ingest (off until Settings → Enable QVAC) | | [QVAC.md](QVAC.md) | Local QVAC inference, vision input, desktop still ingest (off until Settings → Enable QVAC) |
| [AGENT.md](AGENT.md) | Grok-class agent API | | [AGENT.md](AGENT.md) | Grok-class agent API |
| [DISCORD.md](DISCORD.md) | discord.js 14 on the Bare host (`BridgeSwarm.DiscordJS`) |
| [DEFAULT-MODULES.md](DEFAULT-MODULES.md) | What the default Bare host ships | | [DEFAULT-MODULES.md](DEFAULT-MODULES.md) | What the default Bare host ships |
| [PROTOMUX.md](PROTOMUX.md) | In-page mux + host attachment | | [PROTOMUX.md](PROTOMUX.md) | In-page mux + host attachment |
| [HRPC.md](HRPC.md) | Host HRPC | | [HRPC.md](HRPC.md) | Host HRPC |
+5 -1
View File
@@ -31,9 +31,13 @@ After the 2026-09-03 pass. These are **known**, not forgotten.
- No integration test that boots Bare + Hyperswarm + two in-process peers. - No integration test that boots Bare + Hyperswarm + two in-process peers.
- `extension/examples/` and `native-host/examples/` are copies; forget `npm run sync:examples` and packaged demos drift. - `extension/examples/` and `native-host/examples/` are copies; forget `npm run sync:examples` and packaged demos drift.
- `api.js` `arrayBufferToBase64` is O(n) string concat — fine for chat, painful for big media frames (live-encode already JPEGs to stay small). - `api.js` `arrayBufferToBase64` is O(n) string concat — fine for chat, painful for big media frames (live-encode already JPEGs to stay small).
- Dashboard Capabilities shows QVAC/agent health (Vision, Desktop fps). Live encode job inspector still later. - Dashboard Capabilities shows QVAC/agent/Discord health (Vision, Desktop fps). Live encode job inspector still later.
- Firefox permanent install still needs Nightly/Dev + `.xpi` signing story. - Firefox permanent install still needs Nightly/Dev + `.xpi` signing story.
## Discord
- Page `BridgeSwarm.DiscordJS` is a handle proxy: Collections/cache are snapshots, not live Maps. Voice is not shipped. Bots die with the tab.
## Docs hygiene ## Docs hygiene
- `theory/bridgeswarm-for-dummies.md` is a long-form essay; it can lag the API. Prefer `docs/` + `docs/living/` as source of truth. - `theory/bridgeswarm-for-dummies.md` is a long-form essay; it can lag the API. Prefer `docs/` + `docs/living/` as source of truth.
+3
View File
@@ -8,6 +8,8 @@ Living plan. Check items off in STATUS.md when they land. Dates are ordering, no
2. ~~**QVAC + grok-class agent packs**~~ **Done.** In-process `@qvac/inference`, `BridgeSwarm.qvac` / `.agent`, OpenAI loopback on `127.0.0.1:11435`. **Off until Settings → Enable QVAC.** Multimodal **image input** (mmproj + `attachments`) and live desktop still ingest (`qvac.pushDesktopFrame` / `POST /v1/vision/frames`, ≤5 FPS) landed 2026-09-03. See [QVAC.md](../QVAC.md) and [AGENT.md](../AGENT.md). 2. ~~**QVAC + grok-class agent packs**~~ **Done.** In-process `@qvac/inference`, `BridgeSwarm.qvac` / `.agent`, OpenAI loopback on `127.0.0.1:11435`. **Off until Settings → Enable QVAC.** Multimodal **image input** (mmproj + `attachments`) and live desktop still ingest (`qvac.pushDesktopFrame` / `POST /v1/vision/frames`, ≤5 FPS) landed 2026-09-03. See [QVAC.md](../QVAC.md) and [AGENT.md](../AGENT.md).
2b. ~~**Discord.js pack**~~ **Done.** Vendored `bare-discord-js` (discord.js 14.27) as `BridgeSwarm.DiscordJS`. **Off until Settings → Enable Discord.** See [DISCORD.md](../DISCORD.md).
3. **Host-side tests under Bare** 3. **Host-side tests under Bare**
`npm test` covers net policy, origin allowlist, QVAC catalog/device fallback, agent loop (fake completion), path jail. Add a Bare script that opens an in-process Corestore + Hyperbee and exercises `handleMessage` for `beePut/Get/List`, `coreHas`, `resourceClose`. Keep it free of Chrome. `npm test` covers net policy, origin allowlist, QVAC catalog/device fallback, agent loop (fake completion), path jail. Add a Bare script that opens an in-process Corestore + Hyperbee and exercises `handleMessage` for `beePut/Get/List`, `coreHas`, `resourceClose`. Keep it free of Chrome.
@@ -58,6 +60,7 @@ Living plan. Check items off in STATUS.md when they land. Dates are ordering, no
PR1 origin allowlist for capabilities (ext: background + dashboard) PR1 origin allowlist for capabilities (ext: background + dashboard)
PR2 QVAC pack + page API + qvac-chat (native-host + extension + examples) PR2 QVAC pack + page API + qvac-chat (native-host + extension + examples)
PR3 Agent MVP + production tools (native-host/agent + agent-studio) PR3 Agent MVP + production tools (native-host/agent + agent-studio)
PR3b Discord.js pack + DiscordJS page SDK (native-host + extension + examples/discord-bot)
PR4 Bare handleMessage unit tests (native-host + scripts) PR4 Bare handleMessage unit tests (native-host + scripts)
PR5 replicate-info events + sync-demo live UI (host + examples) PR5 replicate-info events + sync-demo live UI (host + examples)
PR6 beeBatch / beeHistory (host + DATA-API + data-demo) PR6 beeBatch / beeHistory (host + DATA-API + data-demo)
+8 -6
View File
@@ -1,6 +1,6 @@
# Status — what ships # Status — what ships
Last reviewed: **2026-09-03**. Last reviewed: **2026-09-04**.
## Product ## Product
@@ -8,12 +8,12 @@ BridgeSwarm is a **desktop-only** MV3 extension + Bare native host that runs the
| Surface | Path | Role | | Surface | Path | Role |
|---------|------|------| |---------|------|------|
| Page API | `extension/api.js` | `BridgeSwarm`, connections, request, packs | | Page API | `extension/api.js` + `discordjs.js` | `BridgeSwarm`, connections, request, packs, `DiscordJS` |
| Content script | `extension/content.js` | Injects defaults + API; bridges postMessage | | Content script | `extension/content.js` | Injects defaults + API + DiscordJS; bridges postMessage |
| Background | `extension/background.js` | Native port, swarm routing, dashboard, examples toggle | | Background | `extension/background.js` | Native port, swarm routing, dashboard, examples / QVAC / Discord toggles |
| Host | `native-host/host.js` | Swarm, Hyper*, HRPC, pack dispatch | | Host | `native-host/host.js` | Swarm, Hyper*, HRPC, pack dispatch |
| Messenger | `native-host/messenger.js` | 4-byte LE + JSON (NMH ~1 MB) | | Messenger | `native-host/messenger.js` | 4-byte LE + JSON (NMH ~1 MB) |
| Packs | `native-host/capabilities/` | media, fs, sqlite, net, qvac, agent | | Packs | `native-host/capabilities/` | media, fs, sqlite, net, qvac, agent, discord |
| Examples | `examples/` (source of truth) | Synced into extension + host | | Examples | `examples/` (source of truth) | Synced into extension + host |
## Completed this pass (were missing) ## Completed this pass (were missing)
@@ -32,6 +32,7 @@ BridgeSwarm is a **desktop-only** MV3 extension + Bare native host that runs the
| Live desktop still ingest | Ring buffer ≤5 FPS. Native apps: `POST /v1/vision/frames`. HTTPS pages (Pip): `qvac.pushDesktopFrame` over NMH. Injected on each complete, not JSONL | | Live desktop still ingest | Ring buffer ≤5 FPS. Native apps: `POST /v1/vision/frames`. HTTPS pages (Pip): `qvac.pushDesktopFrame` over NMH. Injected on each complete, not JSONL |
| Agent loop quality | Parallel tools + path locks, unique `search_replace`, `rg` grep, LLM compaction, git sidecar, persistent allow/deny, MCP HTTP handshake, subagent resume | | Agent loop quality | Parallel tools + path locks, unique `search_replace`, `rg` grep, LLM compaction, git sidecar, persistent allow/deny, MCP HTTP handshake, subagent resume |
| Living docs / continued-dev plan | This folder | | Living docs / continued-dev plan | This folder |
| discord.js 14 on Bare (`BridgeSwarm.DiscordJS`) | Vendored `bare-discord-js`; Settings → Enable Discord; `examples/discord-bot/` |
## Capability packs (default host) ## Capability packs (default host)
@@ -43,8 +44,9 @@ BridgeSwarm is a **desktop-only** MV3 extension + Bare native host that runs the
| `net` | fetch | public http(s) only; private / loopback / metadata blocked; 2 MB body cap | | `net` | fetch | public http(s) only; private / loopback / metadata blocked; 2 MB body cap |
| `qvac` | detect, load, complete, chat, pushDesktopFrame, clearDesktop, embed, … | Off until Settings → Enable QVAC; `$BRIDGE_SWARM_STORAGE/qvac/` (+ `vision/`); origin allowlist | | `qvac` | detect, load, complete, chat, pushDesktopFrame, clearDesktop, embed, … | Off until Settings → Enable QVAC; `$BRIDGE_SWARM_STORAGE/qvac/` (+ `vision/`); origin allowlist |
| `agent` | create, prompt (`images`, `desktopVision`), cancel, … | Same QVAC enable gate; host workspace under `$BRIDGE_SWARM_STORAGE/agent/` unless `hostWorkspace: false` | | `agent` | create, prompt (`images`, `desktopVision`), cancel, … | Same QVAC enable gate; host workspace under `$BRIDGE_SWARM_STORAGE/agent/` unless `hostWorkspace: false` |
| `discord` | surface, construct, call, listen, destroy | Off until Settings → Enable Discord; tab-scoped Clients; tokens never logged |
Page helpers: `BridgeSwarm.media.*`, `BridgeSwarm.fs.*`, `BridgeSwarm.sqlite.*`, `BridgeSwarm.net.fetch`, `BridgeSwarm.qvac.*`, `BridgeSwarm.agent.*`. Page helpers: `BridgeSwarm.media.*`, `BridgeSwarm.fs.*`, `BridgeSwarm.sqlite.*`, `BridgeSwarm.net.fetch`, `BridgeSwarm.qvac.*`, `BridgeSwarm.agent.*`, `BridgeSwarm.DiscordJS`.
Dashboard Capabilities shows **Vision** (projector loaded) and **Desktop** (`live` / `fps` / stale). Dashboard Capabilities shows **Vision** (projector loaded) and **Desktop** (`live` / `fps` / stale).
+6 -1
View File
@@ -45,6 +45,7 @@ Shared chrome lives in [`shared/`](shared/) (`theme.css`, `chrome.css`, `boot.js
| HRPC Demo | http://127.0.0.1:4173/hrpc-demo/ | | HRPC Demo | http://127.0.0.1:4173/hrpc-demo/ |
| QVAC Chat | http://127.0.0.1:4173/qvac-chat/ | | QVAC Chat | http://127.0.0.1:4173/qvac-chat/ |
| Agent Studio | http://127.0.0.1:4173/agent-studio/ | | Agent Studio | http://127.0.0.1:4173/agent-studio/ |
| Discord Bot | http://127.0.0.1:4173/discord-bot/ |
## Connect ## Connect
@@ -103,10 +104,14 @@ Typed RPC ping/streams. See [../docs/HRPC.md](../docs/HRPC.md).
### Agent Studio (`agent-studio/`) ### Agent Studio (`agent-studio/`)
Sandboxed grok-class agent (`BridgeSwarm.agent`). Enable QVAC in Settings first. Attach or paste images; `session.prompt(text, { images })`. See [../docs/AGENT.md](../docs/AGENT.md). Sandboxed grok-class agent (`BridgeSwarm.agent`). Enable QVAC in Settings first. Attach or paste images; `session.prompt(text, { images })`. See [../docs/AGENT.md](../docs/AGENT.md).
### Discord Bot (`discord-bot/`)
`BridgeSwarm.DiscordJS` ping-pong bot. Enable Discord in Settings first. See [../docs/DISCORD.md](../docs/DISCORD.md).
## Troubleshooting ## Troubleshooting
1. Wait for content-script injection, then retry. 1. Wait for content-script injection, then retry.
2. Confirm the BridgeSwarm extension and native host are installed. 2. Confirm the BridgeSwarm extension and native host are installed.
3. Confirm you are on `http://127.0.0.1:4173/…`, not `file://`. 3. Confirm you are on `http://127.0.0.1:4173/…`, not `file://`.
4. For QVAC / Agent Studio: Control Center → Settings → **Enable QVAC**. 4. For QVAC / Agent Studio: Control Center → Settings → **Enable QVAC**.
5. For media demos on macOS after a host update: `npm run repair:macos`, then fully quit the browser. 5. For Discord Bot: Control Center → Settings → **Enable Discord**.
6. For media demos on macOS after a host update: `npm run repair:macos`, then fully quit the browser.
+40
View File
@@ -0,0 +1,40 @@
# Discord Bot — BridgeSwarm.DiscordJS
## What this example does
Runs an official **discord.js 14** bot from the page. The real `Client` lives in the Bare native host (`bare-discord-js`); the page uses `BridgeSwarm.DiscordJS` (`Client`, `GatewayIntentBits`, `Events`, `SlashCommandBuilder`, `REST`, `Routes`).
`/ping` replies `pong`. Channel text `ping``pong` only if you enable Message Content Intent in the Developer Portal and check the box on this page.
## Prerequisites
1. Install the [BridgeSwarm extension and native host](../../README.md#easy-install-recommended).
2. Control Center → Settings → **Enable Discord**.
3. Create a Discord application → Bot → Reset Token. Copy the **bot token** (not the OAuth2 client secret).
## How to run
**Recommended:** Extension Settings → enable **Examples server**, then open the demo URL below.
Dev alternative from the repo root:
```bash
npm run examples
```
Open **http://127.0.0.1:4173/discord-bot/**. Do not use `file://`.
## Usage
1. Paste the bot token (kept in this tab only).
2. Optionally paste a guild id so `/ping` registers immediately.
3. Click **Login**. Watch the log for `Logged in as …`.
4. In Discord, run `/ping`. Click **Destroy** when finished.
See [docs/DISCORD.md](../../docs/DISCORD.md) for the API, the cache fidelity gap, and packing notes.
## Files in this directory
- **index.html** — Token / guild / login UI
- **app.js**`new Client`, slash registration, ping/pong
- **style.css** — Layout
+183
View File
@@ -0,0 +1,183 @@
(function () {
const statusEl = document.getElementById('status');
const logEl = document.getElementById('log');
const tokenEl = document.getElementById('token');
const guildEl = document.getElementById('guild');
const loginBtn = document.getElementById('login');
const logoutBtn = document.getElementById('logout');
const msgContentEl = document.getElementById('msgContent');
let client = null;
function banner(kind, text) {
statusEl.className = 'bs-banner bs-banner--' + kind;
statusEl.textContent = text;
}
function log(line) {
const p = document.createElement('div');
p.textContent = line;
logEl.appendChild(p);
logEl.scrollTop = logEl.scrollHeight;
}
function setBusy(on) {
loginBtn.disabled = on;
logoutBtn.disabled = on && !client;
}
function waitForDiscordJS(timeoutMs) {
timeoutMs = timeoutMs || 8000;
const started = Date.now();
return new Promise(function (resolve, reject) {
function tick() {
const dj = window.BridgeSwarm && window.BridgeSwarm.DiscordJS;
if (dj && typeof dj.status === 'function') {
resolve(dj);
return;
}
if (Date.now() - started >= timeoutMs) {
reject(
new Error(
'BridgeSwarm.DiscordJS is missing. In chrome://extensions reload BridgeSwarm, then refresh this page.'
)
);
return;
}
setTimeout(tick, 50);
}
tick();
});
}
async function boot() {
await BridgeSwarmExamples.waitForBridgeSwarm();
let dj;
try {
dj = await waitForDiscordJS();
} catch (err) {
banner('err', err.message || String(err));
return;
}
let st = {};
try {
st = await dj.status();
} catch (err) {
banner('err', err.message || String(err));
return;
}
if (!st.loaded) {
banner('warn', 'discord.js did not load on the host' + (st.loadError ? ': ' + st.loadError : '.'));
return;
}
if (!st.enabled) {
banner('warn', 'Discord is off. BridgeSwarm Settings → Enable Discord, then refresh.');
return;
}
banner('ok', 'Discord pack ready. Paste a bot token and login.');
try {
await dj.ready();
} catch (_) {}
}
async function onLogin() {
const token = (tokenEl.value || '').trim();
if (!token) {
banner('warn', 'Paste a bot token first.');
return;
}
setBusy(true);
try {
const dj = await waitForDiscordJS();
await dj.ready();
const { Client, GatewayIntentBits, Events, SlashCommandBuilder, REST, Routes } = dj;
const intents = [GatewayIntentBits.Guilds];
if (GatewayIntentBits.DirectMessages) intents.push(GatewayIntentBits.DirectMessages);
if (GatewayIntentBits.GuildMessages) intents.push(GatewayIntentBits.GuildMessages);
if (msgContentEl.checked && GatewayIntentBits.MessageContent) {
intents.push(GatewayIntentBits.MessageContent);
}
if (client) {
try {
await client.destroy();
} catch (_) {}
client = null;
}
client = new Client({ intents: intents });
client.once(Events.ClientReady || Events.Ready || 'clientReady', async function (readyClient) {
const tag = (readyClient && readyClient.user && readyClient.user.tag) || (client.user && client.user.tag) || 'bot';
log('Logged in as ' + tag);
banner('ok', 'Logged in as ' + tag);
const ping = new SlashCommandBuilder().setName('ping').setDescription('Replies with pong.').toJSON();
try {
const rest = new REST().setToken(token);
const appId = readyClient.user && readyClient.user.id;
const guildId = (guildEl.value || '').trim();
if (guildId) {
await rest.put(Routes.applicationGuildCommands(appId, guildId), { body: [ping] });
log('Registered /ping for guild ' + guildId);
} else {
await rest.put(Routes.applicationCommands(appId), { body: [ping] });
log('Registered global /ping (may take up to an hour). Set a guild id for instant updates.');
}
} catch (err) {
log('slash register failed: ' + (err && err.message ? err.message : err));
}
});
client.on(Events.InteractionCreate || 'interactionCreate', async function (ix) {
if (!ix || typeof ix.isChatInputCommand !== 'function' || !ix.isChatInputCommand()) return;
if (ix.commandName !== 'ping') return;
try {
await ix.reply({ content: 'pong' });
log('/ping → pong');
} catch (err) {
log('reply failed: ' + (err && err.message ? err.message : err));
}
});
if (Events.MessageCreate) {
client.on(Events.MessageCreate, async function (message) {
if (!message || (message.author && message.author.bot)) return;
if (String(message.content || '').trim().toLowerCase() !== 'ping') return;
try {
await message.reply('pong');
log('message ping → pong');
} catch (err) {
log('message reply failed: ' + (err && err.message ? err.message : err));
}
});
}
client.on('error', function (err) {
log('client error: ' + (err && err.message ? err.message : err));
});
log('Logging in…');
await client.login(token);
} catch (err) {
banner('err', err.message || String(err));
log(err.message || String(err));
} finally {
setBusy(false);
}
}
async function onLogout() {
if (!client) return;
setBusy(true);
try {
await client.destroy();
client = null;
banner('ok', 'Client destroyed.');
log('Destroyed client.');
} catch (err) {
log(err.message || String(err));
} finally {
setBusy(false);
}
}
loginBtn.addEventListener('click', onLogin);
logoutBtn.addEventListener('click', onLogout);
BridgeSwarmExamples.waitForBridgeSwarm().then(boot).catch(function (err) {
banner('err', err.message || String(err));
});
})();
+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BridgeSwarm Discord Bot</title>
<link rel="stylesheet" href="../shared/theme.css">
<link rel="stylesheet" href="../shared/chrome.css">
<link rel="stylesheet" href="style.css">
<script src="../shared/boot.js"></script>
<script>if (BridgeSwarmExamples.guardFileProtocol('discord-bot/')) { /* blocked */ }</script>
</head>
<body>
<div class="bs-page">
<a class="bs-back" href="../">← Examples</a>
<p class="bs-brand">BridgeSwarm</p>
<h1 class="bs-title">Discord Bot</h1>
<p class="bs-lede">
Full <code>discord.js</code> 14 on the Bare host, as <code>BridgeSwarm.DiscordJS</code>.
Enable Discord in extension Settings, paste a <strong>bot token</strong> (not the OAuth2 client secret),
then login. The token stays in this tab and is never written to disk by the demo.
</p>
<div id="status" class="bs-banner bs-banner--info">Checking Discord pack…</div>
<div class="bs-section">
<h2>Bot</h2>
<p class="bs-meta">Developer Portal → Bot → Reset Token. Privileged Message Content intent is optional (channel ping → pong).</p>
<div class="bs-row">
<input type="password" id="token" placeholder="Bot token" autocomplete="off">
<input type="text" id="guild" placeholder="Guild id (optional, instant slash)">
<button class="primary" id="login">Login</button>
<button class="danger" id="logout">Destroy</button>
</div>
<label class="bs-meta"><input type="checkbox" id="msgContent"> Request Message Content intent</label>
</div>
<div class="bs-section">
<h2>Log</h2>
<div class="bs-log" id="log"></div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
.bs-page code {
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--accent-2);
}
#token, #guild {
min-width: 16rem;
}
+5
View File
@@ -104,6 +104,11 @@
<p>Coding agent on QVAC — live reasoning, tools, and in-thread permissions.</p> <p>Coding agent on QVAC — live reasoning, tools, and in-thread permissions.</p>
<span class="bs-path">/agent-studio/</span> <span class="bs-path">/agent-studio/</span>
</a> </a>
<a class="bs-card" href="./discord-bot/">
<h2>Discord Bot</h2>
<p>Official discord.js on the Bare host via <code>BridgeSwarm.DiscordJS</code>.</p>
<span class="bs-path">/discord-bot/</span>
</a>
</div> </div>
<div class="bs-category">Platform</div> <div class="bs-category">Platform</div>
+40
View File
@@ -44,6 +44,7 @@ let notifyOnDisconnect = false;
let debugMode = false; let debugMode = false;
let examplesServerEnabled = false; let examplesServerEnabled = false;
let qvacEnabled = false; let qvacEnabled = false;
let discordEnabled = false;
let examplesServerPort = 4173; let examplesServerPort = 4173;
let defaultFirewall = { mode: 'off', keys: [] }; let defaultFirewall = { mode: 'off', keys: [] };
let cachedSettings = {}; let cachedSettings = {};
@@ -245,6 +246,18 @@ function applySettingsFromStorage(s) {
} else { } else {
qvacEnabled = wantQvac; qvacEnabled = wantQvac;
} }
const wantDiscord = s.discordEnabled === true;
if (wantDiscord !== discordEnabled) {
discordEnabled = wantDiscord;
log('info', wantDiscord ? 'Discord enabled' : 'Discord disabled');
syncDiscordEnabled().catch((err) => log('error', 'Discord enable sync failed:', err.message || err));
} else if (port) {
discordEnabled = wantDiscord;
syncDiscordEnabled().catch(() => {});
} else {
discordEnabled = wantDiscord;
}
} }
function loadSettings() { function loadSettings() {
@@ -345,6 +358,28 @@ async function syncQvacEnabled() {
} }
} }
async function syncDiscordEnabled() {
if (!port) return;
try {
const res = await send(
{
type: 'capability',
payload: {
pack: 'discord',
cmd: 'setEnabled',
payload: { enabled: discordEnabled === true },
},
},
{ quiet: true }
);
if (res && res.ok === false) {
log('warn', 'Discord setEnabled:', res.error || 'failed');
}
} catch (err) {
log('warn', 'Discord setEnabled failed:', err.message || err);
}
}
async function refreshHostSnapshot(force) { async function refreshHostSnapshot(force) {
if (!port) { if (!port) {
hostSnapshot = null; hostSnapshot = null;
@@ -428,6 +463,10 @@ function gateCapabilityPayload(msg, sender) {
log('warn', 'Blocked QVAC/agent (disabled)', msg.type); log('warn', 'Blocked QVAC/agent (disabled)', msg.type);
return { ok: false, error: 'QVAC is disabled. Enable it in BridgeSwarm Settings.' }; return { ok: false, error: 'QVAC is disabled. Enable it in BridgeSwarm Settings.' };
} }
if (allow.isDiscordDisabled(msg, cachedSettings)) {
log('warn', 'Blocked Discord (disabled)', msg.type);
return { ok: false, error: 'Discord is disabled. Enable it in BridgeSwarm Settings.' };
}
const inner = msg.payload && typeof msg.payload === 'object' ? msg.payload : {}; const inner = msg.payload && typeof msg.payload === 'object' ? msg.payload : {};
const always = allow.isAlwaysApproveOrigin(origin, cachedSettings); const always = allow.isAlwaysApproveOrigin(origin, cachedSettings);
if (msg.type === 'capability') { if (msg.type === 'capability') {
@@ -492,6 +531,7 @@ function connect() {
syncExamplesServer().catch(() => {}); syncExamplesServer().catch(() => {});
} }
syncQvacEnabled().catch(() => {}); syncQvacEnabled().catch(() => {});
syncDiscordEnabled().catch(() => {});
syncAgentGrants().catch(() => {}); syncAgentGrants().catch(() => {});
refreshHostSnapshot(true).catch(() => {}); refreshHostSnapshot(true).catch(() => {});
+1 -1
View File
@@ -85,7 +85,7 @@ function injectApi() {
// Must inject defaults.js into the *page* world — content-script window is isolated. // Must inject defaults.js into the *page* world — content-script window is isolated.
inject( inject(
'defaults.js', 'defaults.js',
() => inject('api.js', () => inject('framed-stream.js', () => inject('protomux-bundle.js'))), () => inject('api.js', () => inject('discordjs-builders.js', () => inject('discordjs.js', () => inject('framed-stream.js', () => inject('protomux-bundle.js'))))),
{ defaults: JSON.stringify(defaults), debug: debug ? '1' : '0' } { defaults: JSON.stringify(defaults), debug: debug ? '1' : '0' }
); );
}); });
+8
View File
@@ -194,6 +194,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card" style="margin-top:1rem">
<div class="card-head"><h3>Discord</h3>
<div class="toggle" id="capDiscordToggle" role="switch" aria-checked="false" tabindex="0" title="Enable Discord"></div>
</div>
<div class="card-body" id="capDiscordBody">
<div class="empty-state muted">Waiting for host…</div>
</div>
</div>
<div class="card" style="margin-top:1rem"> <div class="card" style="margin-top:1rem">
<div class="card-head"><h3>Agent</h3></div> <div class="card-head"><h3>Agent</h3></div>
<div class="card-body" id="capAgentBody"> <div class="card-body" id="capAgentBody">
+54
View File
@@ -15,6 +15,7 @@ const DEFAULT_SETTINGS = {
debug: false, debug: false,
examplesServerEnabled: false, examplesServerEnabled: false,
qvacEnabled: false, qvacEnabled: false,
discordEnabled: false,
examplesServerPort: 4173, examplesServerPort: 4173,
defaultFirewallMode: 'off', defaultFirewallMode: 'off',
defaultFirewallKeys: [], defaultFirewallKeys: [],
@@ -295,6 +296,23 @@ function renderCapabilitiesPage(state) {
${q.error ? `<div class="health-row"><span class="k">Error</span><span class="v">${escapeHtml(q.error)}</span></div>` : ''} ${q.error ? `<div class="health-row"><span class="k">Error</span><span class="v">${escapeHtml(q.error)}</span></div>` : ''}
</div>`; </div>`;
} }
const d = h.discord || {};
const dEl = $('capDiscordBody');
const dToggle = $('capDiscordToggle');
const dOn = settings.discordEnabled === true;
if (dToggle) {
dToggle.classList.toggle('active', dOn);
dToggle.setAttribute('aria-checked', dOn ? 'true' : 'false');
}
if (dEl) {
dEl.innerHTML = `
<div class="health">
<div class="health-row"><span class="k">Enabled</span><span class="v">${dOn ? 'yes' : 'no (Settings)'}</span></div>
<div class="health-row"><span class="k">Loaded</span><span class="v">${d.loaded ? 'yes' : 'no'}</span></div>
<div class="health-row"><span class="k">Clients</span><span class="v">${d.clients ?? 0} / ${d.maxClients ?? 4}</span></div>
${d.loadError ? `<div class="health-row"><span class="k">Error</span><span class="v">${escapeHtml(d.loadError)}</span></div>` : ''}
</div>`;
}
const a = h.agent || {}; const a = h.agent || {};
const aEl = $('capAgentBody'); const aEl = $('capAgentBody');
if (aEl) { if (aEl) {
@@ -644,6 +662,13 @@ function renderSettingsForm() {
<div class="toggle" id="toggleQvacEnabled" data-key="qvacEnabled"></div> <div class="toggle" id="toggleQvacEnabled" data-key="qvacEnabled"></div>
</div> </div>
</div> </div>
<div class="settings-group">
<h3>Discord</h3>
<div class="setting-row">
<div><div class="setting-label">Enable Discord</div><div class="setting-desc">Run discord.js bots from the page via the Bare host (<code>BridgeSwarm.DiscordJS</code>). Off until you turn this on. Bot tokens are credentials.</div></div>
<div class="toggle" id="toggleDiscordEnabled" data-key="discordEnabled"></div>
</div>
</div>
<div class="settings-group"> <div class="settings-group">
<h3>Examples server</h3> <h3>Examples server</h3>
<div class="setting-row"> <div class="setting-row">
@@ -687,6 +712,7 @@ function renderSettingsForm() {
$('toggleDisableFileUrls')?.classList.toggle('active', s.disableOnFileUrls === true); $('toggleDisableFileUrls')?.classList.toggle('active', s.disableOnFileUrls === true);
$('toggleExamplesServer')?.classList.toggle('active', s.examplesServerEnabled === true); $('toggleExamplesServer')?.classList.toggle('active', s.examplesServerEnabled === true);
$('toggleQvacEnabled')?.classList.toggle('active', s.qvacEnabled === true); $('toggleQvacEnabled')?.classList.toggle('active', s.qvacEnabled === true);
$('toggleDiscordEnabled')?.classList.toggle('active', s.discordEnabled === true);
const fw = $('defaultFirewallMode'); const fw = $('defaultFirewallMode');
if (fw) fw.value = s.defaultFirewallMode || 'off'; if (fw) fw.value = s.defaultFirewallMode || 'off';
const ver = chrome.runtime.getManifest?.().version; const ver = chrome.runtime.getManifest?.().version;
@@ -799,6 +825,7 @@ function collectSettings() {
debug: $('toggleDebug')?.classList.contains('active') || false, debug: $('toggleDebug')?.classList.contains('active') || false,
examplesServerEnabled: $('toggleExamplesServer')?.classList.contains('active') || false, examplesServerEnabled: $('toggleExamplesServer')?.classList.contains('active') || false,
qvacEnabled: $('toggleQvacEnabled')?.classList.contains('active') || false, qvacEnabled: $('toggleQvacEnabled')?.classList.contains('active') || false,
discordEnabled: $('toggleDiscordEnabled')?.classList.contains('active') || false,
examplesServerPort: port, examplesServerPort: port,
defaultFirewallMode: $('defaultFirewallMode')?.value || 'off', defaultFirewallMode: $('defaultFirewallMode')?.value || 'off',
defaultFirewallKeys: keysRaw, defaultFirewallKeys: keysRaw,
@@ -826,6 +853,8 @@ function saveSettings(quiet) {
$('overviewExamplesToggle')?.classList.toggle('active', settings.examplesServerEnabled); $('overviewExamplesToggle')?.classList.toggle('active', settings.examplesServerEnabled);
$('capQvacToggle')?.classList.toggle('active', settings.qvacEnabled === true); $('capQvacToggle')?.classList.toggle('active', settings.qvacEnabled === true);
$('capQvacToggle')?.setAttribute('aria-checked', settings.qvacEnabled === true ? 'true' : 'false'); $('capQvacToggle')?.setAttribute('aria-checked', settings.qvacEnabled === true ? 'true' : 'false');
$('capDiscordToggle')?.classList.toggle('active', settings.discordEnabled === true);
$('capDiscordToggle')?.setAttribute('aria-checked', settings.discordEnabled === true ? 'true' : 'false');
}); });
} }
@@ -1048,6 +1077,31 @@ function setupEvents() {
} }
}); });
function persistDiscordEnabled(enabled) {
const st = $('toggleDiscordEnabled');
if (st) st.classList.toggle('active', enabled);
const cap = $('capDiscordToggle');
if (cap) {
cap.classList.toggle('active', enabled);
cap.setAttribute('aria-checked', enabled ? 'true' : 'false');
}
settings.discordEnabled = enabled;
chrome.storage.local.set({ [SETTINGS_KEY]: { ...settings, discordEnabled: enabled } }, () => {
if (currentState) renderCapabilitiesPage(currentState);
showToast(enabled ? 'Discord enabled' : 'Discord disabled', 'ok');
});
}
$('capDiscordToggle')?.addEventListener('click', () => {
persistDiscordEnabled(settings.discordEnabled !== true);
});
$('capDiscordToggle')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.currentTarget.click();
}
});
function persistQvacEnabled(enabled) { function persistQvacEnabled(enabled) {
const st = $('toggleQvacEnabled'); const st = $('toggleQvacEnabled');
if (st) st.classList.toggle('active', enabled); if (st) st.classList.toggle('active', enabled);
+304
View File
@@ -0,0 +1,304 @@
/**
* Page-local discord.js builders (sync, no RPC). Subset used by typical bots.
*/
(function (root) {
function toJSON() {
return this.data ? JSON.parse(JSON.stringify(this.data)) : {};
}
function option(type, name, description, required) {
return {
type: type,
name: name,
description: description || name,
required: !!required,
};
}
function SlashCommandBuilder() {
this.data = { name: '', description: '', options: [], type: 1 };
}
SlashCommandBuilder.prototype.setName = function (name) {
this.data.name = String(name);
return this;
};
SlashCommandBuilder.prototype.setDescription = function (desc) {
this.data.description = String(desc);
return this;
};
SlashCommandBuilder.prototype.setNameLocalizations = function (loc) {
this.data.name_localizations = loc;
return this;
};
SlashCommandBuilder.prototype.setDescriptionLocalizations = function (loc) {
this.data.description_localizations = loc;
return this;
};
SlashCommandBuilder.prototype.setDefaultMemberPermissions = function (perm) {
this.data.default_member_permissions = perm == null ? null : String(perm);
return this;
};
SlashCommandBuilder.prototype.setDMPermission = function (v) {
this.data.dm_permission = !!v;
return this;
};
SlashCommandBuilder.prototype.addStringOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(3)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addIntegerOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(4)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addBooleanOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(5)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addUserOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(6)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addChannelOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(7)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addRoleOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(8)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addNumberOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(10)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.addAttachmentOption = function (input) {
const o = typeof input === 'function' ? input(new SlashOpt(11)) : input;
this.data.options.push(o && o.toJSON ? o.toJSON() : o);
return this;
};
SlashCommandBuilder.prototype.toJSON = toJSON;
function SlashOpt(type) {
this.data = { type: type, name: '', description: '', required: false };
}
SlashOpt.prototype.setName = function (n) {
this.data.name = String(n);
return this;
};
SlashOpt.prototype.setDescription = function (d) {
this.data.description = String(d);
return this;
};
SlashOpt.prototype.setRequired = function (v) {
this.data.required = !!v;
return this;
};
SlashOpt.prototype.addChoices = function () {
const list = [];
for (let i = 0; i < arguments.length; i++) list.push(arguments[i]);
this.data.choices = list;
return this;
};
SlashOpt.prototype.toJSON = toJSON;
function EmbedBuilder() {
this.data = { type: 'rich' };
}
EmbedBuilder.prototype.setTitle = function (t) {
this.data.title = String(t);
return this;
};
EmbedBuilder.prototype.setDescription = function (d) {
this.data.description = String(d);
return this;
};
EmbedBuilder.prototype.setColor = function (c) {
this.data.color = typeof c === 'number' ? c : parseInt(String(c).replace('#', ''), 16);
return this;
};
EmbedBuilder.prototype.setURL = function (u) {
this.data.url = String(u);
return this;
};
EmbedBuilder.prototype.setTimestamp = function (d) {
this.data.timestamp = (d ? new Date(d) : new Date()).toISOString();
return this;
};
EmbedBuilder.prototype.setFooter = function (f) {
this.data.footer = typeof f === 'string' ? { text: f } : f;
return this;
};
EmbedBuilder.prototype.setAuthor = function (a) {
this.data.author = typeof a === 'string' ? { name: a } : a;
return this;
};
EmbedBuilder.prototype.setThumbnail = function (u) {
this.data.thumbnail = typeof u === 'string' ? { url: u } : u;
return this;
};
EmbedBuilder.prototype.setImage = function (u) {
this.data.image = typeof u === 'string' ? { url: u } : u;
return this;
};
EmbedBuilder.prototype.addFields = function () {
if (!this.data.fields) this.data.fields = [];
for (let i = 0; i < arguments.length; i++) {
const f = arguments[i];
if (Array.isArray(f)) this.data.fields.push.apply(this.data.fields, f);
else this.data.fields.push(f);
}
return this;
};
EmbedBuilder.prototype.toJSON = toJSON;
function ActionRowBuilder() {
this.data = { type: 1, components: [] };
}
ActionRowBuilder.prototype.addComponents = function () {
for (let i = 0; i < arguments.length; i++) {
const c = arguments[i];
if (Array.isArray(c)) {
for (let j = 0; j < c.length; j++) {
this.data.components.push(c[j] && c[j].toJSON ? c[j].toJSON() : c[j]);
}
} else {
this.data.components.push(c && c.toJSON ? c.toJSON() : c);
}
}
return this;
};
ActionRowBuilder.prototype.toJSON = toJSON;
const ButtonStyle = { Primary: 1, Secondary: 2, Success: 3, Danger: 4, Link: 5 };
function ButtonBuilder() {
this.data = { type: 2, style: 1 };
}
ButtonBuilder.prototype.setCustomId = function (id) {
this.data.custom_id = String(id);
return this;
};
ButtonBuilder.prototype.setLabel = function (l) {
this.data.label = String(l);
return this;
};
ButtonBuilder.prototype.setStyle = function (s) {
this.data.style = typeof s === 'number' ? s : ButtonStyle[s] || 1;
return this;
};
ButtonBuilder.prototype.setDisabled = function (v) {
this.data.disabled = !!v;
return this;
};
ButtonBuilder.prototype.setURL = function (u) {
this.data.url = String(u);
this.data.style = 5;
return this;
};
ButtonBuilder.prototype.setEmoji = function (e) {
this.data.emoji = typeof e === 'string' ? { name: e } : e;
return this;
};
ButtonBuilder.prototype.toJSON = toJSON;
function StringSelectMenuBuilder() {
this.data = { type: 3, custom_id: '', options: [] };
}
StringSelectMenuBuilder.prototype.setCustomId = function (id) {
this.data.custom_id = String(id);
return this;
};
StringSelectMenuBuilder.prototype.setPlaceholder = function (p) {
this.data.placeholder = String(p);
return this;
};
StringSelectMenuBuilder.prototype.addOptions = function () {
for (let i = 0; i < arguments.length; i++) {
const o = arguments[i];
if (Array.isArray(o)) this.data.options.push.apply(this.data.options, o);
else this.data.options.push(o && o.toJSON ? o.toJSON() : o);
}
return this;
};
StringSelectMenuBuilder.prototype.toJSON = toJSON;
function AttachmentBuilder(data, extra) {
this.attachment = data;
this.name = (extra && extra.name) || 'file';
this.description = extra && extra.description;
}
AttachmentBuilder.prototype.setName = function (n) {
this.name = String(n);
return this;
};
AttachmentBuilder.prototype.toJSON = function () {
return { attachment: this.attachment, name: this.name, description: this.description };
};
const Routes = {
applicationCommands: function (id) {
return '/applications/' + id + '/commands';
},
applicationCommand: function (id, cid) {
return '/applications/' + id + '/commands/' + cid;
},
applicationGuildCommands: function (id, gid) {
return '/applications/' + id + '/guilds/' + gid + '/commands';
},
applicationGuildCommand: function (id, gid, cid) {
return '/applications/' + id + '/guilds/' + gid + '/commands/' + cid;
},
channel: function (id) {
return '/channels/' + id;
},
channelMessages: function (id) {
return '/channels/' + id + '/messages';
},
channelMessage: function (id, mid) {
return '/channels/' + id + '/messages/' + mid;
},
gatewayBot: function () {
return '/gateway/bot';
},
user: function (id) {
return '/users/' + (id || '@me');
},
oauth2CurrentApplication: function () {
return '/oauth2/applications/@me';
},
};
const MessageFlags = { Ephemeral: 64, SuppressEmbeds: 4, SuppressNotifications: 4096 };
const ChannelType = {
GuildText: 0,
DM: 1,
GuildVoice: 2,
GroupDM: 3,
GuildCategory: 4,
GuildAnnouncement: 5,
AnnouncementThread: 10,
PublicThread: 11,
PrivateThread: 12,
GuildStageVoice: 13,
GuildForum: 15,
};
root.BridgeSwarmDiscordBuilders = {
SlashCommandBuilder: SlashCommandBuilder,
EmbedBuilder: EmbedBuilder,
ActionRowBuilder: ActionRowBuilder,
ButtonBuilder: ButtonBuilder,
StringSelectMenuBuilder: StringSelectMenuBuilder,
AttachmentBuilder: AttachmentBuilder,
ButtonStyle: ButtonStyle,
Routes: Routes,
MessageFlags: MessageFlags,
ChannelType: ChannelType,
};
})(typeof window !== 'undefined' ? window : globalThis);
+371
View File
@@ -0,0 +1,371 @@
/**
* BridgeSwarm.DiscordJS page-side discord.js 14 surface.
* Real Client/REST live on the Bare host; this file proxies them over native messaging.
*/
(function () {
function cap(cmd, payload, options) {
const bs = window.BridgeSwarm;
if (!bs || !bs.capabilities || typeof bs.capabilities.call !== 'function') {
return Promise.reject(
new Error('BridgeSwarm.capabilities is missing. Reload the extension, then refresh this page.')
);
}
return bs.capabilities.call('discord', cmd, payload || {}, options || {});
}
function EventEmitter() {
this._listeners = {};
}
EventEmitter.prototype.on = function (ev, fn) {
if (!this._listeners[ev]) this._listeners[ev] = [];
this._listeners[ev].push(fn);
return this;
};
EventEmitter.prototype.once = function (ev, fn) {
const self = this;
function wrap() {
self.off(ev, wrap);
return fn.apply(this, arguments);
}
wrap._orig = fn;
return this.on(ev, wrap);
};
EventEmitter.prototype.off = function (ev, fn) {
if (!this._listeners[ev]) return this;
this._listeners[ev] = this._listeners[ev].filter(function (x) {
return x !== fn && x._orig !== fn;
});
return this;
};
EventEmitter.prototype.emit = function (ev) {
const args = Array.prototype.slice.call(arguments, 1);
const list = (this._listeners[ev] || []).slice();
const all = (this._listeners['*'] || []).slice();
for (let i = 0; i < list.length; i++) {
try {
list[i].apply(this, args);
} catch (_) {}
}
for (let i = 0; i < all.length; i++) {
try {
all[i].apply(this, [ev].concat(args));
} catch (_) {}
}
return this;
};
const STUB_METHODS = [
'reply',
'followUp',
'editReply',
'deferReply',
'deferUpdate',
'deleteReply',
'fetchReply',
'showModal',
'update',
'react',
'delete',
'edit',
'send',
'startThread',
'pin',
'unpin',
'fetch',
'put',
'post',
'get',
'patch',
];
function encodeArg(arg) {
if (arg == null) return arg;
if (typeof arg !== 'object') return arg;
if (arg._handle) return { _handle: arg._handle };
if (typeof arg.toJSON === 'function') return arg.toJSON();
if (Array.isArray(arg)) return arg.map(encodeArg);
return arg;
}
function hydrate(value) {
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(hydrate);
if (value._handle) return makeStub(value);
return value;
}
function makeStub(snap) {
const obj = Object.assign({}, snap);
const methods = snap._methods && snap._methods.length ? snap._methods : STUB_METHODS;
for (let i = 0; i < methods.length; i++) {
(function (name) {
if (typeof obj[name] === 'function') return;
obj[name] = function () {
const args = Array.prototype.slice.call(arguments).map(encodeArg);
return cap('call', { handle: snap._handle, path: name, args: args }).then(function (r) {
return hydrate(r && r.value);
});
};
})(methods[i]);
}
const preds = [
'isChatInputCommand',
'isButton',
'isRepliable',
'isStringSelectMenu',
'isAnySelectMenu',
'isModalSubmit',
'isAutocomplete',
'isContextMenuCommand',
'isMessageComponent',
'isCommand',
];
for (let i = 0; i < preds.length; i++) {
(function (name) {
const flag = snap[name];
obj[name] = function () {
return flag === true || flag === false ? flag : name === 'isRepliable';
};
})(preds[i]);
}
if (obj.channel && obj.channel._handle && !obj.channel.send) {
obj.channel = makeStub(obj.channel);
}
return obj;
}
const clients = [];
function Client(options) {
EventEmitter.call(this);
const self = this;
this.options = options || {};
this.user = null;
this.readyAt = null;
this._handle = null;
this._destroyed = false;
this._ready = cap('construct', { className: 'Client', args: [this.options] }).then(function (r) {
if (!r || !r.handle) throw new Error((r && r.error) || 'Client construct failed');
self._handle = r.handle;
clients.push(self);
return cap('listen', { handle: r.handle }).then(function () {
return r;
});
});
}
Client.prototype = Object.create(EventEmitter.prototype);
Client.prototype.login = function (token) {
const self = this;
return this._ready.then(function () {
return cap(
'call',
{ handle: self._handle, path: 'login', args: [token] },
{ timeoutMs: 0 }
);
}).then(function (r) {
return r && r.value != null ? r.value : r;
});
};
Client.prototype.destroy = function () {
const self = this;
this._destroyed = true;
const i = clients.indexOf(this);
if (i >= 0) clients.splice(i, 1);
if (!this._handle) {
return this._ready.then(function () {
return cap('destroy', { handle: self._handle });
}).catch(function () {});
}
return cap('destroy', { handle: this._handle }).catch(function () {});
};
Client.prototype.invoke = function (path, args) {
const self = this;
return this._ready.then(function () {
return cap('call', {
handle: self._handle,
path: path,
args: (args || []).map(encodeArg),
}).then(function (r) {
return hydrate(r && r.value);
});
});
};
function REST(options) {
this._options = options || {};
this._handle = null;
this._ready = cap('construct', { className: 'REST', args: [this._options] }).then(function (r) {
if (!r || !r.handle) throw new Error((r && r.error) || 'REST construct failed');
return r;
});
const self = this;
this._ready.then(function (r) {
self._handle = r.handle;
});
}
REST.prototype.setToken = function (token) {
const self = this;
this._ready = this._ready.then(function (r) {
return cap('call', { handle: r.handle, path: 'setToken', args: [token] }).then(function () {
return r;
});
});
return this;
};
function restVerb(name) {
REST.prototype[name] = function (route, opts) {
const self = this;
return this._ready.then(function (r) {
return cap(
'call',
{ handle: r.handle, path: name, args: [route, opts] },
{ timeoutMs: 0 }
).then(function (res) {
return hydrate(res && res.value);
});
});
};
}
restVerb('put');
restVerb('post');
restVerb('get');
restVerb('patch');
REST.prototype.delete = function (route, opts) {
const self = this;
return this._ready.then(function (r) {
return cap('call', { handle: r.handle, path: 'delete', args: [route, opts] }, { timeoutMs: 0 }).then(
function (res) {
return hydrate(res && res.value);
}
);
});
};
const DiscordJS = {
Client: Client,
REST: REST,
invoke: function (handle, path, args) {
return cap('call', { handle: handle, path: path, args: (args || []).map(encodeArg) }).then(
function (r) {
return hydrate(r && r.value);
}
);
},
status: function () {
return cap('status', {});
},
ready: null,
};
function applyBuilders() {
const b = window.BridgeSwarmDiscordBuilders || {};
const keys = [
'SlashCommandBuilder',
'EmbedBuilder',
'ActionRowBuilder',
'ButtonBuilder',
'StringSelectMenuBuilder',
'AttachmentBuilder',
'ButtonStyle',
'Routes',
'MessageFlags',
'ChannelType',
];
for (let i = 0; i < keys.length; i++) {
if (b[keys[i]] && DiscordJS[keys[i]] == null) DiscordJS[keys[i]] = b[keys[i]];
}
}
applyBuilders();
let surfacePromise = null;
function ensureSurface() {
if (surfacePromise) return surfacePromise;
applyBuilders();
surfacePromise = cap('surface', {})
.then(function (r) {
const s = (r && r.surface) || {};
Object.keys(s).forEach(function (k) {
if (k.charAt(0) === '_') return;
if (DiscordJS[k] == null) DiscordJS[k] = s[k];
});
if (s.Events && s.Events.ClientReady && !s.Events.Ready) {
s.Events.Ready = s.Events.ClientReady;
DiscordJS.Events = s.Events;
}
return DiscordJS;
})
.catch(function (err) {
surfacePromise = null;
throw err;
});
return surfacePromise;
}
DiscordJS.ready = ensureSurface;
function onChunk(p) {
if (!p || p.pack !== 'discord' || p.kind !== 'event') return;
for (let i = 0; i < clients.length; i++) {
const c = clients[i];
if (c._handle !== p.handle) continue;
const args = (p.args || []).map(hydrate);
if (p.name === 'clientReady' || p.name === 'ready') {
const readyClient = args[0];
if (readyClient && readyClient.user) c.user = readyClient.user;
c.readyAt = new Date();
}
c.emit.apply(c, [p.name].concat(args));
break;
}
}
function attach() {
if (!window.BridgeSwarm) return false;
if (window.BridgeSwarm.DiscordJS && window.BridgeSwarm.DiscordJS.Client === Client) return true;
window.BridgeSwarm.DiscordJS = DiscordJS;
window.BridgeSwarm.discordJS = DiscordJS;
if (window.BridgeSwarm.capabilities && typeof window.BridgeSwarm.capabilities.on === 'function') {
window.BridgeSwarm.capabilities.on('cap-chunk', onChunk);
}
const prevReady = window.BridgeSwarm.ready;
if (typeof prevReady === 'function' && !prevReady._bsDiscordWrapped) {
const wrapped = function (opts) {
return prevReady.call(window.BridgeSwarm, opts).then(function (BS) {
return ensureSurface()
.then(function () {
return BS;
})
.catch(function () {
return BS;
});
});
};
wrapped._bsDiscordWrapped = true;
window.BridgeSwarm.ready = wrapped;
} else if (typeof prevReady !== 'function') {
ensureSurface().catch(function () {});
}
window.addEventListener('pagehide', function () {
const list = clients.slice();
for (let i = 0; i < list.length; i++) {
try {
list[i].destroy();
} catch (_) {}
}
});
try {
window.dispatchEvent(new CustomEvent('bridge-swarm-discord-ready'));
} catch (_) {}
return true;
}
if (!attach()) {
window.addEventListener('bridge-swarm-ready', function () {
attach();
});
let n = 0;
const t = setInterval(function () {
n += 1;
if (attach() || n > 80) clearInterval(t);
}, 50);
}
})();
+2
View File
@@ -37,6 +37,8 @@
{ {
"resources": [ "resources": [
"api.js", "api.js",
"discordjs-builders.js",
"discordjs.js",
"framed-stream.js", "framed-stream.js",
"protomux-bundle.js", "protomux-bundle.js",
"defaults.js", "defaults.js",
+2
View File
@@ -36,6 +36,8 @@
{ {
"resources": [ "resources": [
"api.js", "api.js",
"discordjs-builders.js",
"discordjs.js",
"framed-stream.js", "framed-stream.js",
"protomux-bundle.js", "protomux-bundle.js",
"defaults.js", "defaults.js",
+15 -2
View File
@@ -14,7 +14,7 @@
})(typeof globalThis !== 'undefined' ? globalThis : this, function () { })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
const DEFAULT_EXAMPLES_PORT = 4173; const DEFAULT_EXAMPLES_PORT = 4173;
const CAP_PACK_PREFIX = const CAP_PACK_PREFIX =
/^(media|fs|sqlite|net|qvac|agent)\.[a-zA-Z0-9_-]+$/; /^(media|fs|sqlite|net|qvac|agent|discord)\.[a-zA-Z0-9_-]+$/;
function defaultOrigins(examplesPort) { function defaultOrigins(examplesPort) {
const port = Number(examplesPort) > 0 ? Number(examplesPort) : DEFAULT_EXAMPLES_PORT; const port = Number(examplesPort) > 0 ? Number(examplesPort) : DEFAULT_EXAMPLES_PORT;
@@ -110,13 +110,14 @@
const QVAC_META_CMDS = { detect: 1, status: 1, catalog: 1, openaiStatus: 1 }; const QVAC_META_CMDS = { detect: 1, status: 1, catalog: 1, openaiStatus: 1 };
const AGENT_META_CMDS = { status: 1, setGrants: 1, list: 1 }; const AGENT_META_CMDS = { status: 1, setGrants: 1, list: 1 };
const DISCORD_META_CMDS = { surface: 1, status: 1 };
function qvacPackAndCmd(msg) { function qvacPackAndCmd(msg) {
if (!msg || !msg.type) return null; if (!msg || !msg.type) return null;
if (msg.type === 'capability' && msg.payload) { if (msg.type === 'capability' && msg.payload) {
return { pack: msg.payload.pack, cmd: msg.payload.cmd }; return { pack: msg.payload.pack, cmd: msg.payload.cmd };
} }
const m = typeof msg.type === 'string' && msg.type.match(/^(qvac|agent)\.([a-zA-Z0-9_-]+)$/); const m = typeof msg.type === 'string' && msg.type.match(/^(qvac|agent|discord)\.([a-zA-Z0-9_-]+)$/);
if (m) return { pack: m[1], cmd: m[2] }; if (m) return { pack: m[1], cmd: m[2] };
return null; return null;
} }
@@ -133,6 +134,17 @@
return true; return true;
} }
/** True when Discord construct/login must be blocked (user has not enabled Discord).
* discord.setEnabled is never allowed from pages. surface/status stay available. */
function isDiscordDisabled(msg, settings) {
const pc = qvacPackAndCmd(msg);
if (pc && pc.pack === 'discord' && pc.cmd === 'setEnabled') return true;
if (settings && settings.discordEnabled === true) return false;
if (!pc || pc.pack !== 'discord') return false;
if (DISCORD_META_CMDS[pc.cmd]) return false;
return true;
}
return { return {
DEFAULT_EXAMPLES_PORT, DEFAULT_EXAMPLES_PORT,
defaultOrigins, defaultOrigins,
@@ -146,5 +158,6 @@
isCapabilityType, isCapabilityType,
qvacPackAndCmd, qvacPackAndCmd,
isQvacDisabled, isQvacDisabled,
isDiscordDisabled,
}; };
}); });
+4
View File
@@ -65,6 +65,10 @@ export const DEFAULT_ADDON_PACKAGES = [
'@qvac/llm-llamacpp', '@qvac/llm-llamacpp',
'@qvac/embed-llamacpp', '@qvac/embed-llamacpp',
'bare-gpu-info', 'bare-gpu-info',
// Discord (bare-discord-js / discord.js gateway)
'bare-tls',
'bare-crypto',
'bare-zlib',
]; ];
/** @deprecated Use DEFAULT_ADDON_PACKAGES — media is default. */ /** @deprecated Use DEFAULT_ADDON_PACKAGES — media is default. */
+367
View File
@@ -0,0 +1,367 @@
/**
* Discord capability pack real discord.js Client on the Bare host.
* Page talks to it as BridgeSwarm.DiscordJS.
*/
'use strict';
const {
loadBareDiscordJsSync,
getBareDiscordJsLoadError,
normalizeDiscordToken,
} = require('../discord/load-discord.js');
const {
intern,
snapshotValue,
snapshotEventArgs,
serializeConstants,
createHandleTable,
} = require('../discord/snapshot.js');
const MAX_CLIENTS = 4;
const SKIP_EVENTS = new Set(['raw', 'debug', 'apiRequest', 'apiResponse']);
const CONSTRUCT_ALLOWED = new Set(['Client', 'REST', 'WebhookClient']);
let enabled = false;
const handles = createHandleTable();
/** handle -> { names: Set, listener } */
const listeners = new Map();
let clientCount = 0;
function logErr(msg) {
try {
if (process.stderr) process.stderr.write('[bridge-swarm-host] discord: ' + msg + '\n');
} catch (_) {}
}
function getDiscord() {
const d = loadBareDiscordJsSync();
if (!d || typeof d.Client !== 'function') {
const err = getBareDiscordJsLoadError() || 'bare-discord-js unavailable';
throw new Error(err);
}
return d;
}
function decodeArg(arg) {
if (arg && typeof arg === 'object' && typeof arg._handle === 'string' && handles.byId.has(arg._handle)) {
return handles.byId.get(arg._handle);
}
if (Array.isArray(arg)) return arg.map(decodeArg);
return arg;
}
function decodeArgs(args) {
if (!Array.isArray(args)) return [];
return args.map(decodeArg);
}
function sanitizeConstructArgs(className, args) {
const a = Array.isArray(args) ? args.map((x) => decodeArg(x)) : [];
if (className === 'Client' && a[0] && typeof a[0] === 'object') {
const o = a[0];
const clean = {};
if (o.intents != null) clean.intents = o.intents;
if (o.partials != null) clean.partials = o.partials;
if (o.failIfNotExists != null) clean.failIfNotExists = o.failIfNotExists;
if (o.presence && typeof o.presence === 'object') clean.presence = o.presence;
a[0] = clean;
}
if ((className === 'REST' || className === 'WebhookClient') && a[0] && typeof a[0] === 'object') {
const o = a[0];
const clean = {};
if (typeof o.version === 'number' || typeof o.version === 'string') clean.version = o.version;
if (typeof o.timeout === 'number') clean.timeout = o.timeout;
if (typeof o.authPrefix === 'string') clean.authPrefix = o.authPrefix;
a[0] = clean;
}
return a;
}
function getAtPath(obj, path) {
const parts = String(path || '').split('.').filter(Boolean);
let ctx = obj;
let fn = obj;
for (let i = 0; i < parts.length; i++) {
ctx = fn;
if (fn == null) return { ctx: null, fn: undefined };
fn = fn[parts[i]];
}
return { ctx, fn };
}
function wrapResult(result) {
if (result == null || typeof result !== 'object') return { value: result };
if (typeof result.then === 'function') {
return Promise.resolve(result).then(wrapResult);
}
return { value: snapshotValue(result, handles, 0) };
}
function dropHandle(handle) {
const obj = handles.byId.get(handle);
const rec = listeners.get(handle);
if (rec && obj && typeof obj.off === 'function') {
try {
obj.off(rec.event || '*');
} catch (_) {}
}
if (rec && rec.unlisten) {
try {
rec.unlisten();
} catch (_) {}
}
listeners.delete(handle);
if (obj) {
const type = (obj.constructor && obj.constructor.name) || '';
if (type === 'Client') {
clientCount = Math.max(0, clientCount - 1);
if (typeof obj.destroy === 'function') {
Promise.resolve(obj.destroy()).catch(() => {});
}
}
}
handles.byId.delete(handle);
handles.meta.delete(handle);
}
function destroyAll() {
const ids = Array.from(handles.byId.keys());
for (const id of ids) dropHandle(id);
clientCount = 0;
}
function refuseDisabled(ctx, allowMeta) {
if (enabled) return false;
if (allowMeta) return false;
ctx.reply({ ok: false, error: 'Discord is disabled. Enable it in BridgeSwarm Settings.' });
return true;
}
function createDiscordPack() {
const commands = {
async status(ctx) {
let loaded = false;
let loadError = '';
try {
const d = loadBareDiscordJsSync();
loaded = !!(d && typeof d.Client === 'function');
if (!loaded) loadError = getBareDiscordJsLoadError();
} catch (err) {
loadError = err && err.message ? err.message : String(err);
}
ctx.reply({
ok: true,
enabled,
loaded,
loadError: loadError || undefined,
clients: clientCount,
maxClients: MAX_CLIENTS,
});
},
async setEnabled(ctx) {
const on = !!(ctx.payload && ctx.payload.enabled === true);
enabled = on;
if (!on) destroyAll();
ctx.reply({ ok: true, enabled, clients: clientCount });
},
async surface(ctx) {
try {
const discord = getDiscord();
ctx.reply({
ok: true,
enabled,
loaded: true,
surface: serializeConstants(discord),
});
} catch (err) {
ctx.reply({
ok: false,
error: err.message,
enabled,
loaded: false,
loadError: getBareDiscordJsLoadError() || err.message,
});
}
},
async construct(ctx) {
if (refuseDisabled(ctx)) return;
try {
const discord = getDiscord();
const className = String((ctx.payload && ctx.payload.className) || 'Client');
if (!CONSTRUCT_ALLOWED.has(className)) {
ctx.reply({ ok: false, error: 'class not allowed: ' + className });
return;
}
const Ctor = discord[className];
if (typeof Ctor !== 'function') {
ctx.reply({ ok: false, error: className + ' is not available' });
return;
}
if (className === 'Client' && clientCount >= MAX_CLIENTS) {
ctx.reply({ ok: false, error: 'too many Discord clients (max ' + MAX_CLIENTS + ')' });
return;
}
const args = sanitizeConstructArgs(className, (ctx.payload && ctx.payload.args) || []);
const inst = new Ctor(...args);
const handle = intern(handles, inst);
if (className === 'Client') clientCount += 1;
handles.meta.get(handle).origin = ctx.payload && ctx.payload._origin;
ctx.reply({
ok: true,
handle,
_type: className,
_methods: className === 'Client' ? ['login', 'destroy'] : ['setToken', 'put', 'post', 'get', 'patch', 'delete'],
});
} catch (err) {
ctx.reply({ ok: false, error: err.message });
}
},
async call(ctx) {
if (refuseDisabled(ctx)) return;
try {
const handle = ctx.payload && ctx.payload.handle;
const path = (ctx.payload && ctx.payload.path) || '';
const obj = handles.byId.get(handle);
if (!obj) {
ctx.reply({ ok: false, error: 'unknown handle' });
return;
}
let args = decodeArgs((ctx.payload && ctx.payload.args) || []);
if (path === 'login' || path === 'setToken') {
if (typeof args[0] === 'string') args[0] = normalizeDiscordToken(args[0]);
}
const { ctx: recv, fn } = getAtPath(obj, path);
if (typeof fn !== 'function') {
ctx.reply({ ok: true, value: snapshotValue(fn, handles, 0) });
return;
}
const result = await wrapResult(fn.apply(recv, args));
ctx.reply(Object.assign({ ok: true }, result));
} catch (err) {
ctx.reply({ ok: false, error: err.message });
}
},
async get(ctx) {
if (refuseDisabled(ctx)) return;
try {
const handle = ctx.payload && ctx.payload.handle;
const path = (ctx.payload && ctx.payload.path) || '';
const obj = handles.byId.get(handle);
if (!obj) {
ctx.reply({ ok: false, error: 'unknown handle' });
return;
}
const { fn } = getAtPath(obj, path);
ctx.reply({ ok: true, value: snapshotValue(fn, handles, 0) });
} catch (err) {
ctx.reply({ ok: false, error: err.message });
}
},
async listen(ctx) {
if (refuseDisabled(ctx)) return;
const handle = ctx.payload && ctx.payload.handle;
const obj = handles.byId.get(handle);
if (!obj || typeof obj.on !== 'function') {
ctx.reply({ ok: false, error: 'handle is not an EventEmitter' });
return;
}
if (listeners.has(handle)) {
ctx.reply({ ok: true, handle, listening: true });
return;
}
const emit = ctx.emit;
const includeDebug = !!(ctx.payload && ctx.payload.debug);
const listener = function (eventName) {
if (!includeDebug && SKIP_EVENTS.has(String(eventName))) return;
const args = Array.prototype.slice.call(arguments, 1);
let payload;
try {
payload = snapshotEventArgs(args, handles);
} catch (err) {
payload = [{ _type: 'Error', message: err.message }];
}
try {
emit('cap-chunk', {
pack: 'discord',
kind: 'event',
handle,
name: eventName,
args: payload,
});
} catch (err) {
logErr('emit failed: ' + (err && err.message));
}
};
obj.on('error', function (err) {
listener('error', err);
});
if (typeof obj.on === 'function') {
obj.on('*', function () {});
}
const origEmit = obj.emit;
if (typeof origEmit === 'function') {
obj.emit = function (eventName) {
try {
listener.apply(null, arguments);
} catch (_) {}
return origEmit.apply(obj, arguments);
};
}
listeners.set(handle, {
unlisten() {
if (typeof origEmit === 'function') obj.emit = origEmit;
},
});
ctx.reply({ ok: true, handle, listening: true });
},
async destroy(ctx) {
const handle = ctx.payload && ctx.payload.handle;
if (handle) {
dropHandle(handle);
ctx.reply({ ok: true, handle });
return;
}
destroyAll();
ctx.reply({ ok: true, clients: 0 });
},
};
return {
id: 'discord',
commands,
getPublicStatus() {
let loaded = false;
try {
const d = loadBareDiscordJsSync();
loaded = !!(d && typeof d.Client === 'function');
} catch (_) {}
return {
installed: true,
enabled,
clients: clientCount,
maxClients: MAX_CLIENTS,
loaded,
loadError: getBareDiscordJsLoadError() || undefined,
};
},
onLoad() {},
cleanup() {
destroyAll();
},
};
}
module.exports = {
createDiscordPack,
normalizeDiscordToken,
serializeConstants: require('../discord/snapshot.js').serializeConstants,
snapshotValue: require('../discord/snapshot.js').snapshotValue,
createHandleTable: require('../discord/snapshot.js').createHandleTable,
};
+139
View File
@@ -0,0 +1,139 @@
/**
* Load vendored bare-discord-js. CJS only the ESM entry uses node:module.
* Apply WHATWG WS + FormData before requiring the package.
*/
'use strict';
const { takePackedDiscordJs, unwrapDiscordModule } = require('./registry.js');
let discordJsCache;
let discordJsTried = false;
let discordJsLastError = '';
function getBareDiscordJsLoadError() {
return discordJsLastError;
}
function noteDiscordLoadError(err) {
const msg =
err && typeof err === 'object' && err.message
? String(err.message || err)
: String(err || 'unknown error');
discordJsLastError = msg.slice(0, 800);
return discordJsLastError;
}
function bareRuntime() {
if (typeof globalThis.Bare !== 'undefined') return true;
const v = globalThis.process && globalThis.process.versions;
return Boolean(v && typeof v.bare === 'string');
}
function normalizeDiscordToken(raw) {
if (typeof raw !== 'string') return '';
let t = raw.trim();
if (t.charCodeAt(0) === 0xfeff) t = t.slice(1);
t = t.replace(/[\u200B-\u200D\uFEFF]/g, '');
return t.trim();
}
function applyBareProcessEmitWarning() {
const proc = globalThis.process;
if (!proc || typeof proc.emitWarning === 'function') return;
proc.emitWarning = function emitWarning(warning, type, code) {
const msg = warning instanceof Error ? warning.message : String(warning);
const name = typeof type === 'string' ? type : 'Warning';
const id = typeof code === 'string' ? code : '';
const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg;
try {
if (typeof proc.emit === 'function') proc.emit('warning', warning);
} catch (_) {}
try {
console.error(line);
} catch (_) {}
};
}
function applyBareTlsCompat() {
const proc = globalThis.process;
if (!proc || !proc.env) return;
if (!proc.env.NODE_TLS_REJECT_UNAUTHORIZED) {
proc.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
try {
const req = globalThis.require;
if (typeof req !== 'function') return;
const bareHttps = req('bare-https');
if (!bareHttps || typeof bareHttps.Agent !== 'function') return;
const insecureAgent = new bareHttps.Agent({ rejectUnauthorized: false });
bareHttps.globalAgent = insecureAgent;
if (bareHttps.Agent) bareHttps.Agent.global = insecureAgent;
} catch (_) {}
}
function applyAdapters() {
applyBareProcessEmitWarning();
if (bareRuntime()) applyBareTlsCompat();
try {
const ws = require('../vendor/bare-discord-js/src/adapters/whatwg-ws.cjs');
if (typeof ws.installBareOsDiscordGatewayWs === 'function') {
ws.installBareOsDiscordGatewayWs();
}
} catch (_) {}
try {
const fd = require('../vendor/bare-discord-js/src/adapters/form-data.cjs');
if (typeof fd.installBareOsFormDataGlobal === 'function') {
fd.installBareOsFormDataGlobal();
}
} catch (_) {}
}
function loadBareDiscordJsSync() {
applyAdapters();
if (discordJsTried) return discordJsCache;
discordJsTried = true;
discordJsLastError = '';
let lastErr = null;
try {
const packed = unwrapDiscordModule(takePackedDiscordJs());
if (packed) {
discordJsCache = packed;
return discordJsCache;
}
} catch (err) {
lastErr = err;
}
try {
const mod = require('bare-discord-js');
const discord = unwrapDiscordModule(mod);
if (discord) {
discordJsCache = discord;
return discordJsCache;
}
if (!lastErr) lastErr = new Error('require("bare-discord-js") returned no Client');
} catch (err) {
lastErr = err;
}
noteDiscordLoadError(lastErr || 'vendored bare-discord-js unresolved');
try {
if (process.stderr) {
process.stderr.write(
'[bridge-swarm-host] BridgeSwarm.DiscordJS: failed to load bare-discord-js: ' +
discordJsLastError +
'\n'
);
}
} catch (_) {}
discordJsCache = undefined;
return undefined;
}
module.exports = {
loadBareDiscordJsSync,
getBareDiscordJsLoadError,
normalizeDiscordToken,
unwrapDiscordModule,
};
+34
View File
@@ -0,0 +1,34 @@
/**
* Standalone pack binding. Static import so bare-pack rewrites discord.js
* into the host bundle. Adapters in ws-bootstrap.cjs must load first.
* Avoid Node `module` / createRequire those are missing in the packed graph.
*/
import discord from 'discord.js';
import registry from './registry.js';
const registerPackedDiscordJs =
(registry && registry.registerPackedDiscordJs) ||
(registry && registry.default && registry.default.registerPackedDiscordJs);
function unwrap(mod) {
if (mod && typeof mod.Client === 'function') return mod;
const def = mod && mod.default;
if (def && typeof def === 'object' && typeof def.Client === 'function') return def;
return null;
}
function applyShims(d) {
if (!d || typeof d !== 'object') return d;
const events = d.Events;
if (events && events.ClientReady && events.Ready == null) {
events.Ready = events.ClientReady;
}
return d;
}
if (typeof registerPackedDiscordJs !== 'function') {
throw new Error('discord registry missing registerPackedDiscordJs');
}
export const bareDiscordJs = registerPackedDiscordJs(applyShims(unwrap(discord)));
export default bareDiscordJs;
+29
View File
@@ -0,0 +1,29 @@
/**
* Live binding filled by packed.js when that file is statically imported
* so bare-pack rewrites discord.js into the host bundle.
*/
const registry = { module: null };
function registerPackedDiscordJs(mod) {
const unwrapped = unwrapDiscordModule(mod);
registry.module = unwrapped || null;
return registry.module;
}
function takePackedDiscordJs() {
return registry.module;
}
function unwrapDiscordModule(mod) {
if (!mod || typeof mod !== 'object') return undefined;
if (typeof mod.Client === 'function') return mod;
const def = mod.default;
if (def && typeof def === 'object' && typeof def.Client === 'function') return def;
return undefined;
}
module.exports = {
registerPackedDiscordJs,
takePackedDiscordJs,
unwrapDiscordModule,
};
+226
View File
@@ -0,0 +1,226 @@
/**
* Serialize discord.js structures for native messaging (JSON, ~1 MB cap).
*/
'use strict';
const PREDICATES = [
'isChatInputCommand',
'isButton',
'isRepliable',
'isStringSelectMenu',
'isUserSelectMenu',
'isRoleSelectMenu',
'isMentionableSelectMenu',
'isChannelSelectMenu',
'isAnySelectMenu',
'isModalSubmit',
'isAutocomplete',
'isContextMenuCommand',
'isUserContextMenuCommand',
'isMessageContextMenuCommand',
'isMessageComponent',
'isCommand',
];
const STUB_METHODS = [
'reply',
'followUp',
'editReply',
'deferReply',
'deferUpdate',
'deleteReply',
'fetchReply',
'showModal',
'update',
'react',
'delete',
'edit',
'send',
'startThread',
'pin',
'unpin',
'fetch',
'setToken',
'put',
'post',
'get',
'patch',
'login',
'destroy',
];
function intern(handles, obj) {
if (!obj || typeof obj !== 'object') return null;
if (obj.__bsHandle && handles.byId.has(obj.__bsHandle)) return obj.__bsHandle;
const id = handles.nextId++;
const handle = 'dj_' + id;
obj.__bsHandle = handle;
handles.byId.set(handle, obj);
handles.meta.set(handle, {
type: (obj.constructor && obj.constructor.name) || 'Object',
});
return handle;
}
function jsonSafe(value, depth) {
if (depth > 5) return undefined;
if (value == null) return value;
const t = typeof value;
if (t === 'string' || t === 'number' || t === 'boolean') return value;
if (t === 'bigint') return value.toString();
if (t === 'function') return undefined;
if (Array.isArray(value)) {
const out = [];
for (let i = 0; i < value.length && i < 50; i++) {
const item = jsonSafe(value[i], depth + 1);
if (item !== undefined) out.push(item);
}
return out;
}
if (t !== 'object') return String(value);
if (typeof value.toJSON === 'function') {
try {
return jsonSafe(value.toJSON(), depth + 1);
} catch (_) {}
}
const out = {};
let n = 0;
for (const key of Object.keys(value)) {
if (n++ > 80) break;
if (key.charAt(0) === '_') continue;
const v = jsonSafe(value[key], depth + 1);
if (v !== undefined) out[key] = v;
}
return out;
}
function snapshotValue(value, handles, depth) {
if (depth > 4) return jsonSafe(value, 0);
if (value == null || typeof value !== 'object') return jsonSafe(value, 0);
if (typeof value.then === 'function') return { _type: 'Promise' };
const ctor = value.constructor && value.constructor.name;
const looksLive =
typeof value.reply === 'function' ||
typeof value.login === 'function' ||
typeof value.setToken === 'function' ||
typeof value.toJSON === 'function' ||
(ctor && ctor !== 'Object' && ctor !== 'Array');
if (!looksLive) return jsonSafe(value, 0);
const handle = intern(handles, value);
let json = {};
if (typeof value.toJSON === 'function') {
try {
json = jsonSafe(value.toJSON(), 0) || {};
} catch (_) {
json = {};
}
} else {
json = jsonSafe(value, 1) || {};
}
const pred = {};
for (const name of PREDICATES) {
if (typeof value[name] === 'function') {
try {
pred[name] = !!value[name]();
} catch (_) {}
}
}
const methods = [];
for (const name of STUB_METHODS) {
if (typeof value[name] === 'function') methods.push(name);
}
const snap = Object.assign({ _handle: handle, _type: ctor || 'Object', _methods: methods }, json, pred);
if (value.user && typeof value.user === 'object') {
snap.user = jsonSafe(value.user, 0);
if (value.user.tag) snap.user.tag = value.user.tag;
if (value.user.id) snap.user.id = value.user.id;
}
if (value.author && typeof value.author === 'object') {
snap.author = jsonSafe(value.author, 0);
}
if (value.channel && typeof value.channel === 'object') {
snap.channel = {
id: value.channel.id,
type: value.channel.type,
name: value.channel.name,
_handle: intern(handles, value.channel),
_type: (value.channel.constructor && value.channel.constructor.name) || 'Channel',
_methods: typeof value.channel.send === 'function' ? ['send'] : [],
};
}
if (value.guild && typeof value.guild === 'object') {
snap.guild = {
id: value.guild.id,
name: value.guild.name,
_handle: intern(handles, value.guild),
_type: 'Guild',
};
}
if (value.commandName != null) snap.commandName = value.commandName;
if (value.customId != null) snap.customId = value.customId;
if (value.content != null) snap.content = value.content;
return snap;
}
function snapshotEventArgs(args, handles) {
const out = [];
for (let i = 0; i < args.length && i < 6; i++) {
const a = args[i];
if (a instanceof Error) {
out.push({ _type: 'Error', message: a.message, code: a.code });
} else {
out.push(snapshotValue(a, handles, 0));
}
}
return out;
}
function serializeConstants(discord) {
const out = { _classes: [] };
if (!discord || typeof discord !== 'object') return out;
for (const key of Object.keys(discord)) {
const v = discord[key];
if (typeof v === 'function') {
out._classes.push(key);
continue;
}
if (v && typeof v === 'object') {
const plain = {};
let n = 0;
for (const k of Object.keys(v)) {
if (n++ > 200) break;
const x = v[k];
if (typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean') {
plain[k] = x;
}
}
if (Object.keys(plain).length) out[key] = plain;
} else if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
out[key] = v;
}
}
return out;
}
function createHandleTable() {
return { nextId: 1, byId: new Map(), meta: new Map() };
}
module.exports = {
PREDICATES,
STUB_METHODS,
intern,
snapshotValue,
snapshotEventArgs,
serializeConstants,
createHandleTable,
jsonSafe,
};
@@ -0,0 +1,8 @@
'use strict';
/**
* Optional native. discord.js does `zlib = require('zlib-sync')` then
* `compression: zlib ? ZlibStream : null`. A truthy empty object enables
* zlib-stream, after which `new zlib.Inflate()` throws and Client.login()
* hangs forever.
*/
module.exports = null;
@@ -0,0 +1,7 @@
{
"name": "zlib-sync",
"version": "0.0.0",
"description": "Null stub: a truthy empty zlib-sync hangs discord.js Client.login forever",
"main": "./index.js",
"private": true
}
+31
View File
@@ -0,0 +1,31 @@
/**
* MUST load before discord.js / @discordjs/ws evaluation.
* Forces WHATWG WebSocket (bare-ws), FormData/Blob/File, and TLS defaults
* so IDENTIFY produces READY. CJS so bare-pack does not need Node `module`.
*/
'use strict';
function apply() {
try {
const ws = require('../vendor/bare-discord-js/src/adapters/whatwg-ws.cjs');
const install =
(ws && ws.installBareOsDiscordGatewayWs) ||
(ws && ws.default && ws.default.installBareOsDiscordGatewayWs);
if (typeof install === 'function') install();
} catch (_) {}
try {
const fd = require('../vendor/bare-discord-js/src/adapters/form-data.cjs');
const installFd =
(fd && fd.installBareOsFormDataGlobal) ||
(fd && fd.default && fd.default.installBareOsFormDataGlobal);
if (typeof installFd === 'function') installFd();
} catch (_) {}
try {
const proc = globalThis.process;
if (proc && proc.env && !proc.env.NODE_TLS_REJECT_UNAUTHORIZED) {
proc.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
} catch (_) {}
}
apply();
+1
View File
@@ -1735,6 +1735,7 @@ async function handleMessageAsync(send, msg) {
packs: capabilities.listPackIds(), packs: capabilities.listPackIds(),
qvac: packStatus('qvac'), qvac: packStatus('qvac'),
agent: packStatus('agent'), agent: packStatus('agent'),
discord: packStatus('discord'),
examples: examplesServer.status(), examples: examplesServer.status(),
streamPort: streamPort.status(), streamPort: streamPort.status(),
swarmCount: swarms.size, swarmCount: swarms.size,
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* BridgeSwarm native messaging host entrypoint. * BridgeSwarm native messaging host entrypoint.
* *
* Includes default capability packs (media, fs, sqlite, net) and curated Bare * Includes default capability packs (media, fs, sqlite, net, qvac, agent, discord) and curated Bare
* modules see docs/DEFAULT-MODULES.md. * modules see docs/DEFAULT-MODULES.md.
* *
* bare-process/global must be the very first import so that `process` is * bare-process/global must be the very first import so that `process` is
+835 -7
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -40,6 +40,17 @@
"bare-os": "^3.9.3", "bare-os": "^3.9.3",
"bare-subprocess": "^6.1.0", "bare-subprocess": "^6.1.0",
"bare-gpu-info": "0.1.1", "bare-gpu-info": "0.1.1",
"bare-ws": "^3.0.0",
"bare-tls": "^3.1.8",
"bare-https": "^3.0.0",
"bare-net": "^2.3.3",
"bare-crypto": "^1.15.3",
"bare-zlib": "^1.4.1",
"bare-form-data": "^1.2.2",
"bare-node-runtime": "^1.5.0",
"bare-discord-js": "file:./vendor/bare-discord-js",
"discord.js": "^14.27.0",
"zlib-sync": "file:./discord/stubs/zlib-sync",
"@qvac/inference": "^0.17.1", "@qvac/inference": "^0.17.1",
"@qvac/llm-llamacpp": "^0.44.0", "@qvac/llm-llamacpp": "^0.44.0",
"@qvac/embed-llamacpp": "^0.33.0", "@qvac/embed-llamacpp": "^0.33.0",
@@ -49,5 +60,8 @@
}, },
"engines": { "engines": {
"bare": ">=1.30.3" "bare": ">=1.30.3"
},
"overrides": {
"ws": "^8.21.3"
} }
} }
+30
View File
@@ -3,6 +3,7 @@
* Static imports so bare-pack always includes media / fs / sqlite / net in the graph. * Static imports so bare-pack always includes media / fs / sqlite / net in the graph.
*/ */
import './discord/ws-bootstrap.cjs';
import * as bareMedia from 'bare-media'; import * as bareMedia from 'bare-media';
import * as bareFfmpeg from 'bare-ffmpeg'; import * as bareFfmpeg from 'bare-ffmpeg';
import registry from './capabilities/registry.js'; import registry from './capabilities/registry.js';
@@ -12,6 +13,7 @@ import sqliteMod from './capabilities/sqlite.js';
import netMod from './capabilities/net.js'; import netMod from './capabilities/net.js';
import qvacMod from './capabilities/qvac.js'; import qvacMod from './capabilities/qvac.js';
import agentMod from './capabilities/agent.js'; import agentMod from './capabilities/agent.js';
import discordCapMod from './capabilities/discord.js';
import { logErr } from './boot.mjs'; import { logErr } from './boot.mjs';
function loadPack(pack, label) { function loadPack(pack, label) {
@@ -53,6 +55,13 @@ export function registerDefaultCapabilityPacks() {
logErr('qvac pack skipped: ' + (err && err.message)); logErr('qvac pack skipped: ' + (err && err.message));
} }
try {
loadPack(discordCapMod.createDiscordPack(), 'discord (bare-discord-js)');
ids.push('discord');
} catch (err) {
logErr('discord pack skipped: ' + (err && err.message));
}
try { try {
loadPack(agentMod.createAgentPack(), 'agent (grok-class harness)'); loadPack(agentMod.createAgentPack(), 'agent (grok-class harness)');
ids.push('agent'); ids.push('agent');
@@ -99,4 +108,25 @@ export async function warmDefaultModules() {
try { try {
await import('bare-gpu-info'); await import('bare-gpu-info');
} catch (_) {} } catch (_) {}
try {
await import('bare-ws');
} catch (_) {}
try {
await import('bare-tls');
} catch (_) {}
try {
await import('bare-https');
} catch (_) {}
try {
await import('bare-form-data');
} catch (_) {}
try {
await import('bare-crypto');
} catch (_) {}
try {
await import('bare-zlib');
} catch (_) {}
try {
await import('discord.js');
} catch (_) {}
} }
+61
View File
@@ -44,8 +44,15 @@ function testPackRegister() {
ok(typeof a.commands.prompt === 'function', 'agent.prompt'); ok(typeof a.commands.prompt === 'function', 'agent.prompt');
registry.registerPack(q); registry.registerPack(q);
registry.registerPack(a); registry.registerPack(a);
const discord = require('./capabilities/discord.js');
const d = discord.createDiscordPack();
ok(d.id === 'discord', 'discord id');
ok(typeof d.commands.surface === 'function', 'discord.surface');
ok(typeof d.commands.construct === 'function', 'discord.construct');
registry.registerPack(d);
ok(registry.hasPack('qvac'), 'has qvac'); ok(registry.hasPack('qvac'), 'has qvac');
ok(registry.hasPack('agent'), 'has agent'); ok(registry.hasPack('agent'), 'has agent');
ok(registry.hasPack('discord'), 'has discord');
const engine = require('./qvac/engine.js'); const engine = require('./qvac/engine.js');
ok(engine.publicStatus().enabled === false, 'qvac off by default'); ok(engine.publicStatus().enabled === false, 'qvac off by default');
ok(engine.publicStatus().available === false, 'unavailable when disabled'); ok(engine.publicStatus().available === false, 'unavailable when disabled');
@@ -143,6 +150,7 @@ testAgentLoopFakeComplete()
.then(() => testGrepGlobLoop()) .then(() => testGrepGlobLoop())
.then(() => testBareVersionsCoerce()) .then(() => testBareVersionsCoerce())
.then(() => testQvacOffByDefault()) .then(() => testQvacOffByDefault())
.then(() => testDiscordPack())
.then(() => { .then(() => {
console.log('ok — bare host checks passed'); console.log('ok — bare host checks passed');
}) })
@@ -208,6 +216,59 @@ async function testQvacOffByDefault() {
ok(st && st.available === false, 'status available false'); ok(st && st.available === false, 'status available false');
} }
async function testDiscordPack() {
const discord = require('./capabilities/discord.js');
const pack = discord.createDiscordPack();
let denied = null;
await pack.commands.construct({
payload: { className: 'Client', args: [{ intents: [] }] },
reply(r) { denied = r; },
emit() {},
});
ok(denied && denied.ok === false && /disabled/i.test(denied.error), 'construct refused when disabled');
let en = null;
await pack.commands.setEnabled({
payload: { enabled: true },
reply(r) { en = r; },
emit() {},
});
ok(en && en.ok && en.enabled === true, 'setEnabled on');
let surface = null;
await pack.commands.surface({
payload: {},
reply(r) { surface = r; },
emit() {},
});
ok(surface && surface.ok, 'surface ok: ' + (surface && surface.error));
ok(surface.surface && surface.surface.GatewayIntentBits, 'GatewayIntentBits present');
ok(surface.surface._classes && surface.surface._classes.indexOf('Client') >= 0, 'Client listed');
const guilds = surface.surface.GatewayIntentBits.Guilds;
let constructed = null;
await pack.commands.construct({
payload: { className: 'Client', args: [{ intents: [guilds] }] },
reply(r) { constructed = r; },
emit() {},
});
ok(constructed && constructed.ok && constructed.handle, 'construct Client');
let destroyed = null;
await pack.commands.destroy({
payload: { handle: constructed.handle },
reply(r) { destroyed = r; },
emit() {},
});
ok(destroyed && destroyed.ok, 'destroy Client');
await pack.commands.setEnabled({
payload: { enabled: false },
reply() {},
emit() {},
});
}
async function testCustomToolLoop() { async function testCustomToolLoop() {
const tmp = tmpDir('bs-custom-'); const tmp = tmpDir('bs-custom-');
process.env.BRIDGE_SWARM_STORAGE = tmp; process.env.BRIDGE_SWARM_STORAGE = tmp;
+37
View File
@@ -0,0 +1,37 @@
# bare-discord-js Production Implementation Plan
This file mirrors the approved execution plan and is tracked in-repo as the implementation contract.
## Goal
- Provide `require('bare-discord-js')` and `import ... from 'bare-discord-js'` parity with official `discord.js` 14.27+ inside Bare.
- Ensure end users install only one package and do not configure aliases/import maps manually.
- Gate releases on strict parity verification.
## Architecture
- Internal bootstrap loads `bare-node-runtime/global` and import mappings automatically.
- Dual entrypoints (`src/index.js` [CJS], `src/index.mjs` [ESM]) re-export official `discord.js`.
- Compatibility adapters isolate runtime differences (WebSocket, TLS/HTTPS, zlib, crypto).
- Patch manifest and automation scripts track upstream drift and compatibility work.
## Delivery Phases
1. Baseline inventory scripts for dependency and builtin mapping.
2. Bootstrap loader and import-map encapsulation.
3. Adapter and patch infrastructure.
4. Build/packaging workflow (npm + optional bare-pack).
5. Parity test harness (Node vs Bare behavior checks).
6. Release verification and upstream sync automation.
## Repro Command Set
```sh
npm install
npm run analyze
npm run verify-bootstrap-load
npm run generate-patches
npm run apply-patches
npm run test:parity
npm run release:verify
```
## Strict Parity Policy
- Any observed runtime incompatibility blocks GA.
- Voice/gateway/rest paths are all required unless explicitly re-scoped.
+59
View File
@@ -0,0 +1,59 @@
# bare-discord-js
Run official `discord.js` **14.27** inside Bare with one dependency and one import.
## Status
- Targets official `discord.js@^14.27.0` on Bare `>=1.29.4`.
- Bootstrap remaps Node builtins through `[email protected]` (`bare-fetch`, `bare-ws`, `bare-sqlite`, …).
- REST uses the `@discordjs/rest` web/fetch build; gateway uses `ws` over Bare TLS.
## Usage
### CommonJS
```js
const { Client, GatewayIntentBits, Events } = require('bare-discord-js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, (readyClient) => {
console.log(`Logged in as ${readyClient.user.tag}`);
});
```
### ESM
```js
import { Client, GatewayIntentBits, Events } from 'bare-discord-js';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, (readyClient) => {
console.log(`Logged in as ${readyClient.user.tag}`);
});
```
Use `Events.ClientReady` (`clientReady`). discord.js 14.27 still emits `ready` with a deprecation warning; `Events.Ready` is aliased to `clientReady` so it stays current.
## Scripts
- `npm run patch:runtime-deps` - applies reproducible runtime compatibility patches to installed dependencies.
- `npm run sync:imports-map` - copies `bare-node-runtime/imports` into the shipped Node→Bare map.
- `npm run analyze` - generates runtime/dependency inventories.
- `npm run verify-bootstrap-load` - validates CJS/ESM bootstrap loading.
- `npm run generate-patches` - writes patch manifest placeholders.
- `npm run apply-patches` - validates patch manifest integrity.
- `npm run test:parity` - verifies baseline API parity surface.
- `npm run test:bare` - loads the library under the `bare` runtime.
- `npm run release:verify` - checks required release artifacts.
## Example Bot
- `bare examples/basic-bot.cjs` - runs a minimal gateway login bot using `.env` (`DISCORD_TOKEN=...`).
## Runtime Patch Config
- Runtime dependency patch rules are versioned in `patches/runtime-dependency-patches.json`.
- `scripts/patch-runtime-deps.mjs` applies those rules after install (`postinstall`) and in CI.
## Project Layout
- `src/bootstrap` - runtime import-map/global bootstrap.
- `src/adapters` - compatibility adapters for runtime deltas.
- `scripts` - analysis, patching, sync, release automation.
- `test` - parity and smoke harness.
- `patches` - patch metadata and fallback import mappings.
## Roadmap
1. Add real gateway/REST/voice conformance integration runs under Bare.
2. Automate upstream v14 sync with patch rebase + report generation.
+85
View File
@@ -0,0 +1,85 @@
{
"name": "bare-discord-js",
"version": "0.2.0",
"description": "Run official discord.js on Bare with zero end-user extra modules.",
"license": "MIT",
"type": "commonjs",
"main": "./src/index.js",
"module": "./src/index.mjs",
"exports": {
".": {
"bare": "./src/index.js",
"require": "./src/index.js",
"import": "./src/index.mjs"
}
},
"files": [
"src",
"scripts",
"patches",
"README.md",
"PLAN.md"
],
"engines": {
"node": ">=20",
"bare": ">=1.29.4"
},
"scripts": {
"postinstall": "node scripts/apply-workspace-patches.mjs",
"patch:runtime-deps": "node scripts/apply-workspace-patches.mjs",
"sync:imports-map": "node scripts/sync-imports-map.mjs",
"analyze": "node scripts/analyze-discordjs-surface.mjs --out artifacts/analysis && node scripts/analyze-bare-surface.mjs --out artifacts/analysis",
"verify-bootstrap-load": "node scripts/verify-bootstrap-load.mjs",
"pear:prepare": "node scripts/pear-prepare-release.mjs",
"generate-patches": "node scripts/generate-patches.mjs --out patches",
"apply-patches": "node scripts/apply-patches.mjs --verify",
"build": "node scripts/build.mjs",
"pack:bare": "node scripts/pack-bare.mjs",
"test:parity": "node test/parity.test.mjs",
"test:bot:smoke": "node test/smoke-bot.test.mjs",
"test:bot:voice": "node test/voice-bot.test.mjs",
"test:bare": "bare test/bare-load.test.cjs",
"test:bare:rest": "bare test/bare-network.test.cjs",
"test:bare:ws": "bare test/bare-ws.test.cjs",
"sync:upstream": "node scripts/sync-upstream.mjs",
"sync:rebase-patches": "node scripts/sync-rebase-patches.mjs",
"release:verify": "node scripts/release-verify.mjs",
"ci:full": "npm run patch:runtime-deps && npm run sync:imports-map && npm run analyze && npm run verify-bootstrap-load && npm run generate-patches && npm run apply-patches && npm run test:parity && npm run test:bot:smoke && npm run release:verify"
},
"dependencies": {
"bare-assert": "^1.2.0",
"bare-buffer": "^3.7.0",
"bare-console": "^6.2.0",
"bare-crypto": "^1.15.3",
"bare-diagnostics-channel": "^1.1.0",
"bare-encoding": "^1.0.3",
"bare-events": "^2.9.1",
"bare-fetch": "^3.2.0",
"bare-form-data": "^1.2.2",
"bare-fs": "^4.8.0",
"bare-http1": "^4.5.7",
"bare-https": "^3.0.0",
"bare-module": "^6.4.0",
"bare-net": "^2.3.3",
"bare-node-runtime": "^1.5.0",
"bare-os": "^3.9.3",
"bare-pack": "^2.2.1",
"bare-path": "^3.1.1",
"bare-performance": "^2.1.1",
"bare-process": "^4.5.1",
"bare-querystring": "^1.1.0",
"bare-stream": "^2.13.3",
"bare-string-decoder": "^1.0.0",
"bare-timers": "^3.2.1",
"bare-tls": "^3.1.8",
"bare-url": "^2.5.2",
"bare-utils": "^1.6.0",
"bare-ws": "^3.0.0",
"bare-zlib": "^1.4.1",
"discord.js": "^14.27.0"
},
"overrides": {
"ws": "^8.21.3"
},
"private": true
}
@@ -0,0 +1,466 @@
{
"assert": {
"bare": "bare-assert",
"default": "assert"
},
"node:assert": {
"bare": "bare-assert",
"default": "assert"
},
"assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"node:assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"async_hooks": {
"bare": "bare-async-hooks",
"default": "async_hooks"
},
"node:async_hooks": {
"bare": "bare-async-hooks",
"default": "async_hooks"
},
"buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"node:buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"node:child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"node:cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"console": {
"bare": "bare-console",
"default": "console"
},
"node:console": {
"bare": "bare-console",
"default": "console"
},
"constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"node:constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"node:crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"node:dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"diagnostics_channel": {
"bare": "bare-diagnostics-channel",
"default": "diagnostics_channel"
},
"node:diagnostics_channel": {
"bare": "bare-diagnostics-channel",
"default": "diagnostics_channel"
},
"dns": {
"bare": "bare-dns",
"default": "dns"
},
"node:dns": {
"bare": "bare-dns",
"default": "dns"
},
"dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"node:dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"node:domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"events": {
"bare": "bare-events",
"default": "events"
},
"node:events": {
"bare": "bare-events",
"default": "events"
},
"fs": {
"bare": "bare-fs",
"default": "fs"
},
"node:fs": {
"bare": "bare-fs",
"default": "fs"
},
"fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"node:fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"http": {
"bare": "bare-http1",
"default": "http"
},
"node:http": {
"bare": "bare-http1",
"default": "http"
},
"http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"node:http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"https": {
"bare": "bare-https",
"default": "https"
},
"node:https": {
"bare": "bare-https",
"default": "https"
},
"inspector": {
"bare": "bare-inspector",
"default": "inspector"
},
"node:inspector": {
"bare": "bare-inspector",
"default": "inspector"
},
"inspector/promises": {
"bare": "bare-inspector/promises",
"default": "inspector/promises"
},
"node:inspector/promises": {
"bare": "bare-inspector/promises",
"default": "inspector/promises"
},
"module": {
"bare": "bare-module",
"default": "module"
},
"node:module": {
"bare": "bare-module",
"default": "module"
},
"net": {
"bare": "bare-net",
"default": "net"
},
"node:net": {
"bare": "bare-net",
"default": "net"
},
"os": {
"bare": "bare-os",
"default": "os"
},
"node:os": {
"bare": "bare-os",
"default": "os"
},
"path": {
"bare": "bare-path",
"default": "path"
},
"node:path": {
"bare": "bare-path",
"default": "path"
},
"path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"node:path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"node:path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"node:perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"process": {
"bare": "bare-process",
"default": "process"
},
"node:process": {
"bare": "bare-process",
"default": "process"
},
"punycode": {
"bare": "bare-punycode",
"default": "punycode"
},
"node:punycode": {
"bare": "bare-punycode",
"default": "punycode"
},
"querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"node:querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"readline": {
"bare": "bare-readline",
"default": "readline"
},
"node:readline": {
"bare": "bare-readline",
"default": "readline"
},
"readline/promises": {
"bare": "bare-readline/promises",
"default": "readline/promises"
},
"node:readline/promises": {
"bare": "bare-readline/promises",
"default": "readline/promises"
},
"repl": {
"bare": "bare-repl",
"default": "repl"
},
"node:repl": {
"bare": "bare-repl",
"default": "repl"
},
"sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"node:sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"sqlite": {
"bare": "bare-sqlite",
"default": "sqlite"
},
"node:sqlite": {
"bare": "bare-sqlite",
"default": "sqlite"
},
"stream": {
"bare": "bare-stream",
"default": "stream"
},
"node:stream": {
"bare": "bare-stream",
"default": "stream"
},
"stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"node:stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"node:stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"node:stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"node:string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"node:sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"node:test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"node:test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"timers": {
"bare": "bare-timers",
"default": "timers"
},
"node:timers": {
"bare": "bare-timers",
"default": "timers"
},
"timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"node:timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"tls": {
"bare": "bare-tls",
"default": "tls"
},
"node:tls": {
"bare": "bare-tls",
"default": "tls"
},
"trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"node:trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"tty": {
"bare": "bare-tty",
"default": "tty"
},
"node:tty": {
"bare": "bare-tty",
"default": "tty"
},
"url": {
"bare": "bare-url",
"default": "url"
},
"node:url": {
"bare": "bare-url",
"default": "url"
},
"util": {
"bare": "bare-utils",
"default": "util"
},
"node:util": {
"bare": "bare-utils",
"default": "util"
},
"util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"node:util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"v8": {
"bare": "bare-v8",
"default": "v8"
},
"node:v8": {
"bare": "bare-v8",
"default": "v8"
},
"vm": {
"bare": "bare-vm",
"default": "vm"
},
"node:vm": {
"bare": "bare-vm",
"default": "vm"
},
"wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"node:wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"node:worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"zlib": {
"bare": "bare-zlib",
"default": "zlib"
},
"node:zlib": {
"bare": "bare-zlib",
"default": "zlib"
}
}
@@ -0,0 +1,18 @@
{
"generatedAt": "2026-08-13T05:22:18.617Z",
"discordRoot": "/Volumes/storage/dev/bare-discord-js/node_modules/discord.js",
"patches": [
{
"id": "runtime-loader-hook",
"description": "Placeholder patch for injected runtime websocket/fetch adapters.",
"target": "packages/ws/src/ws/WebSocketShard.ts",
"strategy": "future-overlay"
},
{
"id": "rest-transport-bridge",
"description": "Placeholder patch for undici/Bare transport bridge if needed.",
"target": "packages/rest/src/index.ts",
"strategy": "future-overlay"
}
]
}
@@ -0,0 +1,73 @@
{
"bareEngineMinimum": ">=1.24.0",
"bareEngineRelaxTargets": [
"node_modules/bare-performance/package.json"
],
"engineRangeTargets": [
"node_modules/@vladfrangu/async_event_emitter/package.json",
"node_modules/@sapphire/async-queue/package.json",
"node_modules/@sapphire/snowflake/package.json",
"node_modules/@discordjs/rest/node_modules/@sapphire/snowflake/package.json",
"node_modules/@sapphire/shapeshift/package.json"
],
"replaceRules": [
{
"file": "node_modules/undici/lib/dispatcher/client.js",
"findRegex": "const connectH2 = require\\(['\\\"]\\./client-h2\\.js['\\\"]\\)",
"replaceWith": "let connectH2 = null\\ntry {\\n connectH2 = require('./client-h2.js')\\n} catch {\\n connectH2 = null\\n}"
},
{
"file": "node_modules/discord.js/src/util/Util.js",
"findRegex": "const \\{ fetch \\} = require\\(['\\\"]undici['\\\"]\\)",
"replaceWith": "const fetch = globalThis.fetch"
},
{
"file": "node_modules/discord.js/src/util/DataResolver.js",
"findRegex": "const \\{ fetch \\} = require\\(['\\\"]undici['\\\"]\\)",
"replaceWith": "const fetch = globalThis.fetch"
},
{
"file": "node_modules/undici/lib/dispatcher/client.js",
"findRegex": "client\\[kHTTPContext\\] = socket\\.alpnProtocol === 'h2'\\n\\s*\\? await connectH2\\(client, socket\\)\\n\\s*:\\s*await connectH1\\(client, socket\\)",
"replaceWith": "client[kHTTPContext] = socket.alpnProtocol === 'h2'\\n ? await (connectH2 ? connectH2(client, socket) : connectH1(client, socket))\\n : await connectH1(client, socket)"
},
{
"file": "node_modules/@discordjs/rest/package.json",
"findRegex": "\"node\":\\s*\\{\\s*\"require\":\\s*\\{\\s*\"types\":\\s*\"\\.\\/dist\\/index\\.d\\.ts\",\\s*\"default\":\\s*\"\\.\\/dist\\/index\\.js\"\\s*\\},\\s*\"import\":\\s*\\{\\s*\"types\":\\s*\"\\.\\/dist\\/index\\.d\\.mts\",\\s*\"default\":\\s*\"\\.\\/dist\\/index\\.mjs\"\\s*\\}\\s*\\}",
"flags": "m",
"replaceWith": "\"node\": {\\n \"require\": {\\n \"types\": \"./dist/web.d.ts\",\\n \"default\": \"./dist/web.js\"\\n },\\n \"import\": {\\n \"types\": \"./dist/web.d.mts\",\\n \"default\": \"./dist/web.mjs\"\\n }\\n }"
},
{
"file": "node_modules/@discordjs/ws/dist/index.js",
"findRegex": "const connection = new WebSocketConstructor\\(url, \\[\\], \\{\\s*handshakeTimeout: this\\.strategy\\.options\\.handshakeTimeout \\?\\? void 0(?:,\\s*rejectUnauthorized: false,\\s*perMessageDeflate: false,\\s*skipUTF8Validation: true)?\\s*\\}\\);",
"flags": "m",
"replaceWith": "const connection = new WebSocketConstructor(url, [], {\\n handshakeTimeout: this.strategy.options.handshakeTimeout ?? void 0,\\n rejectUnauthorized: false,\\n perMessageDeflate: false,\\n skipUTF8Validation: true\\n });"
},
{
"file": "node_modules/@discordjs/ws/dist/index.mjs",
"findRegex": "const connection = new WebSocketConstructor\\(url, \\[\\], \\{\\s*handshakeTimeout: this\\.strategy\\.options\\.handshakeTimeout \\?\\? void 0(?:,\\s*rejectUnauthorized: false,\\s*perMessageDeflate: false,\\s*skipUTF8Validation: true)?\\s*\\}\\);",
"flags": "m",
"replaceWith": "const connection = new WebSocketConstructor(url, [], {\\n handshakeTimeout: this.strategy.options.handshakeTimeout ?? void 0,\\n rejectUnauthorized: false,\\n perMessageDeflate: false,\\n skipUTF8Validation: true\\n });"
},
{
"file": "node_modules/@discordjs/ws/dist/index.js",
"findRegex": "const \\{ ok \\} = await this\\.waitForEvent\\(\"hello\" /\\* Hello \\*/ , this\\.strategy\\.options\\.helloTimeout\\);\\n if \\(!ok\\) \\{\\n return;\\n \\}\\n if \\(session",
"replaceWith": "const { ok } = await this.waitForEvent(\"hello\" /* Hello */, this.strategy.options.helloTimeout);\\n if (!ok) {\\n return;\\n }\\n this.debug([\"Hello received; yielding before identify to avoid write-during-read\"]);\\n await (0, import_promises2.setTimeout)(25);\\n if (session"
},
{
"file": "node_modules/discord.js/src/client/websocket/WebSocketManager.js",
"findRegex": "try \\{\\n zlib = require\\(['\\\"]zlib-sync['\\\"]\\);\\n\\} catch \\{\\}",
"replaceWith": "try {\\n zlib = require('zlib-sync');\\n if (!zlib || typeof zlib.Inflate !== 'function') zlib = null;\\n} catch {}"
},
{
"file": "node_modules/@discordjs/ws/dist/index.js",
"findRegex": "var getZlibSync = \\(0, import_util2\\.lazy\\)\\(async \\(\\) => import\\([\"']zlib-sync[\"']\\)\\.then\\(\\(mod\\) => mod\\.default\\)\\.catch\\(\\(\\) => null\\)\\);",
"replaceWith": "var getZlibSync = (0, import_util2.lazy)(async () => import(\"zlib-sync\").then((mod) => {\\n const z = mod && (mod.default !== undefined ? mod.default : mod);\\n return z && typeof z.Inflate === \"function\" ? z : null;\\n}).catch(() => null));"
},
{
"file": "node_modules/@discordjs/ws/dist/index.mjs",
"findRegex": "var getZlibSync = lazy2\\(async \\(\\) => import\\([\"']zlib-sync[\"']\\)\\.then\\(\\(mod\\) => mod\\.default\\)\\.catch\\(\\(\\) => null\\)\\);",
"replaceWith": "const getZlibSync = lazy(async () => import(\"zlib-sync\").then((mod) => {\\n const z = mod && (mod.default !== undefined ? mod.default : mod);\\n return z && typeof z.Inflate === \"function\" ? z : null;\\n}).catch(() => null));"
}
]
}
@@ -0,0 +1,68 @@
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
const args = process.argv.slice(2);
const defaultBareRoot = [
'/Volumes/storage/dev/pearcli/holepunch-repos/holepunchto_repos',
'/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos'
].find((candidate) => existsSync(candidate));
const root = args.includes('--root') ? args[args.indexOf('--root') + 1] : defaultBareRoot;
const out = args.includes('--out') ? args[args.indexOf('--out') + 1] : path.join(process.cwd(), 'artifacts', 'analysis');
if (!root || !out) {
console.error('Usage: node scripts/analyze-bare-surface.mjs [--root <path>] [--out <path>]');
process.exit(1);
}
async function getRepoInfo(repoPath) {
const pkgPath = path.join(repoPath, 'package.json');
try {
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
return {
repoPath,
name: pkg.name || path.basename(repoPath),
version: pkg.version || null,
description: pkg.description || null,
dependencies: Object.keys(pkg.dependencies || {}),
optionalDependencies: Object.keys(pkg.optionalDependencies || {}),
peerDependencies: Object.keys(pkg.peerDependencies || {}),
exports: pkg.exports || null
};
} catch {
return null;
}
}
const entries = await fs.readdir(root, { withFileTypes: true });
const repos = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (!entry.name.startsWith('bare-') && entry.name !== 'node-bare-bundle') continue;
repos.push(path.join(root, entry.name));
}
const analyzed = (await Promise.all(repos.map(getRepoInfo))).filter(Boolean);
const grouped = {
runtime: analyzed.filter((r) => /bare-(fs|http1|https|tls|ws|crypto|tcp|zlib|stream|buffer|events|net|dgram)/.test(r.name)),
loaderAndBundling: analyzed.filter((r) => /bare-(module|pack|module-resolve|module-traverse|module-lexer)/.test(r.name)),
wrappers: analyzed.filter((r) => /bare-node/.test(r.name)),
other: analyzed.filter((r) => !/bare-(fs|http1|https|tls|ws|crypto|tcp|zlib|stream|buffer|events|net|dgram|module|pack|module-resolve|module-traverse|module-lexer)|bare-node/.test(r.name))
};
await fs.mkdir(out, { recursive: true });
await fs.writeFile(
path.join(out, 'bare-surface.json'),
JSON.stringify(
{
scannedRoot: root,
generatedAt: new Date().toISOString(),
repositories: analyzed.sort((a, b) => a.name.localeCompare(b.name)),
grouped
},
null,
2
)
);
console.log('Wrote', path.join(out, 'bare-surface.json'));
@@ -0,0 +1,103 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { builtinModules } from 'node:module';
const args = process.argv.slice(2);
const root = args.includes('--root')
? args[args.indexOf('--root') + 1]
: path.join(process.cwd(), 'node_modules', 'discord.js');
const out = args.includes('--out') ? args[args.indexOf('--out') + 1] : path.join(process.cwd(), 'artifacts', 'analysis');
if (!root || !out) {
console.error('Usage: node scripts/analyze-discordjs-surface.mjs [--root <path>] [--out <path>]');
process.exit(1);
}
const builtinSet = new Set();
const thirdPartySet = new Set();
const manifests = [];
const knownBuiltins = new Set([...builtinModules, ...builtinModules.map((m) => m.replace(/^node:/, ''))]);
const IMPORT_RE = /\b(?:import\s+[^'"]*from\s*|import\s*\(|require\()\s*['"]([^'"]+)['"]/g;
function normalizeSpecifier(specifier) {
if (specifier.startsWith('node:')) return specifier.slice(5);
return specifier;
}
function isBuiltin(specifier) {
const normalized = normalizeSpecifier(specifier);
return knownBuiltins.has(normalized);
}
function isThirdParty(specifier) {
const normalized = normalizeSpecifier(specifier);
if (normalized.startsWith('.') || normalized.startsWith('/')) return false;
const first = normalized.split('/')[0];
return !isBuiltin(first);
}
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') continue;
await walk(full);
continue;
}
if (entry.name === 'package.json') {
manifests.push(full);
continue;
}
if (!/\.(mjs|cjs|js|ts|mts|cts)$/.test(entry.name)) continue;
const content = await fs.readFile(full, 'utf8');
for (const match of content.matchAll(IMPORT_RE)) {
const specifier = match[1];
const normalized = normalizeSpecifier(specifier);
if (isBuiltin(normalized)) builtinSet.add(normalized);
if (isThirdParty(normalized)) thirdPartySet.add(normalized.split('/')[0].startsWith('@') ? normalized.split('/').slice(0, 2).join('/') : normalized.split('/')[0]);
}
}
}
async function parseManifest(manifestPath) {
try {
const json = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
return {
path: manifestPath,
name: json.name || null,
version: json.version || null,
type: json.type || 'commonjs',
engines: json.engines || {},
dependencies: Object.keys(json.dependencies || {}),
optionalDependencies: Object.keys(json.optionalDependencies || {}),
peerDependencies: Object.keys(json.peerDependencies || {}),
exports: json.exports ? true : false,
imports: json.imports ? true : false
};
} catch {
return null;
}
}
await walk(root);
const manifestDetails = (await Promise.all(manifests.map(parseManifest))).filter(Boolean);
await fs.mkdir(out, { recursive: true });
await fs.writeFile(
path.join(out, 'discordjs-surface.json'),
JSON.stringify(
{
scannedRoot: root,
generatedAt: new Date().toISOString(),
builtinModules: Array.from(builtinSet).sort(),
thirdPartyModules: Array.from(thirdPartySet).sort(),
manifests: manifestDetails
},
null,
2
)
);
console.log('Wrote', path.join(out, 'discordjs-surface.json'));
@@ -0,0 +1,16 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const manifestPath = path.join(process.cwd(), 'patches', 'patch-manifest.json');
try {
const content = await fs.readFile(manifestPath, 'utf8');
const manifest = JSON.parse(content);
if (!Array.isArray(manifest.patches)) {
throw new Error('Invalid patch manifest format.');
}
console.log(`Patch manifest verified with ${manifest.patches.length} entries.`);
} catch (error) {
console.error(`Patch verification failed: ${error.message}`);
process.exit(1);
}
@@ -0,0 +1,67 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const require = createRequire(path.join(pkgRoot, 'package.json'))
function findTargetsRoot() {
let dir = pkgRoot
for (let i = 0; i < 10; i++) {
if (
fs.existsSync(
path.join(dir, 'node_modules', 'discord.js', 'package.json')
)
) {
return dir
}
const parent = path.dirname(dir)
if (parent === dir) break
dir = parent
}
try {
const resolved = require.resolve('discord.js')
let cur = path.dirname(resolved)
for (let i = 0; i < 8; i++) {
const pkg = path.join(cur, 'package.json')
if (fs.existsSync(pkg)) {
try {
if (JSON.parse(fs.readFileSync(pkg, 'utf8')).name === 'discord.js') {
return path.dirname(path.dirname(cur))
}
} catch {
/* continue */
}
}
const parent = path.dirname(cur)
if (parent === cur) break
cur = parent
}
} catch {
/* ignore */
}
return pkgRoot
}
const targetsRoot = findTargetsRoot()
const env = {
...process.env,
BARE_DISCORD_REPO_ROOT: pkgRoot,
BARE_DISCORD_PATCH_TARGETS_ROOT: targetsRoot
}
const patch = spawnSync(
process.execPath,
[path.join(pkgRoot, 'scripts', 'patch-runtime-deps.mjs')],
{ stdio: 'inherit', env }
)
if (patch.status) process.exit(patch.status || 1)
const sync = spawnSync(
process.execPath,
[path.join(pkgRoot, 'scripts', 'sync-imports-map.mjs')],
{ stdio: 'inherit', env }
)
if (sync.status) process.exit(sync.status || 1)
+18
View File
@@ -0,0 +1,18 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const outPath = path.join(process.cwd(), 'artifacts', 'analysis', 'build-report.json');
await fs.mkdir(path.dirname(outPath), { recursive: true });
await fs.writeFile(
outPath,
JSON.stringify(
{
generatedAt: new Date().toISOString(),
status: 'ok',
outputs: ['src/index.js', 'src/index.mjs']
},
null,
2
)
);
console.log('Build pipeline report created:', outPath);
@@ -0,0 +1,36 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const args = process.argv.slice(2);
const discordRoot = args.includes('--discord-root')
? args[args.indexOf('--discord-root') + 1]
: path.join(process.cwd(), 'node_modules', 'discord.js');
const out = args.includes('--out') ? args[args.indexOf('--out') + 1] : path.join(process.cwd(), 'patches');
if (!discordRoot || !out) {
console.error('Usage: node scripts/generate-patches.mjs [--discord-root <path>] [--out <path>]');
process.exit(1);
}
const manifest = {
generatedAt: new Date().toISOString(),
discordRoot,
patches: [
{
id: 'runtime-loader-hook',
description: 'Placeholder patch for injected runtime websocket/fetch adapters.',
target: 'packages/ws/src/ws/WebSocketShard.ts',
strategy: 'future-overlay'
},
{
id: 'rest-transport-bridge',
description: 'Placeholder patch for undici/Bare transport bridge if needed.',
target: 'packages/rest/src/index.ts',
strategy: 'future-overlay'
}
]
};
await fs.mkdir(out, { recursive: true });
await fs.writeFile(path.join(out, 'patch-manifest.json'), JSON.stringify(manifest, null, 2));
console.log('Generated patch manifest at', path.join(out, 'patch-manifest.json'));
@@ -0,0 +1,18 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const outPath = path.join(process.cwd(), 'artifacts', 'analysis', 'bare-pack-report.json');
await fs.mkdir(path.dirname(outPath), { recursive: true });
await fs.writeFile(
outPath,
JSON.stringify(
{
generatedAt: new Date().toISOString(),
status: 'pending-integration',
commandHint: 'npx bare-pack src/index.js --out dist/bare-discord-js.bundle'
},
null,
2
)
);
console.log('Bare pack report created:', outPath);
@@ -0,0 +1,90 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const repoRoot = process.env.BARE_DISCORD_REPO_ROOT
? path.resolve(process.env.BARE_DISCORD_REPO_ROOT)
: process.cwd();
const targetsRoot = process.env.BARE_DISCORD_PATCH_TARGETS_ROOT
? path.resolve(process.env.BARE_DISCORD_PATCH_TARGETS_ROOT)
: repoRoot;
const configPath = path.join(repoRoot, 'patches', 'runtime-dependency-patches.json');
async function read(file) {
return fs.readFile(path.join(targetsRoot, file), 'utf8');
}
async function write(file, contents) {
await fs.writeFile(path.join(targetsRoot, file), contents);
}
async function patchText(file, transform) {
const before = await read(file);
const after = transform(before);
if (after !== before) {
await write(file, after);
return true;
}
return false;
}
function decodeEscapedReplacement(value) {
return value.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
}
function replaceAllVersionRanges(input) {
return input.replace(/>=v(\d+(?:\.\d+){0,2})/g, '>=$1');
}
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
let changed = 0;
let applied = 0;
const bareEngineMinimum = config.bareEngineMinimum ?? '>=1.24.0';
for (const file of config.bareEngineRelaxTargets ?? []) {
try {
const fullPath = path.join(targetsRoot, file);
const raw = await fs.readFile(fullPath, 'utf8');
const pkg = JSON.parse(raw);
if (!pkg.engines) pkg.engines = {};
const prev = pkg.engines.bare;
pkg.engines.bare = bareEngineMinimum;
if (prev !== pkg.engines.bare) {
await fs.writeFile(fullPath, JSON.stringify(pkg, null, 2) + '\n');
changed++;
applied++;
}
} catch {
// dependency tree may differ by version
}
}
for (const file of config.engineRangeTargets) {
try {
if (await patchText(file, replaceAllVersionRanges)) {
changed++;
applied++;
}
} catch {
// dependency tree may differ by version
}
}
for (const rule of config.replaceRules) {
try {
if (
await patchText(rule.file, (text) =>
text.replace(new RegExp(rule.findRegex, rule.flags ?? ''), decodeEscapedReplacement(rule.replaceWith))
)
) {
changed++;
applied++;
}
} catch {
// optional rule target may be absent in some versions
}
}
console.log(`Runtime patching complete. Rules applied: ${applied}, files changed: ${changed}`);
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Packs bare-discord-js into a tarball, installs examples/pear-discord-bot deps,
* applies runtime patches, writes bare-imports.json for Pear, and syncs key patched files.
*
* Run from repo root: node scripts/pear-prepare-release.mjs
*/
import { spawnSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.join(__dirname, '..');
const pearDir = path.join(repoRoot, 'examples', 'pear-discord-bot');
if (!existsSync(pearDir)) {
console.error('Missing examples/pear-discord-bot');
process.exit(1);
}
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
console.log('npm pack bare-discord-js → examples/pear-discord-bot …');
const pack = spawnSync(npmBin, ['pack', '--pack-destination', pearDir], {
cwd: repoRoot,
stdio: 'inherit',
shell: false
});
if (pack.status !== 0) process.exit(pack.status ?? 1);
console.log('npm install in examples/pear-discord-bot …');
const npm = spawnSync(npmBin, ['install'], { cwd: pearDir, stdio: 'inherit', shell: false });
if (npm.status !== 0) process.exit(npm.status ?? 1);
const libPkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
const libInstalled = path.join(pearDir, 'node_modules', 'bare-discord-js');
console.log('Reinstall bare-discord-js from fresh tarball (npm may otherwise keep a stale extract) …');
rmSync(libInstalled, { recursive: true, force: true });
const reinstallLib = spawnSync(
npmBin,
['install', `file:bare-discord-js-${libPkg.version}.tgz`, '--no-save'],
{ cwd: pearDir, stdio: 'inherit', shell: false }
);
if (reinstallLib.status !== 0) process.exit(reinstallLib.status ?? 1);
console.log('Applying runtime patches to pear app node_modules …');
const patch = spawnSync(process.execPath, ['scripts/patch-runtime-deps.mjs'], {
cwd: repoRoot,
stdio: 'inherit',
env: {
...process.env,
BARE_DISCORD_REPO_ROOT: repoRoot,
BARE_DISCORD_PATCH_TARGETS_ROOT: pearDir
}
});
if (patch.status !== 0) process.exit(patch.status ?? 1);
const bareImportsSrc = path.join(pearDir, 'node_modules', 'bare-node-runtime', 'imports.json');
const bareImportsDst = path.join(pearDir, 'bare-imports.json');
if (existsSync(bareImportsSrc)) {
cpSync(bareImportsSrc, bareImportsDst);
console.log('Wrote bare-imports.json (Pear reads import map via bare-fs, not pear:// JSON).');
}
/** Pear often omits deep node_modules trees; ship discord.js as app-owned files. */
const discordSrc = path.join(pearDir, 'node_modules', 'discord.js');
const discordVendor = path.join(pearDir, 'vendor', 'discord.js');
if (existsSync(discordSrc)) {
rmSync(discordVendor, { recursive: true, force: true });
mkdirSync(path.dirname(discordVendor), { recursive: true });
cpSync(discordSrc, discordVendor, { recursive: true });
console.log('Vendored discord.js → examples/pear-discord-bot/vendor/discord.js (Pear staging).');
}
/** Align patched artifacts with the repo root tree (nested semver/layout can miss regex rules). */
const vendoredPatches = [
'node_modules/undici/lib/dispatcher/client.js',
'node_modules/@discordjs/rest/package.json',
'node_modules/@discordjs/ws/dist/index.js',
'node_modules/@discordjs/ws/dist/index.mjs'
];
for (const rel of vendoredPatches) {
const from = path.join(repoRoot, rel);
const to = path.join(pearDir, rel);
if (existsSync(from)) {
mkdirSync(path.dirname(to), { recursive: true });
cpSync(from, to);
}
}
console.log(
'Pear release prep done. Next: cd examples/pear-discord-bot && npm run pear:ship -- <channel-or-link>'
);
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env node
/**
* Usage (from examples/pear-discord-bot):
* npm run pear:stage [-- <extra pear-args>]
* npm run pear:release [-- <extra pear-args>]
* npm run pear:ship [-- <extra pear-args>] # prepare once, then stage + release
*
* With no CLI args after the subcommand, `pear.channel` from examples/pear-discord-bot/package.json
* is used (fallback: pear.name). Override by passing <channel-or-link> first.
*/
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.join(__dirname, '..');
const pearDir = path.join(repoRoot, 'examples', 'pear-discord-bot');
const sub = process.argv[2];
let pearArgs = process.argv.slice(3);
function defaultPearChannel() {
const pkgPath = path.join(pearDir, 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
const ch = pkg.pear?.channel ?? pkg.pear?.name;
if (!ch || typeof ch !== 'string') {
console.error(
'examples/pear-discord-bot/package.json must define pear.channel or pear.name for stage/release/ship'
);
process.exit(1);
}
return ch;
}
function ensureDefaultPearArgs() {
if (pearArgs.length === 0) {
pearArgs = [defaultPearChannel()];
}
}
function runPrepare() {
const prep = spawnSync(process.execPath, [path.join(repoRoot, 'scripts', 'pear-prepare-release.mjs')], {
cwd: repoRoot,
stdio: 'inherit'
});
if (prep.status !== 0) process.exit(prep.status ?? 1);
}
const pearBin = process.platform === 'win32' ? 'pear.cmd' : 'pear';
if (sub === 'ship') {
ensureDefaultPearArgs();
runPrepare();
const st = spawnSync(pearBin, ['stage', ...pearArgs], {
cwd: pearDir,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (st.status !== 0) process.exit(st.status ?? 1);
const rel = spawnSync(pearBin, ['release', ...pearArgs], {
cwd: pearDir,
stdio: 'inherit',
shell: process.platform === 'win32'
});
process.exit(rel.status ?? 1);
}
if (sub !== 'stage' && sub !== 'release') {
console.error('Usage: pear-run.mjs <stage|release|ship> [...pear-args]');
process.exit(1);
}
ensureDefaultPearArgs();
runPrepare();
const pr = spawnSync(pearBin, [sub, ...pearArgs], {
cwd: pearDir,
stdio: 'inherit',
shell: process.platform === 'win32'
});
process.exit(pr.status ?? 1);
@@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const required = [
path.join(process.cwd(), 'artifacts', 'analysis', 'discordjs-surface.json'),
path.join(process.cwd(), 'artifacts', 'analysis', 'bare-surface.json'),
path.join(process.cwd(), 'patches', 'patch-manifest.json')
];
const missing = [];
for (const file of required) {
try {
await fs.access(file);
} catch {
missing.push(file);
}
}
if (missing.length) {
console.error('Release verification failed. Missing artifacts:\n' + missing.join('\n'));
process.exit(1);
}
console.log('Release verification passed.');
@@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Refresh the shipped NodeBare import map from the installed bare-node-runtime.
* Also writes a CJS module so Pear `pear://` loads do not need to read JSON from disk.
*/
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const bootstrapDir = path.join(repoRoot, 'src', 'bootstrap');
const fallbackPath = path.join(repoRoot, 'patches', 'imports-fallback.json');
function loadUpstreamImports() {
try {
const resolved = require.resolve('bare-node-runtime/imports');
return JSON.parse(fs.readFileSync(resolved, 'utf8'));
} catch {
const local = path.join(bootstrapDir, 'node-imports-map.json');
if (fs.existsSync(local)) {
return JSON.parse(fs.readFileSync(local, 'utf8'));
}
throw new Error('Unable to resolve bare-node-runtime/imports');
}
}
const imports = loadUpstreamImports();
const json = JSON.stringify(imports, null, 2) + '\n';
const cjs = `'use strict';\nmodule.exports = ${JSON.stringify(imports)};\n`;
fs.mkdirSync(bootstrapDir, { recursive: true });
fs.writeFileSync(path.join(bootstrapDir, 'node-imports-map.json'), json);
fs.writeFileSync(path.join(bootstrapDir, 'node-imports-map.cjs'), cjs);
fs.writeFileSync(fallbackPath, json);
console.log('Synced Node→Bare import map from bare-node-runtime/imports');
@@ -0,0 +1,16 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const outPath = path.join(process.cwd(), 'artifacts', 'analysis', 'patch-rebase-report.json');
const report = {
generatedAt: new Date().toISOString(),
status: 'simulated',
notes: [
'Patch rebase automation placeholder.',
'Integrate with upstream sync once patches become concrete.'
]
};
await fs.mkdir(path.dirname(outPath), { recursive: true });
await fs.writeFile(outPath, JSON.stringify(report, null, 2));
console.log('Wrote', outPath);
@@ -0,0 +1,18 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const report = {
generatedAt: new Date().toISOString(),
target: 'discord.js 14.27.x',
actions: [
'Fetch latest v14.x metadata',
'Re-run analysis scripts',
'Regenerate patch manifest',
'Run parity suite'
]
};
const outPath = path.join(process.cwd(), 'artifacts', 'analysis', 'upstream-sync-report.json');
await fs.mkdir(path.dirname(outPath), { recursive: true });
await fs.writeFile(outPath, JSON.stringify(report, null, 2));
console.log('Wrote', outPath);
@@ -0,0 +1,30 @@
import { createRequire } from 'node:module';
import path from 'node:path';
const require = createRequire(import.meta.url);
const projectRoot = path.join(process.cwd());
function fail(message) {
console.error(message);
process.exit(1);
}
try {
const cjsEntry = require(path.join(projectRoot, 'src', 'index.js'));
if (!cjsEntry || typeof cjsEntry.Client !== 'function') {
fail('CJS entry does not expose discord.js Client.');
}
} catch (error) {
fail(`CJS bootstrap failed: ${error.message}`);
}
try {
const esmEntry = await import(path.join(projectRoot, 'src', 'index.mjs'));
if (!esmEntry || typeof esmEntry.Client !== 'function') {
fail('ESM entry does not expose discord.js Client.');
}
} catch (error) {
fail(`ESM bootstrap failed: ${error.message}`);
}
console.log('Bootstrap verification passed.');
@@ -0,0 +1,19 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const reportPath = path.join(process.cwd(), 'artifacts', 'analysis', 'parity-report.json');
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const report = {
generatedAt: new Date().toISOString(),
strictParityMode: true,
suites: [
{ name: 'bootstrap-load', status: 'planned' },
{ name: 'gateway-login-smoke', status: 'planned' },
{ name: 'rest-rate-limit', status: 'planned' },
{ name: 'voice-transport', status: 'planned' }
]
};
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
console.log('Wrote', reportPath);
@@ -0,0 +1,19 @@
function createCryptoAdapter() {
return {
id: 'crypto',
apply() {
if (typeof Bare === 'undefined') return true;
if (globalThis.crypto) return true;
try {
require('bare-crypto/global');
} catch {
// Optional; mapped `node:crypto` still covers most discord.js usage.
}
return true;
}
};
}
module.exports = {
createCryptoAdapter
};
@@ -0,0 +1,68 @@
/**
* discord.js REST builds multipart bodies with `new FormData()` / `new Blob()`.
* Bare has neither global; install `bare-form-data` classes so `instanceof FormData`
* matches on the way out. Inlined from Bare OS (the vendor re-export path does not exist here).
*/
'use strict'
function isBareRuntime() {
if (typeof Bare !== 'undefined') return true
const versions =
typeof process !== 'undefined' && process.versions ? process.versions : null
return Boolean(versions && typeof versions.bare === 'string')
}
function assignGlobal(name, value) {
if (typeof value !== 'function') return
try {
globalThis[name] = value
} catch {
/* frozen */
}
try {
if (typeof global !== 'undefined') global[name] = value
} catch {
/* frozen */
}
}
function loadBareFormData() {
try {
return require('bare-form-data')
} catch {
return null
}
}
function installBareOsFormDataGlobal() {
if (!isBareRuntime()) return typeof globalThis.FormData === 'function'
const fd = loadBareFormData()
if (!fd) {
try {
require('bare-form-data/global')
} catch {
/* optional */
}
return typeof globalThis.FormData === 'function'
}
const FormData = typeof fd === 'function' ? fd : fd.FormData
assignGlobal('FormData', FormData)
assignGlobal('Blob', fd.Blob)
assignGlobal('File', fd.File)
return typeof globalThis.FormData === 'function'
}
function createFormDataAdapter() {
return {
id: 'form-data',
apply() {
installBareOsFormDataGlobal()
return true
}
}
}
module.exports = {
createFormDataAdapter,
installBareOsFormDataGlobal
}
@@ -0,0 +1,19 @@
function createHttpTlsAdapter() {
return {
id: 'http-tls',
apply() {
if (typeof Bare === 'undefined') return true;
if (typeof globalThis.fetch === 'function') return true;
try {
require('bare-fetch/global');
} catch {
// REST web build and discord.js Util/DataResolver use global fetch.
}
return true;
}
};
}
module.exports = {
createHttpTlsAdapter
};
@@ -0,0 +1,33 @@
const { createWsAdapter } = require('./ws.cjs');
const { createHttpTlsAdapter } = require('./http-tls.cjs');
const { createZlibAdapter } = require('./zlib.cjs');
const { createCryptoAdapter } = require('./crypto.cjs');
const { createFormDataAdapter } = require('./form-data.cjs');
function applyDiscordJsCompatShims(discord) {
if (!discord || typeof discord !== 'object') return discord;
const events = discord.Events;
if (events && events.ClientReady && events.Ready == null) {
events.Ready = events.ClientReady;
}
return discord;
}
function applyRuntimeAdapters() {
const adapters = [
createFormDataAdapter(),
createWsAdapter(),
createHttpTlsAdapter(),
createZlibAdapter(),
createCryptoAdapter()
];
for (const adapter of adapters) {
adapter.apply();
}
}
module.exports = {
applyRuntimeAdapters,
applyDiscordJsCompatShims
};
@@ -0,0 +1,7 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
export function applyRuntimeAdapters() {
return require('./index.cjs').applyRuntimeAdapters();
}
@@ -0,0 +1,194 @@
/**
* WHATWG WebSocket facade over bare-ws.Socket.
* @discordjs/ws calls `new WebSocket(url, protocols, opts)` and uses
* onmessage/send. bare-ws.Socket is a Duplex (`write` / `data`) using it
* as globalThis.WebSocket makes IDENTIFY never produce READY.
*/
'use strict'
function createWhatwgWebSocket() {
const BareSocket = require('bare-ws').Socket
class WhatwgWebSocket {
constructor(url, protocols, options) {
if (protocols && !Array.isArray(protocols) && typeof protocols === 'object') {
options = protocols
}
this.url = String(url || '')
this.readyState = WhatwgWebSocket.CONNECTING
this.binaryType = 'arraybuffer'
this.protocol = ''
this.extensions = ''
this.onopen = null
this.onmessage = null
this.onerror = null
this.onclose = null
this._listeners = Object.create(null)
const opts = options && typeof options === 'object' ? Object.assign({}, options) : {}
if (opts.rejectUnauthorized == null) opts.rejectUnauthorized = false
const self = this
this._ws = new BareSocket(this.url, opts)
this._ws.on('open', function () {
self.readyState = WhatwgWebSocket.OPEN
self._dispatch('open', { type: 'open', target: self })
})
this._ws.on('data', function (chunk) {
let data = chunk
if (typeof chunk !== 'string' && chunk && typeof chunk.toString === 'function') {
const text = chunk.toString()
const c0 = text.charCodeAt(0)
if (c0 === 0x7b || c0 === 0x5b) data = text
}
self._dispatch('message', { type: 'message', data: data, target: self })
})
this._ws.on('error', function (err) {
self._dispatch('error', {
type: 'error',
error: err,
message: err && err.message,
target: self
})
})
this._ws.on('close', function () {
self.readyState = WhatwgWebSocket.CLOSED
self._dispatch('close', {
type: 'close',
code: 1000,
reason: '',
wasClean: true,
target: self
})
})
}
send(data) {
if (this.readyState !== WhatwgWebSocket.OPEN) {
throw new Error('WebSocket is not open')
}
if (typeof data === 'string') this._ws.write(data)
else this._ws.write(data)
}
close() {
if (
this.readyState === WhatwgWebSocket.CLOSING ||
this.readyState === WhatwgWebSocket.CLOSED
) {
return
}
this.readyState = WhatwgWebSocket.CLOSING
try {
this._ws.end()
} catch {
try {
this._ws.destroy()
} catch {
/* ignore */
}
}
}
ping(data) {
if (this._ws && typeof this._ws.ping === 'function') this._ws.ping(data)
}
addEventListener(type, fn) {
if (typeof fn !== 'function') return
if (!this._listeners[type]) this._listeners[type] = []
this._listeners[type].push(fn)
}
removeEventListener(type, fn) {
const list = this._listeners[type]
if (!list) return
this._listeners[type] = list.filter(function (x) {
return x !== fn
})
}
_dispatch(type, event) {
const handler = this['on' + type]
if (typeof handler === 'function') {
try {
handler.call(this, event)
} catch {
/* isolate */
}
}
const list = this._listeners[type] || []
for (let i = 0; i < list.length; i++) {
try {
list[i].call(this, event)
} catch {
/* isolate */
}
}
}
}
WhatwgWebSocket.CONNECTING = 0
WhatwgWebSocket.OPEN = 1
WhatwgWebSocket.CLOSING = 2
WhatwgWebSocket.CLOSED = 3
WhatwgWebSocket.bareOsDiscordGatewayWs = 'bare-os-discord-gateway-ws'
return WhatwgWebSocket
}
function installBareOsProcessEmitWarning(proc) {
const p =
proc ||
(typeof globalThis.process !== 'undefined' ? globalThis.process : null)
if (!p || typeof p.emitWarning === 'function') return p
p.emitWarning = function emitWarning(warning, type, code) {
const msg = warning instanceof Error ? warning.message : String(warning)
const name = typeof type === 'string' ? type : 'Warning'
const id = typeof code === 'string' ? code : ''
const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg
try {
if (typeof p.emit === 'function') p.emit('warning', warning)
} catch {
/* ignore */
}
try {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(line)
}
} catch {
/* ignore */
}
}
return p
}
function installBareOsDiscordGatewayWs() {
installBareOsProcessEmitWarning()
const WS = createWhatwgWebSocket()
const versions =
typeof process !== 'undefined' && process.versions ? process.versions : null
if (versions && versions.bun == null) {
try {
versions.bun = 'bare-os'
} catch {
/* frozen */
}
}
if (typeof globalThis.fetch !== 'function') {
try {
require('bare-fetch/global')
} catch {
/* optional */
}
}
globalThis.WebSocket = WS
if (typeof global !== 'undefined') global.WebSocket = WS
return WS
}
module.exports = {
createWhatwgWebSocket,
installBareOsDiscordGatewayWs,
installBareOsProcessEmitWarning
}
+18
View File
@@ -0,0 +1,18 @@
function createWsAdapter() {
return {
id: 'ws',
apply() {
if (typeof Bare === 'undefined') return true;
try {
require('./whatwg-ws.cjs').installBareOsDiscordGatewayWs();
} catch {
/* optional when bare-ws is unavailable */
}
return true;
}
};
}
module.exports = {
createWsAdapter
};
@@ -0,0 +1,13 @@
function createZlibAdapter() {
return {
id: 'zlib',
apply() {
// Gateway compression uses mapped `bare-zlib`. zlib-sync is optional.
return true;
}
};
}
module.exports = {
createZlibAdapter
};
@@ -0,0 +1,57 @@
const BARE_GLOBAL_FALLBACKS = [
'bare-abort-controller/global',
'bare-crypto/global',
'bare-encoding/global',
'bare-events/global',
'bare-fetch/global',
'bare-form-data/global',
'bare-performance/global',
'bare-process/global',
'bare-stream/global',
'bare-ws/global'
];
function ensureNodeCompatGlobals() {
if (typeof process === 'undefined') return;
try {
if (!process.versions) process.versions = {};
} catch {
return;
}
const versions = process.versions;
if (!versions || typeof versions !== 'object') return;
if (!versions.node) {
try {
versions.node = '20.0.0';
} catch {
/* frozen / getter-only */
}
}
}
function loadFallbackGlobals() {
for (const specifier of BARE_GLOBAL_FALLBACKS) {
try {
require(specifier);
} catch {
// Individual globals may be unavailable on a slim install.
}
}
}
function setupBareGlobals() {
let loaded = false;
try {
require('bare-node-runtime/global');
loaded = true;
} catch {
loadFallbackGlobals();
}
ensureNodeCompatGlobals();
return loaded;
}
module.exports = {
setupBareGlobals
};
@@ -0,0 +1,7 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
export function setupBareGlobals() {
return require('./global.cjs').setupBareGlobals();
}
@@ -0,0 +1,12 @@
function getImportsSpecifier() {
try {
require.resolve('bare-node-runtime/imports');
return 'bare-node-runtime/imports';
} catch {
return '../patches/imports-fallback.json';
}
}
module.exports = {
getImportsSpecifier
};
@@ -0,0 +1,11 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
export function getImportsSpecifier() {
try {
require.resolve('bare-node-runtime/imports');
return 'bare-node-runtime/imports';
} catch {
return '../patches/imports-fallback.json';
}
}
@@ -0,0 +1,350 @@
const { getImportsSpecifier } = require('./imports-map.cjs');
function isBareRuntime() {
return typeof Bare !== 'undefined';
}
const fs = isBareRuntime() ? require('bare-fs') : require('node:fs');
const path = isBareRuntime() ? require('bare-path') : require('node:path');
const bareUrl = isBareRuntime() ? require('bare-url') : require('node:url');
/** `bare-path` join breaks `pear://key/...` → `pear:/key/...`; keep drive URLs intact. */
function normalizePearDriveUrl(u) {
if (typeof u !== 'string' || !u.startsWith('pear:')) return u;
const t = u.replace(/\/+$/, '');
if (t.startsWith('pear://')) return t;
return `pear://${t.replace(/^pear:\/?/, '')}`;
}
function joinUnderRoot(root, ...segments) {
const base = normalizePearDriveUrl(root);
if (typeof base === 'string' && base.startsWith('pear://')) {
const tail = segments
.map((s) => String(s).replace(/^\/+|\/+$/g, ''))
.filter(Boolean)
.join('/');
return tail ? `${base}/${tail}` : base;
}
return path.join(base, ...segments);
}
/** `bare-path` dirname breaks `pear://` URLs; used when walking resolved discord paths. */
function pearSafeDirname(filePath) {
const s = normalizePearDriveUrl(String(filePath));
if (!s.startsWith('pear://')) {
return path.dirname(filePath);
}
try {
const u = new bareUrl.URL(s);
const pathname = (u.pathname || '').replace(/\/+$/, '');
const slash = pathname.lastIndexOf('/');
if (slash <= 0) {
return `${u.protocol}//${u.host}`;
}
return `${u.protocol}//${u.host}${pathname.slice(0, slash)}`;
} catch {
const trimmed = s.replace(/\/+$/, '');
const idx = trimmed.lastIndexOf('/');
if (idx <= 6) return trimmed;
return trimmed.slice(0, idx);
}
}
function dirnameSafe(p) {
const s = String(p);
if (s.startsWith('pear:')) return pearSafeDirname(s);
return path.dirname(p);
}
/**
* Bare-module CJS passes the *Module instance itself* as the `module` arg to the wrapped
* function (see `bare-module/index.js` _evaluate createRequire(this._url, { module: this })).
* That instance carries `_protocol`/`_resolutions`/`_cache` from the loader. On Pear, that
* loader is the runtime which set a `pear://`-aware protocol; we propagate it via `referrer`
* to any `Module.createRequire` call so resolution can `protocol.exists/read` Pear URLs.
*/
function findPearAwareReferrer(currentModule) {
const candidates = [];
if (currentModule && typeof currentModule === 'object') candidates.push(currentModule);
try {
if (require.main) candidates.push(require.main);
} catch {
// ignore
}
try {
const cache = require.cache;
if (cache && typeof cache === 'object') {
for (const key of Object.keys(cache)) {
if (typeof key === 'string' && key.startsWith('pear:')) {
candidates.push(cache[key]);
}
}
}
} catch {
// ignore
}
for (const c of candidates) {
if (
c &&
typeof c === 'object' &&
c._protocol &&
typeof c._protocol.exists === 'function' &&
typeof c._protocol.read === 'function'
) {
return c;
}
}
return null;
}
/**
* Load NodeBare import mappings. Order:
* 1. `BARE_DISCORD_IMPORT_MAP_PATH` when present (app `bare-imports.json` after prepare).
* 2. **`require('./node-imports-map.cjs')`** always shipped with bare-discord-js; works on
* `pear://` drives (reading JSON via `require.resolve` + `readFileSync` breaks: resolves to
* bogus `/node_modules/...` host paths).
* 3. `bare-node-runtime/imports` JSON file.
* 4. Sibling `node-imports-map.json` via filesystem.
*
* Note: `examples/pear-discord-bot/.gitignore` lists `bare-imports.json`, so Pear stage often
* omits it; (2) keeps `pear run pear://…` working from any cwd. Local `pear run .` still uses (1)
* when the file exists on disk.
*/
/**
* Some Bare modules in the upstream NodeBare imports map self-initialise on require
* (`bare-worker` spawns a Thread, `bare-vm`/`bare-v8`/`bare-inspector` load native bindings).
* For runtime aliasing inside discord.js's transitive deps we don't need those undici
* `require('node:worker_threads')` lazily, only on opt-in features. Redirect the spec-keys to
* `bare-node-runtime/unsupported` (a no-op stub `bare-discord-js/preload-bare-modules.cjs`
* already includes) so `pear stage` doesn't pull side-effecting modules into the bundle.
*/
const SAFE_OVERRIDES = (() => {
const stub = { bare: 'bare-node-runtime/unsupported', default: 'bare-node-runtime/unsupported' };
const keys = [
'worker_threads',
'node:worker_threads',
'vm',
'node:vm',
'v8',
'node:v8',
'inspector',
'node:inspector',
'inspector/promises',
'node:inspector/promises',
'child_process',
'node:child_process',
'repl',
'node:repl',
'tty',
'node:tty',
'readline',
'node:readline',
'readline/promises',
'node:readline/promises',
'sqlite',
'node:sqlite'
];
const out = {};
for (const k of keys) out[k] = stub;
return out;
})();
function applyImportsOverrides(map) {
if (!map || typeof map !== 'object') return map;
return Object.assign({}, map, SAFE_OVERRIDES);
}
function loadRuntimeImportsMap() {
const envPath = process.env.BARE_DISCORD_IMPORT_MAP_PATH;
if (typeof envPath === 'string' && envPath.length > 0) {
try {
if (fs.existsSync(envPath)) {
return applyImportsOverrides(JSON.parse(fs.readFileSync(envPath, 'utf8')));
}
} catch {
// bare-fs may reject non-file URLs; the embedded map below covers pear:// runs.
}
}
try {
return applyImportsOverrides(require('./node-imports-map.cjs'));
} catch {
// ignore
}
try {
const candidate = require.resolve('bare-node-runtime/imports');
if (fs.existsSync(candidate)) {
return applyImportsOverrides(JSON.parse(fs.readFileSync(candidate, 'utf8')));
}
} catch {
// bare-node-runtime may be unavailable.
}
const embedded = path.join(__dirname, 'node-imports-map.json');
try {
if (fs.existsSync(embedded)) {
return applyImportsOverrides(JSON.parse(fs.readFileSync(embedded, 'utf8')));
}
} catch {
// ignore
}
return null;
}
function resolveCreateRequireAnchor() {
const env = process.env.BARE_DISCORD_REQUIRE_ANCHOR;
if (typeof env === 'string' && env.length > 0) {
return normalizePearDriveUrl(env);
}
return path.join(__dirname, '..', 'index.js');
}
/**
* discord.js package "." entry Bare cannot resolve the bare specifier from the
* package's own package.json anchor; load ./src/index.js (exports.require).
*/
function requireDiscordDotEntry(req, packageJsonPath) {
let pkg = null;
try {
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
} catch {
return req('./src/index.js');
}
let rel = null;
const dot = pkg.exports && pkg.exports['.'];
if (dot && typeof dot === 'object' && dot !== null && dot.require) {
const r = dot.require;
rel = typeof r === 'string' ? r : r.default;
}
if (!rel && pkg.main) rel = pkg.main;
if (!rel) rel = './index.js';
if (!rel.startsWith('.')) rel = `./${rel}`;
return req(rel);
}
/**
* Nested install, Pear hoisted sibling, monorepo hoisted.
* Pear: `path.join(__dirname, '..', '..', …)` can normalize to bogus host paths like
* `file:///node_modules/discord.js/…` when `__dirname` is wrong; prefer resolution via
* `require.resolve('discord.js', { paths })` then walk up to `discord.js`'s package.json.
* Apps may set `BARE_DISCORD_DISCORD_ROOT` to a vendored tree (e.g. `./vendor/discord.js`).
*/
function resolveDiscordPackageJsonPath() {
const vendorRoot = process.env.BARE_DISCORD_DISCORD_ROOT;
if (typeof vendorRoot === 'string' && vendorRoot.length > 0) {
return joinUnderRoot(vendorRoot, 'package.json');
}
const bareDiscordPkgRoot = path.join(__dirname, '..', '..');
function discordPkgFromResolvedEntry() {
let entry = null;
try {
entry = require.resolve('discord.js', { paths: [bareDiscordPkgRoot] });
} catch {
try {
entry = require.resolve('discord.js');
} catch {
return null;
}
}
let dir = dirnameSafe(entry);
for (let i = 0; i < 8; i++) {
const pkgJson = joinUnderRoot(dir, 'package.json');
try {
if (fs.existsSync(pkgJson)) {
const name = JSON.parse(fs.readFileSync(pkgJson, 'utf8')).name;
if (name === 'discord.js') return pkgJson;
}
} catch {
// ignore
}
const parent = dirnameSafe(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
const resolved = discordPkgFromResolvedEntry();
if (resolved) return resolved;
const candidates = [
path.join(__dirname, '..', '..', '..', 'discord.js', 'package.json'),
path.join(__dirname, '..', '..', '..', 'node_modules', 'discord.js', 'package.json'),
path.join(__dirname, '..', '..', '..', '..', 'node_modules', 'discord.js', 'package.json'),
path.join(__dirname, '..', 'node_modules', 'discord.js', 'package.json')
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) return p;
} catch {
// bare-fs may throw on some hosts.
}
}
return null;
}
function inBareAppBundle(currentModule) {
const hints = [
typeof __filename === 'string' ? __filename : '',
typeof __dirname === 'string' ? __dirname : '',
currentModule && currentModule.filename,
currentModule && currentModule.url,
currentModule && currentModule._url
];
for (const h of hints) {
const s = String(h || '');
if (s.startsWith('bare:') || s.includes('app.bundle')) return true;
}
return false;
}
function loadDiscordWithRuntimeMappings(currentModule) {
if (!isBareRuntime()) {
return require('discord.js');
}
// bare-pack rewrites this literal require into the app.bundle binding.
// createRequire(anchor).require('discord.js') does not resolve under bare:/app.bundle/.
if (inBareAppBundle(currentModule)) {
return require('discord.js');
}
const map = loadRuntimeImportsMap();
const referrer = findPearAwareReferrer(currentModule);
if (map) {
const Module = require('bare-module');
const discordPkgPath = resolveDiscordPackageJsonPath();
if (discordPkgPath) {
try {
const opts = referrer ? { imports: map, referrer } : { imports: map };
const req = Module.createRequire(discordPkgPath, opts);
return requireDiscordDotEntry(req, discordPkgPath);
} catch (err) {
console.error('[bare-discord-js] discord bootstrap failed:', err && err.message);
// Fall through: packed or hoisted require('discord.js') may still work.
}
}
try {
const anchor = resolveCreateRequireAnchor();
const opts = referrer ? { imports: map, referrer } : { imports: map };
const req = Module.createRequire(anchor, opts);
return req('discord.js');
} catch {
return require('discord.js');
}
}
const imports = getImportsSpecifier();
try {
return require('discord.js', { with: { imports } });
} catch {
return require('discord.js');
}
}
module.exports = {
loadDiscordWithRuntimeMappings
};
@@ -0,0 +1,11 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
/**
* Share the CJS bootstrap so Bare import maps, Pear referrers, and Node
* `require('discord.js')` stay on one path.
*/
export async function loadDiscordWithRuntimeMappings() {
return require('./load-discord.cjs').loadDiscordWithRuntimeMappings();
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,466 @@
{
"assert": {
"bare": "bare-assert",
"default": "assert"
},
"node:assert": {
"bare": "bare-assert",
"default": "assert"
},
"assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"node:assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"async_hooks": {
"bare": "bare-async-hooks",
"default": "async_hooks"
},
"node:async_hooks": {
"bare": "bare-async-hooks",
"default": "async_hooks"
},
"buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"node:buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"node:child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"node:cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"console": {
"bare": "bare-console",
"default": "console"
},
"node:console": {
"bare": "bare-console",
"default": "console"
},
"constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"node:constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"node:crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"node:dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"diagnostics_channel": {
"bare": "bare-diagnostics-channel",
"default": "diagnostics_channel"
},
"node:diagnostics_channel": {
"bare": "bare-diagnostics-channel",
"default": "diagnostics_channel"
},
"dns": {
"bare": "bare-dns",
"default": "dns"
},
"node:dns": {
"bare": "bare-dns",
"default": "dns"
},
"dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"node:dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"node:domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"events": {
"bare": "bare-events",
"default": "events"
},
"node:events": {
"bare": "bare-events",
"default": "events"
},
"fs": {
"bare": "bare-fs",
"default": "fs"
},
"node:fs": {
"bare": "bare-fs",
"default": "fs"
},
"fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"node:fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"http": {
"bare": "bare-http1",
"default": "http"
},
"node:http": {
"bare": "bare-http1",
"default": "http"
},
"http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"node:http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"https": {
"bare": "bare-https",
"default": "https"
},
"node:https": {
"bare": "bare-https",
"default": "https"
},
"inspector": {
"bare": "bare-inspector",
"default": "inspector"
},
"node:inspector": {
"bare": "bare-inspector",
"default": "inspector"
},
"inspector/promises": {
"bare": "bare-inspector/promises",
"default": "inspector/promises"
},
"node:inspector/promises": {
"bare": "bare-inspector/promises",
"default": "inspector/promises"
},
"module": {
"bare": "bare-module",
"default": "module"
},
"node:module": {
"bare": "bare-module",
"default": "module"
},
"net": {
"bare": "bare-net",
"default": "net"
},
"node:net": {
"bare": "bare-net",
"default": "net"
},
"os": {
"bare": "bare-os",
"default": "os"
},
"node:os": {
"bare": "bare-os",
"default": "os"
},
"path": {
"bare": "bare-path",
"default": "path"
},
"node:path": {
"bare": "bare-path",
"default": "path"
},
"path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"node:path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"node:path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"node:perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"process": {
"bare": "bare-process",
"default": "process"
},
"node:process": {
"bare": "bare-process",
"default": "process"
},
"punycode": {
"bare": "bare-punycode",
"default": "punycode"
},
"node:punycode": {
"bare": "bare-punycode",
"default": "punycode"
},
"querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"node:querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"readline": {
"bare": "bare-readline",
"default": "readline"
},
"node:readline": {
"bare": "bare-readline",
"default": "readline"
},
"readline/promises": {
"bare": "bare-readline/promises",
"default": "readline/promises"
},
"node:readline/promises": {
"bare": "bare-readline/promises",
"default": "readline/promises"
},
"repl": {
"bare": "bare-repl",
"default": "repl"
},
"node:repl": {
"bare": "bare-repl",
"default": "repl"
},
"sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"node:sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"sqlite": {
"bare": "bare-sqlite",
"default": "sqlite"
},
"node:sqlite": {
"bare": "bare-sqlite",
"default": "sqlite"
},
"stream": {
"bare": "bare-stream",
"default": "stream"
},
"node:stream": {
"bare": "bare-stream",
"default": "stream"
},
"stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"node:stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"node:stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"node:stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"node:string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"node:sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"node:test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"node:test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"timers": {
"bare": "bare-timers",
"default": "timers"
},
"node:timers": {
"bare": "bare-timers",
"default": "timers"
},
"timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"node:timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"tls": {
"bare": "bare-tls",
"default": "tls"
},
"node:tls": {
"bare": "bare-tls",
"default": "tls"
},
"trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"node:trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"tty": {
"bare": "bare-tty",
"default": "tty"
},
"node:tty": {
"bare": "bare-tty",
"default": "tty"
},
"url": {
"bare": "bare-url",
"default": "url"
},
"node:url": {
"bare": "bare-url",
"default": "url"
},
"util": {
"bare": "bare-utils",
"default": "util"
},
"node:util": {
"bare": "bare-utils",
"default": "util"
},
"util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"node:util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"v8": {
"bare": "bare-v8",
"default": "v8"
},
"node:v8": {
"bare": "bare-v8",
"default": "v8"
},
"vm": {
"bare": "bare-vm",
"default": "vm"
},
"node:vm": {
"bare": "bare-vm",
"default": "vm"
},
"wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"node:wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"node:worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"zlib": {
"bare": "bare-zlib",
"default": "zlib"
},
"node:zlib": {
"bare": "bare-zlib",
"default": "zlib"
}
}
@@ -0,0 +1,58 @@
/**
* Force the Pear bundler to statically include the Bare modules referenced by the runtime
* NodeBare imports map (`./node-imports-map.cjs`). Without these *literal-string* requires
* the bundler's static analysis cannot trace `require('node:util')``bare-utils` etc., so
* `pear stage` skips them and `pear run pear://…` fails with MODULE_NOT_FOUND for `node:util`.
*
* Only modules actually used by discord.js's transitive deps via the imports map are listed.
* Side-effecting Bare modules (`bare-worker` instantiates a Thread on first require,
* `bare-vm`/`bare-v8`/`bare-inspector` load native bindings unconditionally) are intentionally
* omitted pulling them in here breaks app startup even when discord.js never asks for them.
*
* Sub-paths must match each package's `exports` field; e.g. `bare-assert` has no `./strict`,
* `bare-dns` has no `./promises`. Including a non-exported subpath causes
* `PACKAGE_PATH_NOT_EXPORTED` during `pear stage`.
*/
'use strict';
try { require('bare-assert'); } catch {}
try { require('bare-async-hooks'); } catch {}
try { require('bare-buffer'); } catch {}
try { require('bare-console'); } catch {}
try { require('bare-crypto'); } catch {}
try { require('bare-diagnostics-channel'); } catch {}
try { require('bare-encoding'); } catch {}
try { require('bare-events'); } catch {}
try { require('bare-fetch'); } catch {}
try { require('bare-form-data'); } catch {}
try { require('bare-form-data/global'); } catch {}
try { require('bare-fs'); } catch {}
try { require('bare-fs/promises'); } catch {}
try { require('bare-fs/constants'); } catch {}
try { require('bare-http1'); } catch {}
try { require('bare-https'); } catch {}
try { require('bare-module'); } catch {}
try { require('bare-net'); } catch {}
try { require('bare-os'); } catch {}
try { require('bare-path'); } catch {}
try { require('bare-path/posix'); } catch {}
try { require('bare-path/win32'); } catch {}
try { require('bare-performance'); } catch {}
try { require('bare-process'); } catch {}
try { require('bare-querystring'); } catch {}
try { require('bare-stream'); } catch {}
try { require('bare-stream/promises'); } catch {}
try { require('bare-stream/web'); } catch {}
try { require('bare-string-decoder'); } catch {}
try { require('bare-timers'); } catch {}
try { require('bare-timers/promises'); } catch {}
try { require('bare-tls'); } catch {}
try { require('bare-url'); } catch {}
try { require('bare-utils'); } catch {}
try { require('bare-utils/types'); } catch {}
try { require('bare-ws'); } catch {}
try { require('bare-zlib'); } catch {}
try { require('bare-node-runtime/unsupported'); } catch {}
module.exports = {};
+12
View File
@@ -0,0 +1,12 @@
const { setupBareGlobals } = require('./bootstrap/global.cjs');
const { loadDiscordWithRuntimeMappings } = require('./bootstrap/load-discord.cjs');
const { applyRuntimeAdapters, applyDiscordJsCompatShims } = require('./adapters/index.cjs');
require('./bootstrap/preload-bare-modules.cjs');
setupBareGlobals();
applyRuntimeAdapters();
const discord = applyDiscordJsCompatShims(loadDiscordWithRuntimeMappings(module));
module.exports = discord;
+30
View File
@@ -0,0 +1,30 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const discord = require('./index.js');
export default discord;
export const {
ActionRowBuilder,
AttachmentBuilder,
ButtonBuilder,
ChannelType,
Client,
Collection,
EmbedBuilder,
Events,
GatewayIntentBits,
IntentsBitField,
MessageFlags,
ModalBuilder,
Options,
Partials,
PermissionsBitField,
REST,
Routes,
ShardingManager,
SlashCommandBuilder,
StringSelectMenuBuilder,
TextInputBuilder,
version
} = discord;
+1
View File
@@ -27,6 +27,7 @@
"build:dist:package": "node scripts/build-distributable.js --all --package", "build:dist:package": "node scripts/build-distributable.js --all --package",
"build:dist:media": "node scripts/build-distributable.js --package", "build:dist:media": "node scripts/build-distributable.js --package",
"install:capability:media": "bash scripts/install-capability-media.sh", "install:capability:media": "bash scripts/install-capability-media.sh",
"vendor:discord": "node scripts/vendor-bare-discord-js.mjs",
"test": "node scripts/test-host-units.js && npm --prefix native-host run test:bare", "test": "node scripts/test-host-units.js && npm --prefix native-host run test:bare",
"test:qvac-smoke": "npm --prefix native-host run test:qvac-smoke" "test:qvac-smoke": "npm --prefix native-host run test:qvac-smoke"
}, },
+3
View File
@@ -110,6 +110,9 @@ server.listen(PORT, HOST, () => {
console.log(` ${base}/whiteboard/`); console.log(` ${base}/whiteboard/`);
console.log(` ${base}/screenshare/`); console.log(` ${base}/screenshare/`);
console.log(` ${base}/data-demo/`); console.log(` ${base}/data-demo/`);
console.log(` ${base}/qvac-chat/`);
console.log(` ${base}/agent-studio/`);
console.log(` ${base}/discord-bot/`);
console.log(''); console.log('');
console.log('Do not open examples via file:// — Chrome treats each file as a unique origin.'); console.log('Do not open examples via file:// — Chrome treats each file as a unique origin.');
console.log('Press Ctrl+C to stop.'); console.log('Press Ctrl+C to stop.');
+38
View File
@@ -123,10 +123,48 @@ function testOriginAllowlist() {
assert.strictEqual(allow.isQvacDisabled({ type: 'capability', payload: { pack: 'agent', cmd: 'create' } }, {}), true); assert.strictEqual(allow.isQvacDisabled({ type: 'capability', payload: { pack: 'agent', cmd: 'create' } }, {}), true);
assert.strictEqual(allow.isQvacDisabled({ type: 'capability', payload: { pack: 'agent', cmd: 'status' } }, {}), false); assert.strictEqual(allow.isQvacDisabled({ type: 'capability', payload: { pack: 'agent', cmd: 'status' } }, {}), false);
assert.strictEqual(allow.isQvacDisabled({ type: 'fs.write' }, {}), false); assert.strictEqual(allow.isQvacDisabled({ type: 'fs.write' }, {}), false);
assert.strictEqual(allow.isCapabilityType('discord.construct'), true);
assert.strictEqual(allow.isDiscordDisabled({ type: 'discord.construct' }, {}), true);
assert.strictEqual(allow.isDiscordDisabled({ type: 'discord.surface' }, {}), false);
assert.strictEqual(allow.isDiscordDisabled({ type: 'discord.status' }, {}), false);
assert.strictEqual(allow.isDiscordDisabled({ type: 'discord.construct' }, { discordEnabled: true }), false);
assert.strictEqual(allow.isDiscordDisabled({ type: 'discord.setEnabled' }, { discordEnabled: true }), true);
assert.strictEqual(allow.isDiscordDisabled({ type: 'capability', payload: { pack: 'discord', cmd: 'login' } }, {}), true);
} }
testOriginAllowlist(); testOriginAllowlist();
function testDiscordSnapshot() {
const snap = require('../native-host/discord/snapshot.js');
const { normalizeDiscordToken } = require('../native-host/discord/load-discord.js');
assert.strictEqual(normalizeDiscordToken(' abc.def \n'), 'abc.def');
assert.strictEqual(normalizeDiscordToken('\uFEFFtok'), 'tok');
const handles = snap.createHandleTable();
const ix = {
commandName: 'ping',
isChatInputCommand() { return true; },
isButton() { return false; },
reply() { return Promise.resolve(); },
toJSON() { return { id: '1', commandName: 'ping' }; },
user: { id: '99', tag: 'u#0001' },
};
Object.defineProperty(ix, 'constructor', { value: { name: 'ChatInputCommandInteraction' } });
const s = snap.snapshotValue(ix, handles, 0);
assert.ok(s._handle, 'handle interned');
assert.strictEqual(s.isChatInputCommand, true);
assert.strictEqual(s.commandName, 'ping');
assert.ok(s._methods.indexOf('reply') >= 0, 'reply method listed');
const constants = snap.serializeConstants({
GatewayIntentBits: { Guilds: 1, DirectMessages: 4096 },
Client: function Client() {},
Events: { ClientReady: 'clientReady' },
});
assert.strictEqual(constants.GatewayIntentBits.Guilds, 1);
assert.ok(constants._classes.indexOf('Client') >= 0);
}
testDiscordSnapshot();
function testQvacCatalog() { function testQvacCatalog() {
const cat = require('../native-host/qvac/catalog.js'); const cat = require('../native-host/qvac/catalog.js');
assert.strictEqual(cat.resolveModelConstant('qwen3.5-4b'), 'QWEN3_5_4B_MULTIMODAL_Q4_K_M'); assert.strictEqual(cat.resolveModelConstant('qwen3.5-4b'), 'QWEN3_5_4B_MULTIMODAL_Q4_K_M');
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Copy bare-discord-js sources into native-host/vendor/bare-discord-js
* (no node_modules). Replaces any previous vendor tree, then inlines FormData.
*
* Env: BARE_OS_BARE_DISCORD_JS_SRC default /home/raven/dev/bare-discord-js
* Fallback: Bare OS vendor at ../bare-operating-system/... if present.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const dest = path.join(root, 'native-host', 'vendor', 'bare-discord-js');
const defaultSrc = '/home/raven/dev/bare-discord-js';
const bareOsVendor = '/home/raven/dev/bare-operating-system/packages/bare-os-booter/vendor/bare-discord-js';
const src = String(
process.env.BARE_OS_BARE_DISCORD_JS_SRC ||
(fs.existsSync(path.join(defaultSrc, 'package.json')) ? defaultSrc : bareOsVendor)
).trim();
if (!fs.existsSync(path.join(src, 'package.json'))) {
console.error('vendor-bare-discord-js: missing source:', src);
process.exit(1);
}
const srcPkg = JSON.parse(fs.readFileSync(path.join(src, 'package.json'), 'utf8'));
if (srcPkg.name !== 'bare-discord-js') {
console.error('vendor-bare-discord-js: unexpected package name:', srcPkg.name);
process.exit(1);
}
fs.mkdirSync(path.dirname(dest), { recursive: true });
execFileSync(
'rsync',
[
'-a',
'--delete',
'--exclude',
'node_modules',
'--exclude',
'.git',
'--exclude',
'artifacts',
'--exclude',
'examples',
'--exclude',
'test',
'--exclude',
'src/bundles',
'--exclude',
'*.tgz',
'--exclude',
'package-lock.json',
path.join(src, '/'),
dest + path.sep,
],
{ stdio: 'inherit' }
);
const formDataSrc = path.join(root, 'native-host', 'vendor', 'bare-discord-js', 'src', 'adapters', 'form-data.cjs');
const installer = path.join(
'/home/raven/dev/bare-operating-system/packages/bare-os-booter/lib/services/bare-os-discord-form-data.cjs'
);
if (fs.existsSync(formDataSrc)) {
const cur = fs.readFileSync(formDataSrc, 'utf8');
if (cur.includes('../../../../lib/services/bare-os-discord-form-data.cjs')) {
if (fs.existsSync(installer)) {
fs.writeFileSync(formDataSrc, fs.readFileSync(installer));
console.log('vendor-bare-discord-js: inlined FormData installer');
}
}
}
console.log('vendor-bare-discord-js: synced →', dest, `(${srcPkg.version})`);