@mulmobridge/client 0.1.1 → 0.1.3
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/dist/client.d.ts +9 -1
- package/dist/client.js +13 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/options.d.ts +13 -0
- package/dist/options.js +79 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.js +14 -0
- package/package.json +19 -1
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
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// See `docs/bridge-protocol.md` for the wire-level contract and a
|
|
13
13
|
// minimal non-Node equivalent.
|
|
14
14
|
import { io } from "socket.io-client";
|
|
15
|
-
import { CHAT_SOCKET_EVENTS, CHAT_SOCKET_PATH
|
|
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
|
|
53
|
+
auth,
|
|
44
54
|
transports: ["websocket"],
|
|
45
55
|
});
|
|
46
56
|
installDefaultLogging(socket);
|
|
@@ -71,9 +81,7 @@ function sendMessage(socket, externalChatId, text, attachments) {
|
|
|
71
81
|
if (attachments && attachments.length > 0)
|
|
72
82
|
payload.attachments = attachments;
|
|
73
83
|
return new Promise((resolve) => {
|
|
74
|
-
socket
|
|
75
|
-
.timeout(REPLY_TIMEOUT_MS)
|
|
76
|
-
.emit(CHAT_SOCKET_EVENTS.message, payload, (err, ack) => {
|
|
84
|
+
socket.timeout(REPLY_TIMEOUT_MS).emit(CHAT_SOCKET_EVENTS.message, payload, (err, ack) => {
|
|
77
85
|
if (err) {
|
|
78
86
|
resolve({ ok: false, error: `timeout: ${err.message}` });
|
|
79
87
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export { createBridgeClient, requireBearerToken, type MessageAck, type PushEvent, type BridgeClientOptions, type BridgeClient
|
|
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";
|
|
4
|
+
export { chunkText } from "./text.js";
|
|
3
5
|
export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, type ParsedDataUrl, } from "./mime.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @mulmobridge/client — shared socket.io client for all MulmoBridge bridges.
|
|
2
|
-
export { createBridgeClient, requireBearerToken
|
|
2
|
+
export { createBridgeClient, requireBearerToken } from "./client.js";
|
|
3
3
|
export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
|
|
4
|
+
export { readBridgeEnvOptions } from "./options.js";
|
|
5
|
+
export { chunkText } from "./text.js";
|
|
4
6
|
export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, } from "./mime.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
* Example:
|
|
9
|
+
* SLACK_BRIDGE_DEFAULT_ROLE=slack
|
|
10
|
+
* BRIDGE_DEFAULT_ROLE=general
|
|
11
|
+
* → `{ defaultRole: "slack" }`
|
|
12
|
+
*/
|
|
13
|
+
export declare function readBridgeEnvOptions(transportId: string, env: Readonly<Record<string, string | undefined>>): Record<string, string>;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
* Example:
|
|
52
|
+
* SLACK_BRIDGE_DEFAULT_ROLE=slack
|
|
53
|
+
* BRIDGE_DEFAULT_ROLE=general
|
|
54
|
+
* → `{ defaultRole: "slack" }`
|
|
55
|
+
*/
|
|
56
|
+
export function readBridgeEnvOptions(transportId, env) {
|
|
57
|
+
const transportPrefix = `${transportId.toUpperCase()}_BRIDGE_`;
|
|
58
|
+
const shared = {};
|
|
59
|
+
const specific = {};
|
|
60
|
+
for (const [name, value] of Object.entries(env)) {
|
|
61
|
+
if (typeof value !== "string" || value.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const tail = matchBridgePrefix(name, transportPrefix);
|
|
64
|
+
if (tail === null)
|
|
65
|
+
continue;
|
|
66
|
+
const key = snakeToLowerCamel(tail);
|
|
67
|
+
if (!key)
|
|
68
|
+
continue;
|
|
69
|
+
if (name.startsWith(transportPrefix)) {
|
|
70
|
+
specific[key] = value;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
shared[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Transport-specific overrides shared on conflict — spread order
|
|
77
|
+
// (shared first, then specific) gives exactly that behaviour.
|
|
78
|
+
return { ...shared, ...specific };
|
|
79
|
+
}
|
package/dist/text.d.ts
ADDED
package/dist/text.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Text utilities shared across bridge packages.
|
|
2
|
+
/**
|
|
3
|
+
* Split text into chunks of at most `max` characters.
|
|
4
|
+
* Returns `["(empty reply)"]` when text is empty.
|
|
5
|
+
*/
|
|
6
|
+
export function chunkText(text, max) {
|
|
7
|
+
if (text.length === 0)
|
|
8
|
+
return ["(empty reply)"];
|
|
9
|
+
const chunks = [];
|
|
10
|
+
for (let i = 0; i < text.length; i += max) {
|
|
11
|
+
chunks.push(text.slice(i, i + max));
|
|
12
|
+
}
|
|
13
|
+
return chunks;
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmobridge/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Socket.io client library for MulmoBridge — shared by all bridge implementations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,24 @@
|
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"require": "./dist/index.js",
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./text": {
|
|
16
|
+
"types": "./dist/text.d.ts",
|
|
17
|
+
"import": "./dist/text.js",
|
|
18
|
+
"require": "./dist/text.js",
|
|
19
|
+
"default": "./dist/text.js"
|
|
20
|
+
},
|
|
21
|
+
"./mime": {
|
|
22
|
+
"types": "./dist/mime.d.ts",
|
|
23
|
+
"import": "./dist/mime.js",
|
|
24
|
+
"require": "./dist/mime.js",
|
|
25
|
+
"default": "./dist/mime.js"
|
|
26
|
+
},
|
|
27
|
+
"./token": {
|
|
28
|
+
"types": "./dist/token.d.ts",
|
|
29
|
+
"import": "./dist/token.js",
|
|
30
|
+
"require": "./dist/token.js",
|
|
31
|
+
"default": "./dist/token.js"
|
|
14
32
|
}
|
|
15
33
|
},
|
|
16
34
|
"files": [
|