@mulmobridge/client 0.1.4 → 0.2.0
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/frame.d.ts +1 -0
- package/dist/frame.js +15 -0
- package/dist/http.d.ts +10 -0
- package/dist/http.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mime.d.ts +9 -6
- package/dist/mime.js +7 -4
- package/dist/options.js +5 -48
- package/dist/reply.d.ts +8 -0
- package/dist/reply.js +19 -0
- package/package.json +3 -2
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function frameText(data: Buffer | ArrayBuffer | Buffer[]): string;
|
package/dist/frame.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Decode a `ws` message frame to a utf8 string, shared by the WebSocket-based
|
|
2
|
+
// bridges (Mastodon, Signal).
|
|
3
|
+
//
|
|
4
|
+
// `ws` hands the listener `Buffer | ArrayBuffer | Buffer[]`. The default
|
|
5
|
+
// binaryType is nodebuffer so a Buffer is what actually arrives, but the type
|
|
6
|
+
// admits ArrayBuffer — whose `toString()` is the literal "[object ArrayBuffer]",
|
|
7
|
+
// i.e. a frame silently parsed as garbage. Normalise instead of trusting the
|
|
8
|
+
// runtime default to hold.
|
|
9
|
+
export function frameText(data) {
|
|
10
|
+
if (Buffer.isBuffer(data))
|
|
11
|
+
return data.toString("utf8");
|
|
12
|
+
if (Array.isArray(data))
|
|
13
|
+
return Buffer.concat(data).toString("utf8");
|
|
14
|
+
return Buffer.from(data).toString("utf8");
|
|
15
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type JsonRecord = Record<string, unknown>;
|
|
2
|
+
/** Narrow a parsed JSON value to a record, defaulting non-objects to `{}` —
|
|
3
|
+
* the `isRecord(json) ? json : {}` idiom the REST bridges repeated so they
|
|
4
|
+
* never have to cast `JSON.parse` output. */
|
|
5
|
+
export declare function asJsonRecord(json: unknown): JsonRecord;
|
|
6
|
+
/** `fetch` + non-2xx guard + JSON-record narrow. Callers own the URL, auth
|
|
7
|
+
* headers, and timeout (via `init.signal`); `errorLabel` prefixes the thrown
|
|
8
|
+
* non-2xx error (e.g. "GET /im.list"). Network errors propagate from `fetch`
|
|
9
|
+
* unchanged, matching the callers' existing try/catch expectations. */
|
|
10
|
+
export declare function fetchJsonRecord(url: string, init: RequestInit, errorLabel: string): Promise<JsonRecord>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Minimal JSON-over-REST fetch skeleton shared by the polling bridges
|
|
2
|
+
// (Rocket.Chat, Zulip), which used to carry byte-identical GET/POST wrappers.
|
|
3
|
+
import { isRecord } from "@mulmoclaude/common";
|
|
4
|
+
const MAX_ERROR_BODY_CHARS = 200;
|
|
5
|
+
/** Narrow a parsed JSON value to a record, defaulting non-objects to `{}` —
|
|
6
|
+
* the `isRecord(json) ? json : {}` idiom the REST bridges repeated so they
|
|
7
|
+
* never have to cast `JSON.parse` output. */
|
|
8
|
+
export function asJsonRecord(json) {
|
|
9
|
+
return isRecord(json) ? json : {};
|
|
10
|
+
}
|
|
11
|
+
/** `fetch` + non-2xx guard + JSON-record narrow. Callers own the URL, auth
|
|
12
|
+
* headers, and timeout (via `init.signal`); `errorLabel` prefixes the thrown
|
|
13
|
+
* non-2xx error (e.g. "GET /im.list"). Network errors propagate from `fetch`
|
|
14
|
+
* unchanged, matching the callers' existing try/catch expectations. */
|
|
15
|
+
export async function fetchJsonRecord(url, init, errorLabel) {
|
|
16
|
+
const res = await fetch(url, init);
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const text = await res.text().catch(() => "");
|
|
19
|
+
throw new Error(`${errorLabel}: ${res.status} ${text.slice(0, MAX_ERROR_BODY_CHARS)}`);
|
|
20
|
+
}
|
|
21
|
+
return asJsonRecord(await res.json());
|
|
22
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ export { createBridgeClient, requireBearerToken, type MessageAck, type PushEvent
|
|
|
2
2
|
export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
|
|
3
3
|
export { readBridgeEnvOptions } from "./options.js";
|
|
4
4
|
export { chunkText } from "./text.js";
|
|
5
|
+
export { frameText } from "./frame.js";
|
|
6
|
+
export { asJsonRecord, fetchJsonRecord, type JsonRecord } from "./http.js";
|
|
7
|
+
export { formatAckReply } from "./reply.js";
|
|
5
8
|
export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, type ParsedDataUrl, } from "./mime.js";
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,7 @@ export { createBridgeClient, requireBearerToken } from "./client.js";
|
|
|
3
3
|
export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
|
|
4
4
|
export { readBridgeEnvOptions } from "./options.js";
|
|
5
5
|
export { chunkText } from "./text.js";
|
|
6
|
+
export { frameText } from "./frame.js";
|
|
7
|
+
export { asJsonRecord, fetchJsonRecord } from "./http.js";
|
|
8
|
+
export { formatAckReply } from "./reply.js";
|
|
6
9
|
export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, } from "./mime.js";
|
package/dist/mime.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/** Infer MIME type from a file extension (case-insensitive).
|
|
2
2
|
* Returns the fallback if the extension is not recognised. */
|
|
3
3
|
export declare function mimeFromExtension(ext: string, fallback?: string): string;
|
|
4
|
-
/** True when the MIME type is an image Claude can see via vision.
|
|
5
|
-
|
|
4
|
+
/** True when the MIME type is an image Claude can see via vision.
|
|
5
|
+
* Tolerates `undefined` so callers can pass an attachment's optional
|
|
6
|
+
* `mimeType` directly. */
|
|
7
|
+
export declare function isImageMime(mimeType: string | undefined): boolean;
|
|
6
8
|
/** True when the MIME type is a PDF document Claude can read natively
|
|
7
|
-
* via `type: "document"` content blocks. */
|
|
8
|
-
export declare function isPdfMime(mimeType: string): boolean;
|
|
9
|
+
* via `type: "document"` content blocks. Tolerates `undefined`. */
|
|
10
|
+
export declare function isPdfMime(mimeType: string | undefined): boolean;
|
|
9
11
|
/** True when the attachment can be sent to Claude directly as a
|
|
10
|
-
* native content block (image or PDF) — no conversion needed.
|
|
11
|
-
|
|
12
|
+
* native content block (image or PDF) — no conversion needed.
|
|
13
|
+
* Tolerates `undefined`. */
|
|
14
|
+
export declare function isNativeAttachmentMime(mimeType: string | undefined): boolean;
|
|
12
15
|
export declare const isSupportedAttachmentMime: typeof isNativeAttachmentMime;
|
|
13
16
|
export interface ParsedDataUrl {
|
|
14
17
|
mimeType: string;
|
package/dist/mime.js
CHANGED
|
@@ -31,17 +31,20 @@ const EXT_TO_MIME = {
|
|
|
31
31
|
export function mimeFromExtension(ext, fallback = "application/octet-stream") {
|
|
32
32
|
return EXT_TO_MIME[ext.toLowerCase()] ?? fallback;
|
|
33
33
|
}
|
|
34
|
-
/** True when the MIME type is an image Claude can see via vision.
|
|
34
|
+
/** True when the MIME type is an image Claude can see via vision.
|
|
35
|
+
* Tolerates `undefined` so callers can pass an attachment's optional
|
|
36
|
+
* `mimeType` directly. */
|
|
35
37
|
export function isImageMime(mimeType) {
|
|
36
|
-
return mimeType.startsWith("image/");
|
|
38
|
+
return typeof mimeType === "string" && mimeType.startsWith("image/");
|
|
37
39
|
}
|
|
38
40
|
/** True when the MIME type is a PDF document Claude can read natively
|
|
39
|
-
* via `type: "document"` content blocks. */
|
|
41
|
+
* via `type: "document"` content blocks. Tolerates `undefined`. */
|
|
40
42
|
export function isPdfMime(mimeType) {
|
|
41
43
|
return mimeType === "application/pdf";
|
|
42
44
|
}
|
|
43
45
|
/** True when the attachment can be sent to Claude directly as a
|
|
44
|
-
* native content block (image or PDF) — no conversion needed.
|
|
46
|
+
* native content block (image or PDF) — no conversion needed.
|
|
47
|
+
* Tolerates `undefined`. */
|
|
45
48
|
export function isNativeAttachmentMime(mimeType) {
|
|
46
49
|
return isImageMime(mimeType) || isPdfMime(mimeType);
|
|
47
50
|
}
|
package/dist/options.js
CHANGED
|
@@ -9,38 +9,15 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Both forms strip the prefix and convert the `UPPER_SNAKE` tail to
|
|
11
11
|
// `lowerCamel`. Empty string values are dropped so a stray
|
|
12
|
-
// `FOO=""` doesn't shadow `BAR`'s match.
|
|
12
|
+
// `FOO=""` doesn't shadow `BAR`'s match. The scan itself is the
|
|
13
|
+
// shared `scanEnvOptions` (#2487) — the host's relay path resolves
|
|
14
|
+
// its `RELAY_*` scheme through the same algorithm.
|
|
13
15
|
//
|
|
14
16
|
// The `_BRIDGE_` segment is deliberate: it lets the bridge keep its
|
|
15
17
|
// own secrets (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, …) naturally
|
|
16
18
|
// outside the scrape — they have no `_BRIDGE_` segment so they're
|
|
17
19
|
// never picked up, no reserved-list needed.
|
|
18
|
-
|
|
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
|
-
}
|
|
20
|
+
import { scanEnvOptions } from "@mulmoclaude/common";
|
|
44
21
|
/**
|
|
45
22
|
* Read `<TRANSPORT>_BRIDGE_*` and `BRIDGE_*` env vars into a
|
|
46
23
|
* lowerCamelCase-keyed bag ready to hand to `createBridgeClient`.
|
|
@@ -64,25 +41,5 @@ function matchBridgePrefix(name, transportPrefix) {
|
|
|
64
41
|
*/
|
|
65
42
|
export function readBridgeEnvOptions(transportId, env) {
|
|
66
43
|
const transportPrefix = `${transportId.toUpperCase().replace(/-/g, "_")}_BRIDGE_`;
|
|
67
|
-
|
|
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 };
|
|
44
|
+
return scanEnvOptions(env, { prefixes: ["BRIDGE_", transportPrefix] });
|
|
88
45
|
}
|
package/dist/reply.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MessageAck } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a `MessageAck` as a single user-facing string.
|
|
4
|
+
*
|
|
5
|
+
* - `ok` ack → `reply` content (or empty string if absent).
|
|
6
|
+
* - Failed ack → `"Error[ (status)]: <error or 'unknown'>"`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatAckReply(ack: MessageAck): string;
|
package/dist/reply.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Shared reply formatting for bridges.
|
|
2
|
+
//
|
|
3
|
+
// Every bridge converts a `MessageAck` from `BridgeClient.send()`
|
|
4
|
+
// into a single string to send back over its native transport. The
|
|
5
|
+
// happy-path / error-path shape is identical across bridges
|
|
6
|
+
// (LINE / Slack / Teams / Mastodon / XMPP all do the same thing
|
|
7
|
+
// modulo the send call), so the formatting belongs here.
|
|
8
|
+
/**
|
|
9
|
+
* Format a `MessageAck` as a single user-facing string.
|
|
10
|
+
*
|
|
11
|
+
* - `ok` ack → `reply` content (or empty string if absent).
|
|
12
|
+
* - Failed ack → `"Error[ (status)]: <error or 'unknown'>"`.
|
|
13
|
+
*/
|
|
14
|
+
export function formatAckReply(ack) {
|
|
15
|
+
if (ack.ok)
|
|
16
|
+
return ack.reply ?? "";
|
|
17
|
+
const status = ack.status ? ` (${ack.status})` : "";
|
|
18
|
+
return `Error${status}: ${ack.error ?? "unknown"}`;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmobridge/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Socket.io client library for MulmoBridge — shared by all bridge implementations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,10 +46,11 @@
|
|
|
46
46
|
"author": "Receptron Team",
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@mulmobridge/protocol": "^0.1.4",
|
|
49
|
+
"@mulmoclaude/common": "^1.1.0",
|
|
49
50
|
"socket.io-client": "^4.0.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^26.1.1",
|
|
53
54
|
"typescript": "^6.0.3"
|
|
54
55
|
}
|
|
55
56
|
}
|