@mulmobridge/client 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,13 +49,43 @@ client.onPush((ev) => {
49
49
 
50
50
  ## Ecosystem
51
51
 
52
- Part of the `@mulmobridge/*` package family:
52
+ Part of the [`@mulmobridge/*`](https://www.npmjs.com/~mulmobridge) package family.
53
53
 
54
- - **@mulmobridge/protocol** — shared types and constants
55
- - **@mulmobridge/client** — this package
56
- - **@mulmobridge/cli** — interactive terminal bridge
57
- - **@mulmobridge/telegram** — Telegram bot bridge
58
- - **@mulmobridge/chat-service** — server-side chat service
54
+ **Shared libraries:**
55
+
56
+ - [`@mulmobridge/client`](https://www.npmjs.com/package/@mulmobridge/client) — socket.io client library used by every bridge below ← **this package**
57
+ - [`@mulmobridge/protocol`](https://www.npmjs.com/package/@mulmobridge/protocol) — wire types and constants
58
+ - [`@mulmobridge/chat-service`](https://www.npmjs.com/package/@mulmobridge/chat-service) — server-side relay + session store
59
+ - [`@mulmobridge/relay`](https://www.npmjs.com/package/@mulmobridge/relay) — Cloudflare Workers webhook proxy
60
+ - [`@mulmobridge/mock-server`](https://www.npmjs.com/package/@mulmobridge/mock-server) — mock server for local bridge development
61
+
62
+ **Bridges** (one npm package per platform):
63
+
64
+ - [`@mulmobridge/bluesky`](https://www.npmjs.com/package/@mulmobridge/bluesky) — Bluesky DMs over atproto
65
+ - [`@mulmobridge/chatwork`](https://www.npmjs.com/package/@mulmobridge/chatwork) — Chatwork (Japanese business chat)
66
+ - [`@mulmobridge/cli`](https://www.npmjs.com/package/@mulmobridge/cli) — interactive terminal bridge
67
+ - [`@mulmobridge/discord`](https://www.npmjs.com/package/@mulmobridge/discord) — Discord bot via Gateway
68
+ - [`@mulmobridge/email`](https://www.npmjs.com/package/@mulmobridge/email) — IMAP poll + SMTP reply, threading preserved
69
+ - [`@mulmobridge/google-chat`](https://www.npmjs.com/package/@mulmobridge/google-chat) — Google Chat via MulmoBridge relay
70
+ - [`@mulmobridge/irc`](https://www.npmjs.com/package/@mulmobridge/irc) — IRC (Libera, Freenode, custom)
71
+ - [`@mulmobridge/line`](https://www.npmjs.com/package/@mulmobridge/line) — LINE Messaging API via MulmoBridge relay
72
+ - [`@mulmobridge/line-works`](https://www.npmjs.com/package/@mulmobridge/line-works) — LINE Works (enterprise LINE)
73
+ - [`@mulmobridge/mastodon`](https://www.npmjs.com/package/@mulmobridge/mastodon) — Mastodon DMs + mentions
74
+ - [`@mulmobridge/matrix`](https://www.npmjs.com/package/@mulmobridge/matrix) — Matrix / Element
75
+ - [`@mulmobridge/mattermost`](https://www.npmjs.com/package/@mulmobridge/mattermost) — Mattermost
76
+ - [`@mulmobridge/messenger`](https://www.npmjs.com/package/@mulmobridge/messenger) — Facebook Messenger via MulmoBridge relay
77
+ - [`@mulmobridge/nostr`](https://www.npmjs.com/package/@mulmobridge/nostr) — Nostr NIP-04 encrypted DMs
78
+ - [`@mulmobridge/rocketchat`](https://www.npmjs.com/package/@mulmobridge/rocketchat) — Rocket.Chat
79
+ - [`@mulmobridge/signal`](https://www.npmjs.com/package/@mulmobridge/signal) — Signal via signal-cli-rest-api
80
+ - [`@mulmobridge/slack`](https://www.npmjs.com/package/@mulmobridge/slack) — Slack Socket Mode
81
+ - [`@mulmobridge/teams`](https://www.npmjs.com/package/@mulmobridge/teams) — Microsoft Teams via Bot Framework
82
+ - [`@mulmobridge/telegram`](https://www.npmjs.com/package/@mulmobridge/telegram) — Telegram bot
83
+ - [`@mulmobridge/twilio-sms`](https://www.npmjs.com/package/@mulmobridge/twilio-sms) — SMS via Twilio Programmable Messaging
84
+ - [`@mulmobridge/viber`](https://www.npmjs.com/package/@mulmobridge/viber) — Viber Public Account bots
85
+ - [`@mulmobridge/webhook`](https://www.npmjs.com/package/@mulmobridge/webhook) — generic HTTP webhook bridge
86
+ - [`@mulmobridge/whatsapp`](https://www.npmjs.com/package/@mulmobridge/whatsapp) — WhatsApp Cloud API via MulmoBridge relay
87
+ - [`@mulmobridge/xmpp`](https://www.npmjs.com/package/@mulmobridge/xmpp) — XMPP / Jabber
88
+ - [`@mulmobridge/zulip`](https://www.npmjs.com/package/@mulmobridge/zulip) — Zulip
59
89
 
60
90
  ## License
61
91
 
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Socket } from "socket.io-client";
2
- import { type Attachment } from "@mulmobridge/protocol";
2
+ import { type Attachment, type BridgeOptions } from "@mulmobridge/protocol";
3
3
  export interface MessageAck {
4
4
  ok: boolean;
5
5
  reply?: string;
@@ -16,6 +16,14 @@ export interface BridgeClientOptions {
16
16
  transportId: string;
17
17
  /** Defaults to `$MULMOCLAUDE_API_URL` or `http://localhost:3001`. */
18
18
  apiUrl?: string;
19
+ /** Flat primitive bag forwarded to the host app's startChat
20
+ * callback via the handshake (`BridgeOptions` from the
21
+ * protocol). Values must be string / number / boolean — nested
22
+ * objects are rejected server-side by the chat-service. If
23
+ * omitted, the client auto-scrapes `<TRANSPORT>_BRIDGE_*` /
24
+ * `BRIDGE_*` env vars (producing string values). Pass `{}`
25
+ * explicitly to opt out of the scrape. */
26
+ options?: BridgeOptions;
19
27
  }
20
28
  export interface BridgeClient {
21
29
  /** Send a user turn to MulmoClaude, wait for the assistant reply. */
package/dist/client.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { io } from "socket.io-client";
15
15
  import { CHAT_SOCKET_EVENTS, CHAT_SOCKET_PATH } from "@mulmobridge/protocol";
16
16
  import { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
17
+ import { readBridgeEnvOptions } from "./options.js";
17
18
  // 6 min > the server's REPLY_TIMEOUT_MS (5 min) so the server's
18
19
  // timeout surfaces as a reply, not a client-side cancellation.
19
20
  const REPLY_TIMEOUT_MS = 6 * 60 * 1000;
@@ -38,9 +39,18 @@ export function requireBearerToken() {
38
39
  export function createBridgeClient(opts) {
39
40
  const apiUrl = opts.apiUrl ?? process.env.MULMOCLAUDE_API_URL ?? DEFAULT_API_URL;
40
41
  const token = requireBearerToken();
42
+ // `opts.options === undefined` → scrape env automatically.
43
+ // `opts.options === {}` → opt out of the scrape explicitly.
44
+ const options = opts.options ?? readBridgeEnvOptions(opts.transportId, process.env);
45
+ // Only include the `options` key in the handshake when there's
46
+ // something to send — keeps old servers unaware of the field from
47
+ // ever seeing an empty object on the wire.
48
+ const auth = { transportId: opts.transportId, token };
49
+ if (Object.keys(options).length > 0)
50
+ auth.options = options;
41
51
  const socket = io(apiUrl, {
42
52
  path: CHAT_SOCKET_PATH,
43
- auth: { transportId: opts.transportId, token },
53
+ auth,
44
54
  transports: ["websocket"],
45
55
  });
46
56
  installDefaultLogging(socket);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { createBridgeClient, requireBearerToken, type MessageAck, type PushEvent, type BridgeClientOptions, type BridgeClient } from "./client.js";
2
2
  export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
3
+ export { readBridgeEnvOptions } from "./options.js";
3
4
  export { chunkText } from "./text.js";
4
5
  export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, type ParsedDataUrl, } from "./mime.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @mulmobridge/client — shared socket.io client for all MulmoBridge bridges.
2
2
  export { createBridgeClient, requireBearerToken } from "./client.js";
3
3
  export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
4
+ export { readBridgeEnvOptions } from "./options.js";
4
5
  export { chunkText } from "./text.js";
5
6
  export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, } from "./mime.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Read `<TRANSPORT>_BRIDGE_*` and `BRIDGE_*` env vars into a
3
+ * lowerCamelCase-keyed bag ready to hand to `createBridgeClient`.
4
+ *
5
+ * Precedence when the same key resolves from both forms:
6
+ * transport-specific wins over shared.
7
+ *
8
+ * Transport ids with dashes (`google-chat`, `line-works`,
9
+ * `twilio-sms`, …) are normalised to underscores when building the
10
+ * env prefix: `google-chat` → `GOOGLE_CHAT_BRIDGE_*`. Dashes in env
11
+ * var names break shells, so `_` is the portable convention.
12
+ *
13
+ * Example:
14
+ * SLACK_BRIDGE_DEFAULT_ROLE=slack
15
+ * BRIDGE_DEFAULT_ROLE=general
16
+ * → `{ defaultRole: "slack" }`
17
+ *
18
+ * GOOGLE_CHAT_BRIDGE_DEFAULT_ROLE=support
19
+ * (with transportId="google-chat")
20
+ * → `{ defaultRole: "support" }`
21
+ */
22
+ export declare function readBridgeEnvOptions(transportId: string, env: Readonly<Record<string, string | undefined>>): Record<string, string>;
@@ -0,0 +1,88 @@
1
+ // Env-var scraper for the bridge options bag.
2
+ //
3
+ // Bridges don't want to hand-maintain a forward-list of env vars
4
+ // that should travel to the host app. Instead we scrape a single
5
+ // dedicated prefix pattern at `createBridgeClient()` time:
6
+ //
7
+ // <TRANSPORT>_BRIDGE_<KEY> — transport-specific, wins on clash
8
+ // BRIDGE_<KEY> — shared default across every bridge
9
+ //
10
+ // Both forms strip the prefix and convert the `UPPER_SNAKE` tail to
11
+ // `lowerCamel`. Empty string values are dropped so a stray
12
+ // `FOO=""` doesn't shadow `BAR`'s match.
13
+ //
14
+ // The `_BRIDGE_` segment is deliberate: it lets the bridge keep its
15
+ // own secrets (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, …) naturally
16
+ // outside the scrape — they have no `_BRIDGE_` segment so they're
17
+ // never picked up, no reserved-list needed.
18
+ // Convert UPPER_SNAKE_CASE → lowerCamelCase. Leading digits and
19
+ // adjacent underscores degrade gracefully (adjacent underscores
20
+ // collapse to a single word break; leading digits are allowed but
21
+ // kept as-is after the first segment is lowercased).
22
+ function snakeToLowerCamel(snake) {
23
+ const parts = snake
24
+ .toLowerCase()
25
+ .split("_")
26
+ .filter((segment) => segment.length > 0);
27
+ if (parts.length === 0)
28
+ return "";
29
+ const [head, ...rest] = parts;
30
+ return head + rest.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : "")).join("");
31
+ }
32
+ // Strip the prefix, return null if the name doesn't match.
33
+ function matchBridgePrefix(name, transportPrefix) {
34
+ if (name.startsWith(transportPrefix)) {
35
+ const tail = name.slice(transportPrefix.length);
36
+ return tail.length > 0 ? tail : null;
37
+ }
38
+ if (name.startsWith("BRIDGE_")) {
39
+ const tail = name.slice("BRIDGE_".length);
40
+ return tail.length > 0 ? tail : null;
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Read `<TRANSPORT>_BRIDGE_*` and `BRIDGE_*` env vars into a
46
+ * lowerCamelCase-keyed bag ready to hand to `createBridgeClient`.
47
+ *
48
+ * Precedence when the same key resolves from both forms:
49
+ * transport-specific wins over shared.
50
+ *
51
+ * Transport ids with dashes (`google-chat`, `line-works`,
52
+ * `twilio-sms`, …) are normalised to underscores when building the
53
+ * env prefix: `google-chat` → `GOOGLE_CHAT_BRIDGE_*`. Dashes in env
54
+ * var names break shells, so `_` is the portable convention.
55
+ *
56
+ * Example:
57
+ * SLACK_BRIDGE_DEFAULT_ROLE=slack
58
+ * BRIDGE_DEFAULT_ROLE=general
59
+ * → `{ defaultRole: "slack" }`
60
+ *
61
+ * GOOGLE_CHAT_BRIDGE_DEFAULT_ROLE=support
62
+ * (with transportId="google-chat")
63
+ * → `{ defaultRole: "support" }`
64
+ */
65
+ export function readBridgeEnvOptions(transportId, env) {
66
+ const transportPrefix = `${transportId.toUpperCase().replace(/-/g, "_")}_BRIDGE_`;
67
+ const shared = {};
68
+ const specific = {};
69
+ for (const [name, value] of Object.entries(env)) {
70
+ if (typeof value !== "string" || value.length === 0)
71
+ continue;
72
+ const tail = matchBridgePrefix(name, transportPrefix);
73
+ if (tail === null)
74
+ continue;
75
+ const key = snakeToLowerCamel(tail);
76
+ if (!key)
77
+ continue;
78
+ if (name.startsWith(transportPrefix)) {
79
+ specific[key] = value;
80
+ }
81
+ else {
82
+ shared[key] = value;
83
+ }
84
+ }
85
+ // Transport-specific overrides shared on conflict — spread order
86
+ // (shared first, then specific) gives exactly that behaviour.
87
+ return { ...shared, ...specific };
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmobridge/client",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Socket.io client library for MulmoBridge — shared by all bridge implementations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "license": "MIT",
46
46
  "author": "Receptron Team",
47
47
  "dependencies": {
48
- "@mulmobridge/protocol": "^0.1.0",
48
+ "@mulmobridge/protocol": "^0.1.4",
49
49
  "socket.io-client": "^4.0.0"
50
50
  },
51
51
  "devDependencies": {