@mulmobridge/chat-service 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/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)
@@ -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
- const getHelpText = () => [
55
- "Commands:",
56
- " /reset — Start a new session",
57
- " /sessions [page] — List recent sessions (e.g. /sessions 2)",
58
- " /switch <number|sessionId> — Switch to a session",
59
- " /history [page] — Show recent messages in current session",
60
- " /help — Show this help",
61
- " /roles — List available roles",
62
- " /role <id> — Switch role",
63
- " /status — Show current session info",
64
- "",
65
- "Send any other text to chat with the assistant.",
66
- ].join("\n");
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: getHelpText() };
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
- return { reply: `Unknown command: ${command}\n\n${getHelpText()}` };
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
@@ -6,6 +6,12 @@ export interface RelayParams {
6
6
  externalChatId: string;
7
7
  text: string;
8
8
  attachments?: Attachment[];
9
+ /** Flat primitive bag captured at handshake time (string /
10
+ * number / boolean values only — see socket.ts sanitiser).
11
+ * Forwarded to the host app's startChat callback as
12
+ * `bridgeOptions`. Empty when the bridge didn't send any.
13
+ * See plans/done/feat-bridge-options-passthrough.md. */
14
+ bridgeOptions?: Readonly<Record<string, string | number | boolean>>;
9
15
  /** Called for each text chunk as the agent generates it. Used by
10
16
  * the socket transport to stream text to the bridge in real time
11
17
  * (Phase C of #268). */
@@ -30,3 +36,4 @@ export interface RelayDeps {
30
36
  logger: Logger;
31
37
  }
32
38
  export declare function createRelay(deps: RelayDeps): RelayFn;
39
+ export declare function resolveDefaultRole(bridgeOptions: Readonly<Record<string, string | number | boolean>> | undefined, getRole: (roleId: string) => Role, fallbackRoleId: string, logger: Logger, transportId: string): string;
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, text, attachments } = params;
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
@@ -30,12 +31,25 @@ export function createRelay(deps) {
30
31
  });
31
32
  let chatState = await store.getChatState(transportId, externalChatId);
32
33
  if (!chatState) {
33
- const defaultRole = getRole(defaultRoleId);
34
- chatState = await store.resetChatState(transportId, externalChatId, defaultRole.id);
34
+ // Only on FIRST contact do we honour `bridgeOptions.defaultRole`
35
+ // — once the session exists, whatever role the user / command
36
+ // handler settled on is the source of truth. An unknown role
37
+ // id silently falls back to the host-app default (we log it so
38
+ // a typo in the bridge's env var is discoverable).
39
+ const resolved = resolveDefaultRole(bridgeOptions, getRole, defaultRoleId, logger, transportId);
40
+ chatState = await store.resetChatState(transportId, externalChatId, resolved);
35
41
  }
36
42
  const commandResult = await handleCommand(text, transportId, chatState);
37
43
  if (commandResult) {
38
- return { kind: "ok", reply: commandResult.reply };
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;
39
53
  }
