@mulmobridge/line 0.1.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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @mulmobridge/line
2
+
3
+ > **Experimental** — please test and [report issues](https://github.com/receptron/mulmoclaude/issues/new).
4
+
5
+ LINE bridge for [MulmoClaude](https://github.com/receptron/mulmoclaude). Uses webhook events — requires a public URL (ngrok for development).
6
+
7
+ ## Setup
8
+
9
+ ### 1. Create a LINE Messaging API Channel
10
+
11
+ 1. Go to [LINE Developers Console](https://developers.line.biz/console/) → create a Provider → create a **Messaging API** channel
12
+ 2. Note the **Channel secret** (Basic settings tab)
13
+ 3. Issue a **Channel access token** (Messaging API tab → long-lived)
14
+
15
+ ### 2. Set up ngrok (for development)
16
+
17
+ ```bash
18
+ ngrok http 3002
19
+ # Copy the https://xxxx.ngrok-free.app URL
20
+ ```
21
+
22
+ ### 3. Configure the webhook
23
+
24
+ In the LINE Developers Console → Messaging API tab:
25
+ - **Webhook URL**: `https://xxxx.ngrok-free.app/webhook`
26
+ - **Use webhook**: enabled
27
+ - **Auto-reply messages**: disabled (so LINE's default replies don't interfere)
28
+
29
+ ### 4. Run the bridge
30
+
31
+ ```bash
32
+ # With mock server (testing)
33
+ npx @mulmobridge/mock-server &
34
+ LINE_CHANNEL_SECRET=... \
35
+ LINE_CHANNEL_ACCESS_TOKEN=... \
36
+ MULMOCLAUDE_AUTH_TOKEN=mock-test-token \
37
+ npx @mulmobridge/line
38
+
39
+ # With real MulmoClaude
40
+ LINE_CHANNEL_SECRET=... \
41
+ LINE_CHANNEL_ACCESS_TOKEN=... \
42
+ npx @mulmobridge/line
43
+ ```
44
+
45
+ ### 5. Add the bot as a friend
46
+
47
+ Scan the QR code in the LINE Developers Console → Messaging API tab. Send a message — MulmoClaude replies.
48
+
49
+ ## Environment Variables
50
+
51
+ | Variable | Required | Description |
52
+ |---|---|---|
53
+ | `LINE_CHANNEL_SECRET` | Yes | Channel secret for signature verification |
54
+ | `LINE_CHANNEL_ACCESS_TOKEN` | Yes | Long-lived channel access token |
55
+ | `LINE_BRIDGE_PORT` | No | Webhook port (default: 3002) |
56
+ | `MULMOCLAUDE_API_URL` | No | Default `http://localhost:3001` |
57
+ | `MULMOCLAUDE_AUTH_TOKEN` | No | Bearer token |
58
+
59
+ ## Notes
60
+
61
+ - LINE reply tokens expire in **1 minute**. Since Claude responses can take longer, the bridge uses **push messages** instead of reply messages. This requires your bot to be a verified/certified account for push to work with all users, OR the user must have added the bot as a friend first.
62
+ - LINE limits messages to 5 per push call and ~5000 chars per message. Long replies are automatically chunked.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // @mulmobridge/line — LINE bridge for MulmoClaude.
3
+ //
4
+ // Runs a small HTTP server to receive LINE webhook events.
5
+ // Requires a public URL (use ngrok for development).
6
+ //
7
+ // Required env vars:
8
+ // LINE_CHANNEL_SECRET — Channel secret from LINE Developers Console
9
+ // LINE_CHANNEL_ACCESS_TOKEN — Channel access token (long-lived)
10
+ //
11
+ // Optional:
12
+ // LINE_BRIDGE_PORT — Webhook listener port (default: 3002)
13
+ // MULMOCLAUDE_API_URL — default http://localhost:3001
14
+ // MULMOCLAUDE_AUTH_TOKEN — bearer token
15
+ import "dotenv/config";
16
+ import crypto from "crypto";
17
+ import express from "express";
18
+ import { createBridgeClient } from "@mulmobridge/client";
19
+ const TRANSPORT_ID = "line";
20
+ const PORT = Number(process.env.LINE_BRIDGE_PORT) || 3002;
21
+ const channelSecret = process.env.LINE_CHANNEL_SECRET;
22
+ const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN;
23
+ if (!channelSecret || !channelAccessToken) {
24
+ console.error("LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN are required.\n" +
25
+ "See README for setup instructions.");
26
+ process.exit(1);
27
+ }
28
+ const client = createBridgeClient({ transportId: TRANSPORT_ID });
29
+ client.onPush((ev) => {
30
+ pushMessage(ev.chatId, ev.message).catch((err) => console.error(`[line] push send failed: ${err}`));
31
+ });
32
+ // ── LINE API helpers ────────────────────────────────────────────
33
+ async function pushMessage(userId, text) {
34
+ const messages = chunkText(text).map((t) => ({ type: "text", text: t }));
35
+ // LINE allows max 5 messages per push
36
+ for (let i = 0; i < messages.length; i += 5) {
37
+ try {
38
+ const res = await fetch("https://api.line.me/v2/bot/message/push", {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ Authorization: `Bearer ${channelAccessToken}`,
43
+ },
44
+ body: JSON.stringify({
45
+ to: userId,
46
+ messages: messages.slice(i, i + 5),
47
+ }),
48
+ signal: AbortSignal.timeout(30_000),
49
+ });
50
+ if (!res.ok) {
51
+ const body = await res.text().catch(() => "");
52
+ console.error(`[line] pushMessage failed: ${res.status} ${body.slice(0, 200)}`);
53
+ }
54
+ }
55
+ catch (err) {
56
+ console.error(`[line] pushMessage network error: ${err}`);
57
+ }
58
+ }
59
+ }
60
+ function chunkText(text, max = 5000) {
61
+ if (text.length === 0)
62
+ return ["(empty reply)"];
63
+ const chunks = [];
64
+ for (let i = 0; i < text.length; i += max) {
65
+ chunks.push(text.slice(i, i + max));
66
+ }
67
+ return chunks;
68
+ }
69
+ // ── Signature verification ──────────────────────────────────────
70
+ function verifySignature(body, signature) {
71
+ const expected = crypto
72
+ .createHmac("SHA256", channelSecret)
73
+ .update(body)
74
+ .digest("base64");
75
+ if (expected.length !== signature.length)
76
+ return false;
77
+ return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
78
+ }
79
+ // ── Webhook server ──────────────────────────────────────────────
80
+ const app = express();
81
+ app.disable("x-powered-by");
82
+ app.use(express.text({ type: "application/json" }));
83
+ app.post("/webhook", async (req, res) => {
84
+ const signature = req.headers["x-line-signature"];
85
+ const bodyStr = req.body;
86
+ if (!signature || !verifySignature(bodyStr, signature)) {
87
+ res.status(401).send("Invalid signature");
88
+ return;
89
+ }
90
+ res.status(200).send("OK");
91
+ let body;
92
+ try {
93
+ body = JSON.parse(bodyStr);
94
+ }
95
+ catch {
96
+ console.error("[line] malformed JSON in webhook body");
97
+ return;
98
+ }
99
+ for (const event of body.events) {
100
+ if (event.type !== "message" || event.message?.type !== "text")
101
+ continue;
102
+ const userId = event.source?.userId;
103
+ const text = event.message?.text ?? "";
104
+ if (!userId || !text.trim())
105
+ continue;
106
+ console.log(`[line] message user=${userId} len=${text.length}`);
107
+ try {
108
+ const ack = await client.send(userId, text);
109
+ if (ack.ok) {
110
+ await pushMessage(userId, ack.reply ?? "");
111
+ }
112
+ else {
113
+ const status = ack.status ? ` (${ack.status})` : "";
114
+ await pushMessage(userId, `Error${status}: ${ack.error ?? "unknown"}`);
115
+ }
116
+ }
117
+ catch (err) {
118
+ console.error(`[line] message handling failed: ${err}`);
119
+ }
120
+ }
121
+ });
122
+ app.listen(PORT, () => {
123
+ console.log("MulmoClaude LINE bridge");
124
+ console.log(`Webhook listening on http://localhost:${PORT}/webhook`);
125
+ console.log("Set your LINE webhook URL to: <public-url>/webhook");
126
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@mulmobridge/line",
3
+ "version": "0.1.0",
4
+ "description": "LINE bridge for MulmoBridge — connect a LINE bot to MulmoClaude",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "mulmobridge-line": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepack": "yarn build",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "echo no tests yet"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Receptron Team",
23
+ "dependencies": {
24
+ "@mulmobridge/client": "^0.1.0",
25
+ "@mulmobridge/protocol": "^0.1.0",
26
+ "dotenv": "^17.0.0",
27
+ "express": "^5.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/express": "^5.0.0",
31
+ "@types/node": "^25.6.0",
32
+ "typescript": "^6.0.3"
33
+ }
34
+ }