@mulmobridge/chat-service 0.1.2 → 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/README.md +41 -0
- package/dist/commands.d.ts +12 -1
- package/dist/commands.js +59 -17
- package/dist/index.js +1 -0
- package/dist/relay.d.ts +1 -1
- package/dist/relay.js +11 -2
- package/dist/socket.js +1 -1
- package/dist/types.d.ts +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,47 @@ chatService.attachSocket(httpServer);
|
|
|
73
73
|
| [@mulmobridge/cli](https://www.npmjs.com/package/@mulmobridge/cli) | CLI bridge |
|
|
74
74
|
| [@mulmobridge/telegram](https://www.npmjs.com/package/@mulmobridge/telegram) | Telegram bridge |
|
|
75
75
|
|
|
76
|
+
## Ecosystem
|
|
77
|
+
|
|
78
|
+
Part of the [`@mulmobridge/*`](https://www.npmjs.com/~mulmobridge) package family.
|
|
79
|
+
|
|
80
|
+
**Shared libraries:**
|
|
81
|
+
|
|
82
|
+
- [`@mulmobridge/client`](https://www.npmjs.com/package/@mulmobridge/client) — socket.io client library used by every bridge below
|
|
83
|
+
- [`@mulmobridge/protocol`](https://www.npmjs.com/package/@mulmobridge/protocol) — wire types and constants
|
|
84
|
+
- [`@mulmobridge/chat-service`](https://www.npmjs.com/package/@mulmobridge/chat-service) — server-side relay + session store ← **this package**
|
|
85
|
+
- [`@mulmobridge/relay`](https://www.npmjs.com/package/@mulmobridge/relay) — Cloudflare Workers webhook proxy
|
|
86
|
+
- [`@mulmobridge/mock-server`](https://www.npmjs.com/package/@mulmobridge/mock-server) — mock server for local bridge development
|
|
87
|
+
|
|
88
|
+
**Bridges** (one npm package per platform):
|
|
89
|
+
|
|
90
|
+
- [`@mulmobridge/bluesky`](https://www.npmjs.com/package/@mulmobridge/bluesky) — Bluesky DMs over atproto
|
|
91
|
+
- [`@mulmobridge/chatwork`](https://www.npmjs.com/package/@mulmobridge/chatwork) — Chatwork (Japanese business chat)
|
|
92
|
+
- [`@mulmobridge/cli`](https://www.npmjs.com/package/@mulmobridge/cli) — interactive terminal bridge
|
|
93
|
+
- [`@mulmobridge/discord`](https://www.npmjs.com/package/@mulmobridge/discord) — Discord bot via Gateway
|
|
94
|
+
- [`@mulmobridge/email`](https://www.npmjs.com/package/@mulmobridge/email) — IMAP poll + SMTP reply, threading preserved
|
|
95
|
+
- [`@mulmobridge/google-chat`](https://www.npmjs.com/package/@mulmobridge/google-chat) — Google Chat via MulmoBridge relay
|
|
96
|
+
- [`@mulmobridge/irc`](https://www.npmjs.com/package/@mulmobridge/irc) — IRC (Libera, Freenode, custom)
|
|
97
|
+
- [`@mulmobridge/line`](https://www.npmjs.com/package/@mulmobridge/line) — LINE Messaging API via MulmoBridge relay
|
|
98
|
+
- [`@mulmobridge/line-works`](https://www.npmjs.com/package/@mulmobridge/line-works) — LINE Works (enterprise LINE)
|
|
99
|
+
- [`@mulmobridge/mastodon`](https://www.npmjs.com/package/@mulmobridge/mastodon) — Mastodon DMs + mentions
|
|
100
|
+
- [`@mulmobridge/matrix`](https://www.npmjs.com/package/@mulmobridge/matrix) — Matrix / Element
|
|
101
|
+
- [`@mulmobridge/mattermost`](https://www.npmjs.com/package/@mulmobridge/mattermost) — Mattermost
|
|
102
|
+
- [`@mulmobridge/messenger`](https://www.npmjs.com/package/@mulmobridge/messenger) — Facebook Messenger via MulmoBridge relay
|
|
103
|
+
- [`@mulmobridge/nostr`](https://www.npmjs.com/package/@mulmobridge/nostr) — Nostr NIP-04 encrypted DMs
|
|
104
|
+
- [`@mulmobridge/rocketchat`](https://www.npmjs.com/package/@mulmobridge/rocketchat) — Rocket.Chat
|
|
105
|
+
- [`@mulmobridge/signal`](https://www.npmjs.com/package/@mulmobridge/signal) — Signal via signal-cli-rest-api
|
|
106
|
+
- [`@mulmobridge/slack`](https://www.npmjs.com/package/@mulmobridge/slack) — Slack Socket Mode
|
|
107
|
+
- [`@mulmobridge/teams`](https://www.npmjs.com/package/@mulmobridge/teams) — Microsoft Teams via Bot Framework
|
|
108
|
+
- [`@mulmobridge/telegram`](https://www.npmjs.com/package/@mulmobridge/telegram) — Telegram bot
|
|
109
|
+
- [`@mulmobridge/twilio-sms`](https://www.npmjs.com/package/@mulmobridge/twilio-sms) — SMS via Twilio Programmable Messaging
|
|
110
|
+
- [`@mulmobridge/viber`](https://www.npmjs.com/package/@mulmobridge/viber) — Viber Public Account bots
|
|
111
|
+
- [`@mulmobridge/webhook`](https://www.npmjs.com/package/@mulmobridge/webhook) — generic HTTP webhook bridge
|
|
112
|
+
- [`@mulmobridge/whatsapp`](https://www.npmjs.com/package/@mulmobridge/whatsapp) — WhatsApp Cloud API via MulmoBridge relay
|
|
113
|
+
- [`@mulmobridge/xmpp`](https://www.npmjs.com/package/@mulmobridge/xmpp) — XMPP / Jabber
|
|
114
|
+
- [`@mulmobridge/zulip`](https://www.npmjs.com/package/@mulmobridge/zulip) — Zulip
|
|
115
|
+
|
|
116
|
+
|
|
76
117
|
## License
|
|
77
118
|
|
|
78
119
|
MIT — [Receptron Team](https://github.com/receptron)
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { Role, SessionSummary } from "./types.js";
|
|
1
|
+
import type { BridgeSkillSummary, Role, SessionSummary } from "./types.js";
|
|
2
2
|
import type { ChatStateStore, TransportChatState } from "./chat-state.js";
|
|
3
3
|
export interface CommandResult {
|
|
4
4
|
reply: string;
|
|
5
5
|
nextState?: TransportChatState;
|
|
6
|
+
/** When set, the relay must NOT short-circuit with `reply`. It
|
|
7
|
+
* adopts `nextState` as the active chat state and forwards
|
|
8
|
+
* `forwardAs` to the agent as the user message. Used by the
|
|
9
|
+
* `//{skill}` shortcut: reset + run skill in one bridge turn. */
|
|
10
|
+
forwardAs?: string;
|
|
6
11
|
}
|
|
7
12
|
export type CommandHandler = (text: string, transportId: string, chatState: TransportChatState) => Promise<CommandResult | null>;
|
|
8
13
|
export declare function createCommandHandler(opts: {
|
|
@@ -27,4 +32,10 @@ export declare function createCommandHandler(opts: {
|
|
|
27
32
|
}>;
|
|
28
33
|
total: number;
|
|
29
34
|
}>;
|
|
35
|
+
/** Lists the skills the bridge command handler should expose.
|
|
36
|
+
* Drives both the slash-command allowlist (only matching names
|
|
37
|
+
* are forwarded to the agent) and the "Skills:" section in the
|
|
38
|
+
* `/help` reply. When omitted, every unknown slash is rejected
|
|
39
|
+
* and `/help` shows only the built-in commands. */
|
|
40
|
+
listRegisteredSkills?: () => Promise<BridgeSkillSummary[]>;
|
|
30
41
|
}): CommandHandler;
|
package/dist/commands.js
CHANGED
|
@@ -24,7 +24,7 @@ function formatRelativeTime(isoDate) {
|
|
|
24
24
|
}
|
|
25
25
|
// ── Factory ──────────────────────────────────────────────────
|
|
26
26
|
export function createCommandHandler(opts) {
|
|
27
|
-
const { loadAllRoles, getRole, resetChatState, connectSession, listSessions, getSessionHistory } = opts;
|
|
27
|
+
const { loadAllRoles, getRole, resetChatState, connectSession, listSessions, getSessionHistory, listRegisteredSkills } = opts;
|
|
28
28
|
// Cache /sessions results per chat so /switch resolves to the correct list.
|
|
29
29
|
// Key: "transportId:externalChatId". Bounded with max entries + TTL.
|
|
30
30
|
// See docs/bridge-session-design.md for multi-user scaling plan.
|
|
@@ -51,19 +51,30 @@ export function createCommandHandler(opts) {
|
|
|
51
51
|
sessionListCache.set(key, { sessions, createdAt: Date.now() });
|
|
52
52
|
}
|
|
53
53
|
const getRolesText = () => ["Available roles:", ...loadAllRoles().map((r) => ` ${r.id} — ${r.name}`)].join("\n");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
// Built each time so `/help` reflects the live skill list. The
|
|
55
|
+
// `skills` argument is fetched once per command turn (see the
|
|
56
|
+
// `default:` branch and `case "/help"`) so we don't fs-scan twice
|
|
57
|
+
// when the handler both checks membership and renders help.
|
|
58
|
+
const buildHelpText = (skills) => {
|
|
59
|
+
const lines = [
|
|
60
|
+
"Commands:",
|
|
61
|
+
" /reset — Start a new session",
|
|
62
|
+
" /sessions [page] — List recent sessions (e.g. /sessions 2)",
|
|
63
|
+
" /switch <number|sessionId> — Switch to a session",
|
|
64
|
+
" /history [page] — Show recent messages in current session",
|
|
65
|
+
" /help — Show this help",
|
|
66
|
+
" /roles — List available roles",
|
|
67
|
+
" /role <id> — Switch role",
|
|
68
|
+
" /status — Show current session info",
|
|
69
|
+
];
|
|
70
|
+
if (skills.length > 0) {
|
|
71
|
+
lines.push("", "Skills:", ...skills.map((s) => ` /${s.name} — ${s.description}`));
|
|
72
|
+
lines.push("", "Tip: //<skill> [args...] starts a fresh session and runs the skill in one shot.");
|
|
73
|
+
}
|
|
74
|
+
lines.push("", "Send any other text to chat with the assistant.");
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
};
|
|
77
|
+
const fetchSkills = async () => (listRegisteredSkills ? await listRegisteredSkills() : []);
|
|
67
78
|
const handleReset = async (transportId, chatState) => {
|
|
68
79
|
const nextState = await resetChatState(transportId, chatState.externalChatId, chatState.roleId);
|
|
69
80
|
return {
|
|
@@ -211,6 +222,25 @@ export function createCommandHandler(opts) {
|
|
|
211
222
|
const handleCommand = async (text, transportId, chatState) => {
|
|
212
223
|
if (!text.startsWith("/"))
|
|
213
224
|
return null;
|
|
225
|
+
// `//{skill} [args...]` shortcut — start a new session AND run
|
|
226
|
+
// the skill in one bridge turn. Args after the skill name are
|
|
227
|
+
// forwarded verbatim, so `//mag2 https://x.com/post` resets and
|
|
228
|
+
// runs `/mag2 https://x.com/post`.
|
|
229
|
+
if (text.startsWith("//")) {
|
|
230
|
+
const skills = await fetchSkills();
|
|
231
|
+
const [head, ...rest] = text.split(/\s+/);
|
|
232
|
+
const skillName = head.slice(2);
|
|
233
|
+
if (skillName && skills.some((s) => s.name === skillName)) {
|
|
234
|
+
const nextState = await resetChatState(transportId, chatState.externalChatId, chatState.roleId);
|
|
235
|
+
const forwardAs = rest.length > 0 ? `/${skillName} ${rest.join(" ")}` : `/${skillName}`;
|
|
236
|
+
return {
|
|
237
|
+
reply: `Session reset. Running ${forwardAs}`,
|
|
238
|
+
nextState,
|
|
239
|
+
forwardAs,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return { reply: `Unknown command: ${text}\n\n${buildHelpText(skills)}` };
|
|
243
|
+
}
|
|
214
244
|
const [command, ...args] = text.split(/\s+/);
|
|
215
245
|
switch (command) {
|
|
216
246
|
case "/reset":
|
|
@@ -222,15 +252,27 @@ export function createCommandHandler(opts) {
|
|
|
222
252
|
case "/history":
|
|
223
253
|
return handleHistory(chatState, args[0]);
|
|
224
254
|
case "/help":
|
|
225
|
-
return { reply:
|
|
255
|
+
return { reply: buildHelpText(await fetchSkills()) };
|
|
226
256
|
case "/roles":
|
|
227
257
|
return { reply: getRolesText() };
|
|
228
258
|
case "/role":
|
|
229
259
|
return handleRole(transportId, chatState, args[0]);
|
|
230
260
|
case "/status":
|
|
231
261
|
return handleStatus(chatState);
|
|
232
|
-
default:
|
|
233
|
-
|
|
262
|
+
default: {
|
|
263
|
+
// Forward to the agent only if the command names a registered
|
|
264
|
+
// skill; otherwise reply with the standard "Unknown command"
|
|
265
|
+
// help. We deliberately do NOT pass arbitrary slash text
|
|
266
|
+
// through, so a typo can't accidentally invoke the agent and
|
|
267
|
+
// a slash that doesn't match anything stays a transport-level
|
|
268
|
+
// error. Reuse the same skill list for the membership check
|
|
269
|
+
// and the help text to avoid scanning the skills dir twice.
|
|
270
|
+
const skills = await fetchSkills();
|
|
271
|
+
const skillName = command.slice(1);
|
|
272
|
+
if (skillName && skills.some((s) => s.name === skillName))
|
|
273
|
+
return null;
|
|
274
|
+
return { reply: `Unknown command: ${command}\n\n${buildHelpText(skills)}` };
|
|
275
|
+
}
|
|
234
276
|
}
|
|
235
277
|
};
|
|
236
278
|
return handleCommand;
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export function createChatService(deps) {
|
|
|
42
42
|
connectSession: store.connectSession,
|
|
43
43
|
listSessions: deps.listSessions,
|
|
44
44
|
getSessionHistory: deps.getSessionHistory,
|
|
45
|
+
listRegisteredSkills: deps.listRegisteredSkills,
|
|
45
46
|
});
|
|
46
47
|
const relay = createRelay({
|
|
47
48
|
store,
|
package/dist/relay.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface RelayParams {
|
|
|
10
10
|
* number / boolean values only — see socket.ts sanitiser).
|
|
11
11
|
* Forwarded to the host app's startChat callback as
|
|
12
12
|
* `bridgeOptions`. Empty when the bridge didn't send any.
|
|
13
|
-
* See plans/feat-bridge-options-passthrough.md. */
|
|
13
|
+
* See plans/done/feat-bridge-options-passthrough.md. */
|
|
14
14
|
bridgeOptions?: Readonly<Record<string, string | number | boolean>>;
|
|
15
15
|
/** Called for each text chunk as the agent generates it. Used by
|
|
16
16
|
* the socket transport to stream text to the bridge in real time
|
package/dist/relay.js
CHANGED
|
@@ -13,7 +13,8 @@ const REPLY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
13
13
|
export function createRelay(deps) {
|
|
14
14
|
const { store, handleCommand, startChat, onSessionEvent, getRole, defaultRoleId, logger } = deps;
|
|
15
15
|
return async function relayMessage(params) {
|
|
16
|
-
const { transportId, externalChatId,
|
|
16
|
+
const { transportId, externalChatId, attachments, bridgeOptions } = params;
|
|
17
|
+
let { text } = params;
|
|
17
18
|
// Log attachment summary (count + mimeTypes) — NEVER log raw
|
|
18
19
|
// base64 data (performance, log size, information leak risk).
|
|
19
20
|
const attachmentSummary = attachments
|
|
@@ -40,7 +41,15 @@ export function createRelay(deps) {
|
|
|
40
41
|
}
|
|
41
42
|
const commandResult = await handleCommand(text, transportId, chatState);
|
|
42
43
|
if (commandResult) {
|
|
43
|
-
|
|
44
|
+
// `forwardAs` means "reset/mutate state AND continue into the
|
|
45
|
+
// agent with rewritten text" (see //{skill} shortcut). Without
|
|
46
|
+
// it, short-circuit with the canned reply.
|
|
47
|
+
if (!commandResult.forwardAs) {
|
|
48
|
+
return { kind: "ok", reply: commandResult.reply };
|
|
49
|
+
}
|
|
50
|
+
if (commandResult.nextState)
|
|
51
|
+
chatState = commandResult.nextState;
|
|
52
|
+
text = commandResult.forwardAs;
|
|
44
53
|
}
|
|
45
54
|
const result = await startChat({
|
|
46
55
|
message: text,
|
package/dist/socket.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// C — streaming text chunks via `reply.chunk`
|
|
25
25
|
// D — HTTP endpoint deprecation
|
|
26
26
|
//
|
|
27
|
-
// See plans/feat-chat-socketio.md and plans/feat-chat-socketio-phase-b.md.
|
|
27
|
+
// See plans/done/feat-chat-socketio.md and plans/done/feat-chat-socketio-phase-b.md.
|
|
28
28
|
import { Server as SocketServer } from "socket.io";
|
|
29
29
|
export const CHAT_SOCKET_PATH = "/ws/chat";
|
|
30
30
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -99,4 +99,26 @@ export interface ChatServiceDeps {
|
|
|
99
99
|
}>;
|
|
100
100
|
total: number;
|
|
101
101
|
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Return the skills the bridge command handler should expose. The
|
|
104
|
+
* handler uses the result for two things:
|
|
105
|
+
* (1) Decide whether an unknown bridge slash command (e.g. `/foo`
|
|
106
|
+
* from Telegram) names a registered skill — only matching
|
|
107
|
+
* names are forwarded to the agent so the Claude CLI's
|
|
108
|
+
* slash-command resolver runs the skill. Non-matches stay a
|
|
109
|
+
* transport-level "Unknown command" reply.
|
|
110
|
+
* (2) Render a "Skills:" section in the bridge `/help` text and
|
|
111
|
+
* in the "Unknown command" fallback so a bridge user can
|
|
112
|
+
* discover what skills exist without leaving the chat.
|
|
113
|
+
* When omitted, every unknown slash is rejected and `/help` shows
|
|
114
|
+
* only the built-in commands.
|
|
115
|
+
*/
|
|
116
|
+
listRegisteredSkills?: () => Promise<BridgeSkillSummary[]>;
|
|
117
|
+
}
|
|
118
|
+
/** Minimal skill info the bridge command handler needs to render the
|
|
119
|
+
* `/help` text and decide whether a slash command should be forwarded
|
|
120
|
+
* to the agent. Sourced from SKILL.md frontmatter on the host side. */
|
|
121
|
+
export interface BridgeSkillSummary {
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
102
124
|
}
|
package/package.json
CHANGED