40
54
  const result = await startChat({
41
55
  message: text,
@@ -43,6 +57,9 @@ export function createRelay(deps) {
43
57
  chatSessionId: chatState.sessionId,
44
58
  attachments,
45
59
  origin: "bridge",
60
+ // Host app may use other keys (e.g. a future `defaultModel`);
61
+ // we forward the whole bag untouched.
62
+ bridgeOptions,
46
63
  });
47
64
  if (result.kind === "error") {
48
65
  const status = result.status ?? 500;
@@ -90,6 +107,28 @@ export function createRelay(deps) {
90
107
  };
91
108
  }
92
109
  // ── Internals ────────────────────────────────────────────────
110
+ // Resolve the role id to seed a NEW bridge chat state with. Prefers
111
+ // `bridgeOptions.defaultRole` when the bridge sent one and it names
112
+ // a role the host app actually has. Falls back to the host-app
113
+ // default on absence / unknown id (with a warn log so an env-var
114
+ // typo is traceable). Exported for direct unit testing.
115
+ export function resolveDefaultRole(bridgeOptions, getRole, fallbackRoleId, logger, transportId) {
116
+ const requested = bridgeOptions?.defaultRole;
117
+ if (typeof requested !== "string" || requested.length === 0)
118
+ return fallbackRoleId;
119
+ // `getRole` on an unknown id silently returns the first built-in
120
+ // role — compare ids to catch that before we commit to it.
121
+ const resolved = getRole(requested);
122
+ if (resolved.id !== requested) {
123
+ logger.warn("chat-service", "bridge requested unknown default role; falling back", {
124
+ transportId,
125
+ requested,
126
+ fallback: fallbackRoleId,
127
+ });
128
+ return fallbackRoleId;
129
+ }
130
+ return resolved.id;
131
+ }
93
132
  // Kept out of the factory closure so future packaging doesn't need
94
133
  // to re-capture anything; `onSessionEvent` arrives as a plain param.
95
134
  function collectAgentReply(onSessionEvent, chatSessionId, onChunk) {
package/dist/socket.d.ts CHANGED
@@ -39,5 +39,8 @@ export interface ChatSocketHandle {
39
39
  * joiner. */
40
40
  pushToBridge: PushFn;
41
41
  }
42
+ type BridgeOptions = Readonly<Record<string, string | number | boolean>>;
42
43
  export declare function bridgeRoom(transportId: string): string;
43
44
  export declare function attachChatSocket(server: http.Server, deps: ChatSocketDeps): ChatSocketHandle;
45
+ export declare function sanitiseOptions(raw: unknown): BridgeOptions;
46
+ export {};
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
  /**
@@ -63,6 +63,9 @@ export function attachChatSocket(server, deps) {
63
63
  return;
64
64
  }
65
65
  socket.data.transportId = result.transportId;
66
+ // Stash options on the socket so every subsequent message from
67
+ // this bridge inherits the same config without re-sending it.
68
+ socket.data.bridgeOptions = result.options;
66
69
  next();
67
70
  });
68
71
  io.on("connection", (socket) => {
@@ -116,6 +119,10 @@ export function attachChatSocket(server, deps) {
116
119
  externalChatId: parsed.externalChatId,
117
120
  text: parsed.text,
118
121
  attachments: parsed.attachments,
122
+ // Options captured at handshake time (see io.use). Empty
123
+ // object when the bridge didn't send any — the host app
124
+ // treats absence and empty identically.
125
+ bridgeOptions: socket.data.bridgeOptions ?? {},
119
126
  // Stream text chunks to this bridge socket in real time
120
127
  // (Phase C of #268). The ack still returns the full text
121
128
  // for backward compatibility.
@@ -151,6 +158,36 @@ export function attachChatSocket(server, deps) {
151
158
  };
152
159
  return { io, pushToBridge };
153
160
  }
161
+ // `options` on the handshake is reduced to a FLAT PRIMITIVE bag:
162
+ // string / number / boolean only. This serves two purposes —
163
+ // 1. Rejects nested objects / arrays / functions so a downstream
164
+ // merge in the host app can't reintroduce prototype-pollution
165
+ // via a deeply-nested `__proto__` (top-level `__proto__` is
166
+ // stripped separately as a belt-and-braces guard).
167
+ // 2. Makes the wire contract explicit: every value the host sees
168
+ // is something you could put in an env var.
169
+ // Anything else (objects, arrays, null, undefined, functions,
170
+ // symbols, bigints) is dropped silently. The bridge author is
171
+ // responsible for flattening complex config into primitives before
172
+ // sending.
173
+ export function sanitiseOptions(raw) {
174
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
175
+ return {};
176
+ const out = {};
177
+ for (const [key, value] of Object.entries(raw)) {
178
+ if (key === "__proto__" || key === "prototype" || key === "constructor")
179
+ continue;
180
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
181
+ // Drop non-finite numbers (NaN / Infinity) — they serialise
182
+ // oddly and the host app shouldn't be asked to distinguish
183
+ // "valid zero" from "broken input".
184
+ if (typeof value === "number" && !Number.isFinite(value))
185
+ continue;
186
+ out[key] = value;
187
+ }
188
+ }
189
+ return out;
190
+ }
154
191
  function validateHandshake(auth, tokenProvider) {
155
192
  if (!auth || typeof auth !== "object") {
156
193
  return { ok: false, error: "handshake auth is required" };
@@ -160,8 +197,9 @@ function validateHandshake(auth, tokenProvider) {
160
197
  return { ok: false, error: "transportId is required" };
161
198
  }
162
199
  const transportId = transportIdRaw.trim();
200
+ const options = sanitiseOptions(auth.options);
163
201
  if (!tokenProvider) {
164
- return { ok: true, transportId };
202
+ return { ok: true, transportId, options };
165
203
  }
166
204
  const expected = tokenProvider();
167
205
  if (expected === null || expected.length === 0) {
@@ -177,7 +215,7 @@ function validateHandshake(auth, tokenProvider) {
177
215
  if (provided !== expected) {
178
216
  return { ok: false, error: "invalid token" };
179
217
  }
180
- return { ok: true, transportId };
218
+ return { ok: true, transportId, options };
181
219
  }
182
220
  function parseMessagePayload(payload) {
183
221
  if (!payload || typeof payload !== "object") {
package/dist/types.d.ts CHANGED
@@ -25,6 +25,14 @@ export interface StartChatParams {
25
25
  attachments?: Attachment[];
26
26
  /** Session origin — application-defined (e.g. "human", "bridge") */
27
27
  origin?: string;
28
+ /** Flat primitive bag forwarded from the bridge handshake. Chat-
29
+ * service sanitises to string / number / boolean values only
30
+ * before this point — nested objects are dropped at the socket
31
+ * boundary so a downstream merge can't reintroduce prototype-
32
+ * pollution. The host app is free to look up its own keys
33
+ * (e.g. `defaultRole`). Empty object when the bridge didn't
34
+ * send any. */
35
+ bridgeOptions?: Readonly<Record<string, string | number | boolean>>;
28
36
  }
29
37
  export type StartChatResult = {
30
38
  kind: "started";
@@ -91,4 +99,26 @@ export interface ChatServiceDeps {
91
99
  }>;
92
100
  total: number;
93
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;
94
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmobridge/chat-service",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Server-side chat service for MulmoBridge — socket.io + REST bridge to Claude Code agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "license": "MIT",
28
28
  "author": "Receptron Team",
29
29
  "dependencies": {
30
- "@mulmobridge/protocol": "^0.1.3"
30
+ "@mulmobridge/protocol": "^0.1.4"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "express": "^5.0.0",