@alfe.ai/openclaw-chat 0.0.14 → 0.0.16

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/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ const require_plugin = require("./plugin2.cjs");
6
+ exports.createAlfeChannelPlugin = require_plugin.createAlfeChannelPlugin;
7
+ exports.default = require_plugin.plugin;
@@ -0,0 +1,42 @@
1
+ import { a as AlfeResolvedAccount, i as AlfePluginConfig, n as createAlfeChannelPlugin, r as AlfeChannelConfig, t as plugin } from "./plugin.cjs";
2
+
3
+ //#region src/session-store.d.ts
4
+
5
+ /**
6
+ * Session Store — persists chat sessions to the local filesystem.
7
+ *
8
+ * Storage layout:
9
+ * ~/.alfe/sessions/chat/{sessionId}.json
10
+ *
11
+ * Each session file contains metadata and the full message history.
12
+ * Sessions are written on every message to ensure durability.
13
+ * Old sessions are cleaned up automatically (30-day TTL, 1000 max).
14
+ */
15
+ interface ChatMessage {
16
+ role: 'user' | 'assistant';
17
+ content: string;
18
+ timestamp: number;
19
+ senderId?: string;
20
+ senderName?: string;
21
+ }
22
+ interface SessionData {
23
+ sessionId: string;
24
+ agentId: string;
25
+ channel: string;
26
+ tenantId?: string;
27
+ userId?: string;
28
+ createdAt: string;
29
+ updatedAt: string;
30
+ messages: ChatMessage[];
31
+ }
32
+ interface SessionSummary {
33
+ sessionId: string;
34
+ agentId: string;
35
+ channel: string;
36
+ createdAt: string;
37
+ lastMessageAt?: string;
38
+ preview?: string;
39
+ messageCount: number;
40
+ }
41
+ //#endregion
42
+ export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
@@ -0,0 +1,2 @@
1
+ const require_plugin = require("./plugin2.cjs");
2
+ module.exports = require_plugin.plugin;
@@ -0,0 +1,242 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Types for the Alfe chat channel plugin.
4
+ *
5
+ * SDK types (PluginRuntime, OpenClawPluginApi, OpenClawConfig, etc.)
6
+ * are imported from openclaw/plugin-sdk at usage sites.
7
+ * This file only contains Alfe-specific domain types.
8
+ */
9
+ interface AlfeChannelAccountConfig {
10
+ /** Whether this account is enabled. */
11
+ enabled?: boolean;
12
+ /** Allowed sender identifiers (user IDs, email addresses). */
13
+ allowFrom?: string | string[];
14
+ /** Default delivery target. */
15
+ defaultTo?: string;
16
+ /** DM policy (open, allowlist, etc.). */
17
+ dmPolicy?: string;
18
+ }
19
+ interface AlfeChannelConfig {
20
+ /** Whether the Alfe channel is enabled. */
21
+ enabled?: boolean;
22
+ /** Allowed sender identifiers. */
23
+ allowFrom?: string | string[];
24
+ /** Default delivery target for outbound messages. */
25
+ defaultTo?: string;
26
+ /** DM policy. */
27
+ dmPolicy?: string;
28
+ /** Named accounts (multi-account support). */
29
+ accounts?: Record<string, AlfeChannelAccountConfig>;
30
+ }
31
+ interface AlfeResolvedAccount {
32
+ accountId: string;
33
+ enabled: boolean;
34
+ allowFrom: string[];
35
+ defaultTo?: string;
36
+ dmPolicy?: string;
37
+ }
38
+ interface AlfePluginConfig {
39
+ /** Agent ID this plugin is associated with. */
40
+ agentId?: string;
41
+ /** Chat service WebSocket URL (e.g. wss://chat.dev.alfe.ai/ws) */
42
+ chatWsUrl?: string;
43
+ /** API key for chat service auth */
44
+ apiKey?: string;
45
+ }
46
+ //#endregion
47
+ //#region src/alfe-channel.d.ts
48
+ /** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
49
+ interface OpenClawConfig {
50
+ channels?: {
51
+ alfe?: AlfeChannelConfig;
52
+ [key: string]: unknown;
53
+ };
54
+ [key: string]: unknown;
55
+ }
56
+ /**
57
+ * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
58
+ *
59
+ * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
60
+ * but is registered dynamically via api.registerChannel().
61
+ */
62
+ declare function createAlfeChannelPlugin(): {
63
+ id: string;
64
+ meta: {
65
+ id: string;
66
+ label: string;
67
+ selectionLabel: string;
68
+ detailLabel: string;
69
+ docsPath: string;
70
+ docsLabel: string;
71
+ blurb: string;
72
+ systemImage: string;
73
+ order: number;
74
+ aliases: string[];
75
+ forceAccountBinding: boolean;
76
+ showConfigured: boolean;
77
+ };
78
+ capabilities: {
79
+ chatTypes: ("direct" | "group")[];
80
+ reactions: boolean;
81
+ edit: boolean;
82
+ unsend: boolean;
83
+ reply: boolean;
84
+ effects: boolean;
85
+ groupManagement: boolean;
86
+ threads: boolean;
87
+ media: boolean;
88
+ nativeCommands: boolean;
89
+ polls: boolean;
90
+ };
91
+ config: {
92
+ /**
93
+ * List configured account IDs.
94
+ * Supports multi-account via channels.alfe.accounts, with a
95
+ * default account derived from the top-level channels.alfe section.
96
+ */
97
+ listAccountIds(cfg: OpenClawConfig): string[];
98
+ /**
99
+ * Resolve account config for a given account ID.
100
+ */
101
+ resolveAccount(cfg: OpenClawConfig, accountId?: string | null): AlfeResolvedAccount;
102
+ /**
103
+ * Default account ID.
104
+ */
105
+ defaultAccountId(): string;
106
+ /**
107
+ * Check if account is enabled.
108
+ */
109
+ isEnabled(account: AlfeResolvedAccount): boolean;
110
+ /**
111
+ * Check if account is configured (always true for Alfe — no external tokens needed).
112
+ */
113
+ isConfigured(): boolean;
114
+ /**
115
+ * Describe the account state for status display.
116
+ */
117
+ describeAccount(account: AlfeResolvedAccount): {
118
+ accountId: string;
119
+ enabled: boolean;
120
+ configured: boolean;
121
+ dmPolicy: string | undefined;
122
+ };
123
+ /**
124
+ * Resolve allow-from list for an account.
125
+ */
126
+ resolveAllowFrom(params: {
127
+ cfg: OpenClawConfig;
128
+ accountId?: string | null;
129
+ }): string[];
130
+ /**
131
+ * Resolve default outbound target.
132
+ */
133
+ resolveDefaultTo(params: {
134
+ cfg: OpenClawConfig;
135
+ accountId?: string | null;
136
+ }): string | undefined;
137
+ };
138
+ /**
139
+ * Outbound delivery via gateway.
140
+ * The chat relay service on Fly.io handles actual delivery
141
+ * to connected web/mobile clients via the gateway.
142
+ */
143
+ outbound: {
144
+ deliveryMode: "gateway";
145
+ textChunkLimit: number;
146
+ };
147
+ /**
148
+ * Setup adapter — minimal for Alfe since no external tokens are needed.
149
+ */
150
+ setup: {
151
+ resolveAccountId(params: {
152
+ cfg: OpenClawConfig;
153
+ accountId?: string;
154
+ input?: Record<string, unknown>;
155
+ }): string;
156
+ applyAccountConfig(params: {
157
+ cfg: OpenClawConfig;
158
+ accountId: string;
159
+ input: Record<string, unknown>;
160
+ }): OpenClawConfig;
161
+ };
162
+ };
163
+ //#endregion
164
+ //#region src/plugin.d.ts
165
+ interface PluginLogger {
166
+ info(msg: string, ...args: unknown[]): void;
167
+ warn(msg: string, ...args: unknown[]): void;
168
+ error(msg: string, ...args: unknown[]): void;
169
+ debug(msg: string, ...args: unknown[]): void;
170
+ }
171
+ interface PluginRuntime {
172
+ config: {
173
+ loadConfig(): Record<string, unknown>;
174
+ };
175
+ events: {
176
+ onAgentEvent(listener: (evt: AgentEventPayload) => void): () => void;
177
+ };
178
+ subagent: {
179
+ run(params: {
180
+ sessionKey: string;
181
+ message: string;
182
+ idempotencyKey?: string;
183
+ deliver?: boolean;
184
+ }): Promise<{
185
+ runId: string;
186
+ }>;
187
+ waitForRun(params: {
188
+ runId: string;
189
+ timeoutMs?: number;
190
+ }): Promise<{
191
+ status: string;
192
+ error?: string;
193
+ }>;
194
+ };
195
+ channel: {
196
+ routing: {
197
+ resolveAgentRoute(input: Record<string, unknown>): Record<string, unknown>;
198
+ buildAgentSessionKey(params: Record<string, unknown>): string;
199
+ };
200
+ session: {
201
+ resolveStorePath(params: Record<string, unknown>): string;
202
+ readSessionUpdatedAt(params: Record<string, unknown>): number | undefined;
203
+ recordInboundSession(params: Record<string, unknown>): Promise<void>;
204
+ };
205
+ reply: {
206
+ resolveEnvelopeFormatOptions(cfg: Record<string, unknown>): Record<string, unknown>;
207
+ formatAgentEnvelope(params: Record<string, unknown>): string;
208
+ finalizeInboundContext(params: Record<string, unknown>): Record<string, unknown>;
209
+ dispatchReplyWithBufferedBlockDispatcher(params: Record<string, unknown>): Promise<unknown>;
210
+ dispatchReplyFromConfig(params: Record<string, unknown>): Promise<unknown>;
211
+ };
212
+ [key: string]: unknown;
213
+ };
214
+ }
215
+ interface AgentEventPayload {
216
+ runId: string;
217
+ seq: number;
218
+ stream: string;
219
+ ts: number;
220
+ data: Record<string, unknown>;
221
+ sessionKey?: string;
222
+ }
223
+ interface PluginApi {
224
+ logger: PluginLogger;
225
+ config?: Record<string, unknown>;
226
+ runtime?: PluginRuntime;
227
+ registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
228
+ registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
229
+ on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
230
+ priority?: number;
231
+ }): void;
232
+ }
233
+ declare const plugin: {
234
+ id: string;
235
+ name: string;
236
+ description: string;
237
+ version: string;
238
+ activate(api: PluginApi): void;
239
+ deactivate(api: PluginApi): Promise<void>;
240
+ };
241
+ //#endregion
242
+ export { AlfeResolvedAccount as a, AlfePluginConfig as i, createAlfeChannelPlugin as n, AlfeChannelConfig as r, plugin as t };
@@ -0,0 +1,686 @@
1
+ let node_module = require("node:module");
2
+ let _alfe_ai_chat = require("@alfe.ai/chat");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_path = require("node:path");
5
+ let node_os = require("node:os");
6
+ let node_fs = require("node:fs");
7
+ //#region src/alfe-channel.ts
8
+ const CHANNEL_ID = "alfe";
9
+ const DEFAULT_ACCOUNT_ID = "default";
10
+ function getChannelSection(cfg) {
11
+ return cfg.channels?.alfe ?? {};
12
+ }
13
+ function normalizeAllowFrom(raw) {
14
+ if (!raw) return [];
15
+ if (typeof raw === "string") return [raw];
16
+ return raw;
17
+ }
18
+ /**
19
+ * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
20
+ *
21
+ * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
22
+ * but is registered dynamically via api.registerChannel().
23
+ */
24
+ function createAlfeChannelPlugin() {
25
+ return {
26
+ id: CHANNEL_ID,
27
+ meta: {
28
+ id: CHANNEL_ID,
29
+ label: "Alfe",
30
+ selectionLabel: "Alfe (Web & Mobile)",
31
+ detailLabel: "Alfe",
32
+ docsPath: "/channels/alfe",
33
+ docsLabel: "alfe",
34
+ blurb: "Alfe native chat — web widget and mobile app conversations.",
35
+ systemImage: "bubble.left.and.text.bubble.right",
36
+ order: 100,
37
+ aliases: ["alfe-web", "alfe-mobile"],
38
+ forceAccountBinding: false,
39
+ showConfigured: true
40
+ },
41
+ capabilities: {
42
+ chatTypes: ["direct", "group"],
43
+ reactions: false,
44
+ edit: false,
45
+ unsend: false,
46
+ reply: true,
47
+ effects: false,
48
+ groupManagement: false,
49
+ threads: false,
50
+ media: true,
51
+ nativeCommands: false,
52
+ polls: false
53
+ },
54
+ config: {
55
+ listAccountIds(cfg) {
56
+ const section = getChannelSection(cfg);
57
+ const ids = [];
58
+ if (section.enabled !== false && (section.allowFrom ?? section.defaultTo ?? section.dmPolicy)) ids.push(DEFAULT_ACCOUNT_ID);
59
+ if (section.accounts) {
60
+ for (const id of Object.keys(section.accounts)) if (!ids.includes(id)) ids.push(id);
61
+ }
62
+ if (ids.length === 0 && section.enabled !== false) ids.push(DEFAULT_ACCOUNT_ID);
63
+ return ids;
64
+ },
65
+ resolveAccount(cfg, accountId) {
66
+ const section = getChannelSection(cfg);
67
+ const id = accountId ?? DEFAULT_ACCOUNT_ID;
68
+ const accountSection = section.accounts?.[id];
69
+ if (accountSection) return {
70
+ accountId: id,
71
+ enabled: accountSection.enabled !== false,
72
+ allowFrom: normalizeAllowFrom(accountSection.allowFrom),
73
+ defaultTo: accountSection.defaultTo,
74
+ dmPolicy: accountSection.dmPolicy
75
+ };
76
+ return {
77
+ accountId: id,
78
+ enabled: section.enabled !== false,
79
+ allowFrom: normalizeAllowFrom(section.allowFrom),
80
+ defaultTo: section.defaultTo,
81
+ dmPolicy: section.dmPolicy
82
+ };
83
+ },
84
+ defaultAccountId() {
85
+ return DEFAULT_ACCOUNT_ID;
86
+ },
87
+ isEnabled(account) {
88
+ return account.enabled;
89
+ },
90
+ isConfigured() {
91
+ return true;
92
+ },
93
+ describeAccount(account) {
94
+ return {
95
+ accountId: account.accountId,
96
+ enabled: account.enabled,
97
+ configured: true,
98
+ dmPolicy: account.dmPolicy
99
+ };
100
+ },
101
+ resolveAllowFrom(params) {
102
+ const section = getChannelSection(params.cfg);
103
+ const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
104
+ return normalizeAllowFrom((section.accounts?.[id])?.allowFrom ?? section.allowFrom);
105
+ },
106
+ resolveDefaultTo(params) {
107
+ const section = getChannelSection(params.cfg);
108
+ const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
109
+ return section.accounts?.[id]?.defaultTo ?? section.defaultTo;
110
+ }
111
+ },
112
+ outbound: {
113
+ deliveryMode: "gateway",
114
+ textChunkLimit: 4e3
115
+ },
116
+ setup: {
117
+ resolveAccountId(params) {
118
+ return params.accountId ?? DEFAULT_ACCOUNT_ID;
119
+ },
120
+ applyAccountConfig(params) {
121
+ const cfg = { ...params.cfg };
122
+ cfg.channels ??= {};
123
+ cfg.channels.alfe ??= {};
124
+ const section = cfg.channels.alfe;
125
+ if (params.accountId === DEFAULT_ACCOUNT_ID) section.enabled = true;
126
+ else {
127
+ section.accounts ??= {};
128
+ section.accounts[params.accountId] = {
129
+ enabled: true,
130
+ ...params.input
131
+ };
132
+ }
133
+ return cfg;
134
+ }
135
+ }
136
+ };
137
+ }
138
+ //#endregion
139
+ //#region src/session-keys.ts
140
+ /**
141
+ * Session key helpers — handles standardized, canonical, and legacy formats.
142
+ *
143
+ * Standardized format (Alfe-controlled):
144
+ * alfe:{mode}:{identity} — single-threaded (SMS, WhatsApp)
145
+ * alfe:{mode}:{identity}:{convId} — multi-threaded (web chat)
146
+ *
147
+ * OpenClaw canonical format (from resolveAgentRoute):
148
+ * agent:{ocAgentId}:alfe:[default:]direct:{senderId}[:thread:{conversationId}]
149
+ *
150
+ * Legacy formats (deprecated):
151
+ * sms-{agentId}-{phone}
152
+ * wa-{agentId}-{phone}
153
+ * chat-{tenantId}-{agentId}-{suffix}
154
+ * agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}
155
+ */
156
+ /** Single-threaded channel modes — identity IS the conversation. */
157
+ const SINGLE_THREADED_MODES = new Set([
158
+ "sms",
159
+ "whatsapp",
160
+ "mobile"
161
+ ]);
162
+ /**
163
+ * Check if a session key belongs to the Alfe chat channel.
164
+ * Handles standardized, canonical, and legacy formats.
165
+ */
166
+ function isAlfeSessionKey(key) {
167
+ if (key.startsWith("alfe:")) return true;
168
+ if (key.includes(":alfe:")) return true;
169
+ if (key.includes("chat-") || key.startsWith("sms-") || key.startsWith("wa-")) return true;
170
+ return false;
171
+ }
172
+ /**
173
+ * Parse session key metadata. Returns available fields from any format.
174
+ *
175
+ * For the callback flow, `conversationId` is the critical field — it maps
176
+ * to the channel registry key used by getChannelCallback().
177
+ */
178
+ function parseAlfeSessionKey(key) {
179
+ const standardMatch = /^alfe:(\w+):(.+)$/.exec(key);
180
+ if (standardMatch) {
181
+ const [, mode, rest] = standardMatch;
182
+ if (SINGLE_THREADED_MODES.has(mode)) return {
183
+ agentId: "",
184
+ userId: rest,
185
+ conversationId: key,
186
+ tenantId: "",
187
+ mode
188
+ };
189
+ const lastColon = rest.lastIndexOf(":");
190
+ if (lastColon > 0) return {
191
+ agentId: "",
192
+ userId: rest.slice(0, lastColon),
193
+ conversationId: key,
194
+ tenantId: "",
195
+ mode
196
+ };
197
+ return {
198
+ agentId: "",
199
+ userId: rest,
200
+ conversationId: key,
201
+ tenantId: "",
202
+ mode
203
+ };
204
+ }
205
+ const canonicalMatch = /^agent:([^:]+):alfe:(?:default:)?direct:(.+?)(?::thread:(.+))?$/.exec(key);
206
+ if (canonicalMatch) {
207
+ const [, matchAgentId, matchUserId, matchConvId] = canonicalMatch;
208
+ return {
209
+ agentId: matchAgentId,
210
+ userId: matchUserId,
211
+ conversationId: matchConvId || "",
212
+ tenantId: "",
213
+ mode: ""
214
+ };
215
+ }
216
+ const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
217
+ const legacyMatch = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
218
+ return {
219
+ agentId: legacyMatch?.[2] ?? "",
220
+ userId: "",
221
+ conversationId: "",
222
+ tenantId: legacyMatch?.[1] ?? "",
223
+ mode: "chat"
224
+ };
225
+ }
226
+ /**
227
+ * Extract the channel mode from a standardized session key or conversationId.
228
+ * Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
229
+ */
230
+ function extractChannelMode(conversationId, fallback = "chat") {
231
+ return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
232
+ }
233
+ //#endregion
234
+ //#region src/session-store.ts
235
+ /**
236
+ * Session Store — persists chat sessions to the local filesystem.
237
+ *
238
+ * Storage layout:
239
+ * ~/.alfe/sessions/chat/{sessionId}.json
240
+ *
241
+ * Each session file contains metadata and the full message history.
242
+ * Sessions are written on every message to ensure durability.
243
+ * Old sessions are cleaned up automatically (30-day TTL, 1000 max).
244
+ */
245
+ const SESSIONS_DIR = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "sessions", "chat");
246
+ const MAX_SESSIONS = 1e3;
247
+ const MAX_AGE_MS = 720 * 60 * 60 * 1e3;
248
+ const CLEANUP_INTERVAL_MS = 36e5;
249
+ let lastCleanupAt = 0;
250
+ async function ensureDir() {
251
+ if (!(0, node_fs.existsSync)(SESSIONS_DIR)) await (0, node_fs_promises.mkdir)(SESSIONS_DIR, { recursive: true });
252
+ }
253
+ function sessionPath(sessionId) {
254
+ return (0, node_path.join)(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
255
+ }
256
+ async function cleanupOldSessions() {
257
+ if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
258
+ lastCleanupAt = Date.now();
259
+ try {
260
+ const jsonFiles = (await (0, node_fs_promises.readdir)(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
261
+ if (jsonFiles.length <= MAX_SESSIONS) {
262
+ const now = Date.now();
263
+ for (const file of jsonFiles) try {
264
+ const filePath = (0, node_path.join)(SESSIONS_DIR, file);
265
+ if (now - (await (0, node_fs_promises.stat)(filePath)).mtimeMs > MAX_AGE_MS) await (0, node_fs_promises.unlink)(filePath);
266
+ } catch {}
267
+ return;
268
+ }
269
+ const fileStats = [];
270
+ for (const file of jsonFiles) try {
271
+ const filePath = (0, node_path.join)(SESSIONS_DIR, file);
272
+ const fileStat = await (0, node_fs_promises.stat)(filePath);
273
+ fileStats.push({
274
+ path: filePath,
275
+ mtimeMs: fileStat.mtimeMs
276
+ });
277
+ } catch {}
278
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
279
+ const now = Date.now();
280
+ let remaining = fileStats.length;
281
+ for (const entry of fileStats) {
282
+ if (!(now - entry.mtimeMs > MAX_AGE_MS) && !(remaining > MAX_SESSIONS)) break;
283
+ try {
284
+ await (0, node_fs_promises.unlink)(entry.path);
285
+ remaining--;
286
+ } catch {}
287
+ }
288
+ } catch {}
289
+ }
290
+ async function getSession(sessionId) {
291
+ try {
292
+ const data = await (0, node_fs_promises.readFile)(sessionPath(sessionId), "utf-8");
293
+ return JSON.parse(data);
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+ async function saveSession(session) {
299
+ await ensureDir();
300
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
301
+ await (0, node_fs_promises.writeFile)(sessionPath(session.sessionId), JSON.stringify(session, null, 2), "utf-8");
302
+ }
303
+ async function createSession(sessionId, agentId, channel, tenantId, userId) {
304
+ const now = (/* @__PURE__ */ new Date()).toISOString();
305
+ const session = {
306
+ sessionId,
307
+ agentId,
308
+ channel,
309
+ tenantId,
310
+ userId,
311
+ createdAt: now,
312
+ updatedAt: now,
313
+ messages: []
314
+ };
315
+ await saveSession(session);
316
+ cleanupOldSessions();
317
+ return session;
318
+ }
319
+ async function addMessage(sessionId, role, content, senderId, senderName) {
320
+ const session = await getSession(sessionId);
321
+ if (!session) return;
322
+ session.messages.push({
323
+ role,
324
+ content,
325
+ timestamp: Date.now(),
326
+ ...senderId ? { senderId } : {},
327
+ ...senderName ? { senderName } : {}
328
+ });
329
+ await saveSession(session);
330
+ }
331
+ async function listSessions(filters, limit = 50) {
332
+ await ensureDir();
333
+ let files;
334
+ try {
335
+ files = await (0, node_fs_promises.readdir)(SESSIONS_DIR);
336
+ } catch {
337
+ return [];
338
+ }
339
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
340
+ const summaries = [];
341
+ for (const file of jsonFiles) try {
342
+ const data = await (0, node_fs_promises.readFile)((0, node_path.join)(SESSIONS_DIR, file), "utf-8");
343
+ const session = JSON.parse(data);
344
+ if (filters?.agentId && session.agentId !== filters.agentId) continue;
345
+ if (filters?.channel && session.channel !== filters.channel) continue;
346
+ if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
347
+ if (filters?.userId && session.userId !== filters.userId) continue;
348
+ const lastMsg = session.messages.at(-1);
349
+ summaries.push({
350
+ sessionId: session.sessionId,
351
+ agentId: session.agentId,
352
+ channel: session.channel,
353
+ createdAt: session.createdAt,
354
+ lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
355
+ preview: lastMsg?.content.slice(0, 100),
356
+ messageCount: session.messages.length
357
+ });
358
+ } catch {}
359
+ summaries.sort((a, b) => {
360
+ const aTime = a.lastMessageAt ?? a.createdAt;
361
+ return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
362
+ });
363
+ return summaries.slice(0, limit);
364
+ }
365
+ //#endregion
366
+ //#region src/plugin.ts
367
+ /**
368
+ * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
369
+ *
370
+ * Registers the 'alfe' channel with OpenClaw. Messages are dispatched
371
+ * through OpenClaw's auto-reply pipeline via dispatchInboundDirectDmWithRuntime(),
372
+ * the same API built-in channels (Slack, Discord, Telegram) use.
373
+ *
374
+ * Architecture:
375
+ * Chat Service (Fly.io) ←WS→ ChatServiceClient
376
+ * → dispatchInboundDirectDmWithRuntime() → Agent (auto-reply pipeline)
377
+ * ← onAgentEvent streaming ← (real-time deltas)
378
+ * ← deliver() callback ← (final response)
379
+ */
380
+ let dispatchInbound = null;
381
+ /**
382
+ * Resolve OpenClaw SDK functions from the global install.
383
+ * The openclaw package is installed globally (/usr/lib/node_modules/openclaw/)
384
+ * but is NOT in the plugin's node_modules. Built-in extensions can import
385
+ * directly; external plugins must use createRequire.
386
+ */
387
+ function resolveOpenClawSdk(log) {
388
+ for (const globalPath of ["/usr/lib/node_modules/openclaw/package.json", "/usr/local/lib/node_modules/openclaw/package.json"]) try {
389
+ const channelInbound = (0, node_module.createRequire)(globalPath)("openclaw/plugin-sdk/channel-inbound");
390
+ if (channelInbound.dispatchInboundDirectDmWithRuntime) {
391
+ dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
392
+ log.info(`Resolved OpenClaw SDK from ${globalPath}`);
393
+ return;
394
+ }
395
+ } catch {}
396
+ log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
397
+ }
398
+ let pluginRuntime = null;
399
+ let chatClient = null;
400
+ let connectingPromise = null;
401
+ async function handleAgentRequest(request, log) {
402
+ const runtime = pluginRuntime;
403
+ if (!runtime) {
404
+ chatClient?.sendResponse(request.id, false, { message: "Plugin runtime not initialized" });
405
+ return;
406
+ }
407
+ if (!dispatchInbound) {
408
+ chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
409
+ return;
410
+ }
411
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName } = request.params;
412
+ if (!message) {
413
+ chatClient?.sendResponse(request.id, false, { message: "Missing message" });
414
+ return;
415
+ }
416
+ const senderId = displayName ?? userId ?? "anon";
417
+ const cfg = runtime.config.loadConfig();
418
+ const sessionId = conversationId ?? legacySessionKey;
419
+ if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
420
+ await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
421
+ let resolvedOpenClawKey = null;
422
+ const unsubscribe = runtime.events.onAgentEvent((evt) => {
423
+ if (!evt.sessionKey) return;
424
+ if (evt.stream !== "assistant") return;
425
+ resolvedOpenClawKey ??= evt.sessionKey;
426
+ if (evt.sessionKey !== resolvedOpenClawKey) return;
427
+ chatClient?.sendEvent("chat", {
428
+ runId: evt.runId,
429
+ sessionKey: legacySessionKey,
430
+ seq: evt.seq,
431
+ state: "delta",
432
+ message: evt.data
433
+ });
434
+ });
435
+ try {
436
+ const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
437
+ const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
438
+ const shortConvId = conversationId?.slice(-8) ?? "";
439
+ const userLabel = displayName ?? userId ?? senderId;
440
+ const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
441
+ resolvedOpenClawKey = (await dispatchInbound({
442
+ cfg,
443
+ runtime: { channel: runtime.channel },
444
+ channel: "alfe",
445
+ channelLabel,
446
+ accountId: "default",
447
+ peer: conversationType === "group" ? {
448
+ kind: "group",
449
+ id: conversationId ?? senderId
450
+ } : {
451
+ kind: "direct",
452
+ id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
453
+ },
454
+ senderId,
455
+ senderAddress: `user:${senderId}`,
456
+ recipientAddress: "agent",
457
+ conversationLabel,
458
+ rawBody: message,
459
+ messageId: request.id,
460
+ timestamp: Date.now(),
461
+ extraContext: {
462
+ ...tenantId ? { TenantId: tenantId } : {},
463
+ ...clientType ? { ClientType: clientType } : {},
464
+ ...conversationId ? { ConversationId: conversationId } : {},
465
+ ...displayName ? { SenderName: displayName } : {},
466
+ ChannelMode: channelMode
467
+ },
468
+ deliver: async (payload) => {
469
+ const responseText = payload.text ?? "";
470
+ await addMessage(sessionId, "assistant", responseText);
471
+ chatClient?.sendResponse(request.id, true, {
472
+ text: responseText,
473
+ sessionKey: resolvedOpenClawKey ?? legacySessionKey
474
+ });
475
+ },
476
+ onRecordError: (err) => {
477
+ log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
478
+ },
479
+ onDispatchError: (err, info) => {
480
+ log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
481
+ }
482
+ })).route.sessionKey;
483
+ log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
484
+ } catch (err) {
485
+ const errMsg = err instanceof Error ? err.message : String(err);
486
+ log.error(`Agent dispatch failed: ${errMsg}`);
487
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
488
+ } finally {
489
+ unsubscribe();
490
+ }
491
+ }
492
+ async function handleSessionsList(request, log) {
493
+ try {
494
+ const params = request.params;
495
+ const sessions = await listSessions({
496
+ channel: params.channel,
497
+ tenantId: params.tenantId,
498
+ userId: params.userId
499
+ });
500
+ chatClient?.sendResponse(request.id, true, { sessions });
501
+ } catch (err) {
502
+ const errMsg = err instanceof Error ? err.message : String(err);
503
+ log.error(`sessions.list failed: ${errMsg}`);
504
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
505
+ }
506
+ }
507
+ async function handleSessionsGet(request, log) {
508
+ try {
509
+ const params = request.params;
510
+ const session = await getSession(params.sessionId);
511
+ if (!session) {
512
+ chatClient?.sendResponse(request.id, true, {
513
+ ok: false,
514
+ error: "Session not found"
515
+ });
516
+ return;
517
+ }
518
+ if (params.userId && session.userId && session.userId !== params.userId) {
519
+ chatClient?.sendResponse(request.id, true, {
520
+ ok: false,
521
+ error: "Session not found"
522
+ });
523
+ return;
524
+ }
525
+ chatClient?.sendResponse(request.id, true, {
526
+ sessionId: session.sessionId,
527
+ agentId: session.agentId,
528
+ channel: session.channel,
529
+ createdAt: session.createdAt,
530
+ messages: session.messages.map((m) => ({
531
+ id: `msg-${String(m.timestamp)}`,
532
+ role: m.role,
533
+ content: m.content,
534
+ timestamp: m.timestamp
535
+ }))
536
+ });
537
+ } catch (err) {
538
+ const errMsg = err instanceof Error ? err.message : String(err);
539
+ log.error(`sessions.get failed: ${errMsg}`);
540
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
541
+ }
542
+ }
543
+ const plugin = {
544
+ id: "@alfe.ai/openclaw-chat",
545
+ name: "Chat Plugin",
546
+ description: "Chat conversation channel — web widget and mobile app share unified chat sessions",
547
+ version: "0.0.8",
548
+ activate(api) {
549
+ const log = api.logger;
550
+ const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
551
+ globalThis.__alfeChatPluginActivated = true;
552
+ const alfeChannel = createAlfeChannelPlugin();
553
+ api.registerChannel(alfeChannel);
554
+ log.info(`Registered channel: ${alfeChannel.id}`);
555
+ if (!alreadyActivated) {
556
+ log.info("Chat plugin registering...");
557
+ resolveOpenClawSdk(log);
558
+ pluginRuntime = api.runtime ?? null;
559
+ }
560
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
561
+ if (!alreadyActivated) connectingPromise = Promise.resolve().then(() => {
562
+ try {
563
+ const { apiKey, chatWsUrl } = (0, _alfe_ai_chat.resolveAlfeChat)({
564
+ apiKey: pluginConfig.apiKey,
565
+ chatWsUrl: pluginConfig.chatWsUrl
566
+ });
567
+ if (chatWsUrl && apiKey) {
568
+ log.info(`Connecting to chat service: ${chatWsUrl}`);
569
+ chatClient = new _alfe_ai_chat.ChatServiceClient({
570
+ wsUrl: chatWsUrl,
571
+ apiKey,
572
+ onRequest: (request) => {
573
+ if (request.method === "agent") handleAgentRequest(request, log);
574
+ else if (request.method === "sessions.list") handleSessionsList(request, log);
575
+ else if (request.method === "sessions.get") handleSessionsGet(request, log);
576
+ else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
577
+ },
578
+ onConnectionChange: (connected) => {
579
+ log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
580
+ },
581
+ logger: log
582
+ });
583
+ chatClient.start();
584
+ log.info("Chat service relay started");
585
+ } else log.info("Chat service URL not configured — running without chat service relay");
586
+ } catch (err) {
587
+ log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
588
+ }
589
+ });
590
+ if (typeof api.registerGatewayMethod === "function") {
591
+ api.registerGatewayMethod("sessions.list", async (...args) => {
592
+ const params = args[0];
593
+ return { sessions: await listSessions({
594
+ channel: params.channel,
595
+ tenantId: params.tenantId,
596
+ userId: params.userId
597
+ }) };
598
+ });
599
+ api.registerGatewayMethod("sessions.get", async (...args) => {
600
+ const params = args[0];
601
+ const session = await getSession(params.sessionId);
602
+ if (!session) return {
603
+ ok: false,
604
+ error: "Session not found"
605
+ };
606
+ if (params.userId && session.userId && session.userId !== params.userId) return {
607
+ ok: false,
608
+ error: "Session not found"
609
+ };
610
+ return {
611
+ sessionId: session.sessionId,
612
+ agentId: session.agentId,
613
+ channel: session.channel,
614
+ createdAt: session.createdAt,
615
+ messages: session.messages.map((m) => ({
616
+ id: `msg-${String(m.timestamp)}`,
617
+ role: m.role,
618
+ content: m.content,
619
+ timestamp: m.timestamp
620
+ }))
621
+ };
622
+ });
623
+ log.info("Registered gateway RPC methods: sessions.list, sessions.get");
624
+ }
625
+ if (!alreadyActivated) {
626
+ api.on("session_start", async (...eventArgs) => {
627
+ const key = eventArgs[0].sessionKey;
628
+ if (!key || isAlfeSessionKey(key)) return;
629
+ log.info(`Chat session starting: ${key}`);
630
+ await createSession(key, "", "alfe");
631
+ }, { priority: 50 });
632
+ api.on("message", async (...eventArgs) => {
633
+ const event = eventArgs[0];
634
+ const key = event.sessionKey;
635
+ if (!key) return;
636
+ if (isAlfeSessionKey(key)) {
637
+ if (event.role === "assistant" && chatClient) {
638
+ const parsed = parseAlfeSessionKey(key);
639
+ if (parsed.conversationId) chatClient.notify("agent-message", {
640
+ conversationId: parsed.conversationId,
641
+ text: event.content
642
+ });
643
+ }
644
+ } else await addMessage(key, event.role, event.content);
645
+ });
646
+ api.on("session_end", (...eventArgs) => {
647
+ const key = eventArgs[0].sessionKey;
648
+ if (!key || !isAlfeSessionKey(key)) return;
649
+ log.info(`Chat session ending: ${key}`);
650
+ });
651
+ }
652
+ log.info("Chat plugin registered");
653
+ },
654
+ async deactivate(api) {
655
+ globalThis.__alfeChatPluginActivated = false;
656
+ const log = api.logger;
657
+ log.info("Chat plugin deactivating...");
658
+ if (connectingPromise) {
659
+ await connectingPromise.catch((err) => {
660
+ api.logger.debug(`Connection attempt failed: ${err instanceof Error ? err.message : String(err)}`);
661
+ });
662
+ connectingPromise = null;
663
+ }
664
+ if (chatClient) {
665
+ chatClient.stop();
666
+ chatClient = null;
667
+ log.info("Chat service client stopped");
668
+ }
669
+ pluginRuntime = null;
670
+ dispatchInbound = null;
671
+ log.info("Chat plugin deactivated");
672
+ }
673
+ };
674
+ //#endregion
675
+ Object.defineProperty(exports, "createAlfeChannelPlugin", {
676
+ enumerable: true,
677
+ get: function() {
678
+ return createAlfeChannelPlugin;
679
+ }
680
+ });
681
+ Object.defineProperty(exports, "plugin", {
682
+ enumerable: true,
683
+ get: function() {
684
+ return plugin;
685
+ }
686
+ });
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.cjs";
2
+ export { plugin as default };
package/dist/plugin2.js CHANGED
@@ -138,28 +138,98 @@ function createAlfeChannelPlugin() {
138
138
  //#endregion
139
139
  //#region src/session-keys.ts
140
140
  /**
141
- * Session key helpers — handles both legacy and canonical OpenClaw formats.
141
+ * Session key helpers — handles standardized, canonical, and legacy formats.
142
142
  *
143
- * Canonical format (from resolveAgentRoute):
144
- * agent:{agentId}:alfe:direct:{userId}
145
- * agent:{agentId}:alfe:direct:{userId}:thread:{conversationId}
143
+ * Standardized format (Alfe-controlled):
144
+ * alfe:{mode}:{identity} — single-threaded (SMS, WhatsApp)
145
+ * alfe:{mode}:{identity}:{convId} — multi-threaded (web chat)
146
146
  *
147
- * Legacy format (from chat adapter):
147
+ * OpenClaw canonical format (from resolveAgentRoute):
148
+ * agent:{ocAgentId}:alfe:[default:]direct:{senderId}[:thread:{conversationId}]
149
+ *
150
+ * Legacy formats (deprecated):
151
+ * sms-{agentId}-{phone}
152
+ * wa-{agentId}-{phone}
148
153
  * chat-{tenantId}-{agentId}-{suffix}
149
154
  * agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}
150
- *
151
- * The plugin may receive either format depending on which
152
- * OpenClaw event fires and whether the new dispatch path is active.
153
155
  */
156
+ /** Single-threaded channel modes — identity IS the conversation. */
157
+ const SINGLE_THREADED_MODES = new Set([
158
+ "sms",
159
+ "whatsapp",
160
+ "mobile"
161
+ ]);
154
162
  /**
155
163
  * Check if a session key belongs to the Alfe chat channel.
156
- * Handles both canonical and legacy formats.
164
+ * Handles standardized, canonical, and legacy formats.
157
165
  */
158
166
  function isAlfeSessionKey(key) {
167
+ if (key.startsWith("alfe:")) return true;
159
168
  if (key.includes(":alfe:")) return true;
160
- if (key.includes("chat-")) return true;
169
+ if (key.includes("chat-") || key.startsWith("sms-") || key.startsWith("wa-")) return true;
161
170
  return false;
162
171
  }
172
+ /**
173
+ * Parse session key metadata. Returns available fields from any format.
174
+ *
175
+ * For the callback flow, `conversationId` is the critical field — it maps
176
+ * to the channel registry key used by getChannelCallback().
177
+ */
178
+ function parseAlfeSessionKey(key) {
179
+ const standardMatch = /^alfe:(\w+):(.+)$/.exec(key);
180
+ if (standardMatch) {
181
+ const [, mode, rest] = standardMatch;
182
+ if (SINGLE_THREADED_MODES.has(mode)) return {
183
+ agentId: "",
184
+ userId: rest,
185
+ conversationId: key,
186
+ tenantId: "",
187
+ mode
188
+ };
189
+ const lastColon = rest.lastIndexOf(":");
190
+ if (lastColon > 0) return {
191
+ agentId: "",
192
+ userId: rest.slice(0, lastColon),
193
+ conversationId: key,
194
+ tenantId: "",
195
+ mode
196
+ };
197
+ return {
198
+ agentId: "",
199
+ userId: rest,
200
+ conversationId: key,
201
+ tenantId: "",
202
+ mode
203
+ };
204
+ }
205
+ const canonicalMatch = /^agent:([^:]+):alfe:(?:default:)?direct:(.+?)(?::thread:(.+))?$/.exec(key);
206
+ if (canonicalMatch) {
207
+ const [, matchAgentId, matchUserId, matchConvId] = canonicalMatch;
208
+ return {
209
+ agentId: matchAgentId,
210
+ userId: matchUserId,
211
+ conversationId: matchConvId || "",
212
+ tenantId: "",
213
+ mode: ""
214
+ };
215
+ }
216
+ const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
217
+ const legacyMatch = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
218
+ return {
219
+ agentId: legacyMatch?.[2] ?? "",
220
+ userId: "",
221
+ conversationId: "",
222
+ tenantId: legacyMatch?.[1] ?? "",
223
+ mode: "chat"
224
+ };
225
+ }
226
+ /**
227
+ * Extract the channel mode from a standardized session key or conversationId.
228
+ * Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
229
+ */
230
+ function extractChannelMode(conversationId, fallback = "chat") {
231
+ return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
232
+ }
163
233
  //#endregion
164
234
  //#region src/session-store.ts
165
235
  /**
@@ -274,6 +344,7 @@ async function listSessions(filters, limit = 50) {
274
344
  if (filters?.agentId && session.agentId !== filters.agentId) continue;
275
345
  if (filters?.channel && session.channel !== filters.channel) continue;
276
346
  if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
347
+ if (filters?.userId && session.userId !== filters.userId) continue;
277
348
  const lastMsg = session.messages.at(-1);
278
349
  summaries.push({
279
350
  sessionId: session.sessionId,
@@ -362,14 +433,16 @@ async function handleAgentRequest(request, log) {
362
433
  });
363
434
  });
364
435
  try {
436
+ const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
437
+ const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
365
438
  const shortConvId = conversationId?.slice(-8) ?? "";
366
439
  const userLabel = displayName ?? userId ?? senderId;
367
- const conversationLabel = conversationType === "group" ? shortConvId ? `[Alfe] Group (${shortConvId})` : "[Alfe] Group" : shortConvId ? `[Alfe] ${userLabel} (${shortConvId})` : `[Alfe] ${userLabel}`;
440
+ const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
368
441
  resolvedOpenClawKey = (await dispatchInbound({
369
442
  cfg,
370
443
  runtime: { channel: runtime.channel },
371
444
  channel: "alfe",
372
- channelLabel: "Alfe",
445
+ channelLabel,
373
446
  accountId: "default",
374
447
  peer: conversationType === "group" ? {
375
448
  kind: "group",
@@ -389,7 +462,8 @@ async function handleAgentRequest(request, log) {
389
462
  ...tenantId ? { TenantId: tenantId } : {},
390
463
  ...clientType ? { ClientType: clientType } : {},
391
464
  ...conversationId ? { ConversationId: conversationId } : {},
392
- ...displayName ? { SenderName: displayName } : {}
465
+ ...displayName ? { SenderName: displayName } : {},
466
+ ChannelMode: channelMode
393
467
  },
394
468
  deliver: async (payload) => {
395
469
  const responseText = payload.text ?? "";
@@ -420,7 +494,8 @@ async function handleSessionsList(request, log) {
420
494
  const params = request.params;
421
495
  const sessions = await listSessions({
422
496
  channel: params.channel,
423
- tenantId: params.tenantId
497
+ tenantId: params.tenantId,
498
+ userId: params.userId
424
499
  });
425
500
  chatClient?.sendResponse(request.id, true, { sessions });
426
501
  } catch (err) {
@@ -440,6 +515,13 @@ async function handleSessionsGet(request, log) {
440
515
  });
441
516
  return;
442
517
  }
518
+ if (params.userId && session.userId && session.userId !== params.userId) {
519
+ chatClient?.sendResponse(request.id, true, {
520
+ ok: false,
521
+ error: "Session not found"
522
+ });
523
+ return;
524
+ }
443
525
  chatClient?.sendResponse(request.id, true, {
444
526
  sessionId: session.sessionId,
445
527
  agentId: session.agentId,
@@ -510,7 +592,8 @@ const plugin = {
510
592
  const params = args[0];
511
593
  return { sessions: await listSessions({
512
594
  channel: params.channel,
513
- tenantId: params.tenantId
595
+ tenantId: params.tenantId,
596
+ userId: params.userId
514
597
  }) };
515
598
  });
516
599
  api.registerGatewayMethod("sessions.get", async (...args) => {
@@ -520,6 +603,10 @@ const plugin = {
520
603
  ok: false,
521
604
  error: "Session not found"
522
605
  };
606
+ if (params.userId && session.userId && session.userId !== params.userId) return {
607
+ ok: false,
608
+ error: "Session not found"
609
+ };
523
610
  return {
524
611
  sessionId: session.sessionId,
525
612
  agentId: session.agentId,
@@ -545,8 +632,16 @@ const plugin = {
545
632
  api.on("message", async (...eventArgs) => {
546
633
  const event = eventArgs[0];
547
634
  const key = event.sessionKey;
548
- if (!key || isAlfeSessionKey(key)) return;
549
- await addMessage(key, event.role, event.content);
635
+ if (!key) return;
636
+ if (isAlfeSessionKey(key)) {
637
+ if (event.role === "assistant" && chatClient) {
638
+ const parsed = parseAlfeSessionKey(key);
639
+ if (parsed.conversationId) chatClient.notify("agent-message", {
640
+ conversationId: parsed.conversationId,
641
+ text: event.content
642
+ });
643
+ }
644
+ } else await addMessage(key, event.role, event.content);
550
645
  });
551
646
  api.on("session_end", (...eventArgs) => {
552
647
  const key = eventArgs[0].sessionKey;
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.cjs",
12
+ "import": "./dist/index.js"
12
13
  },
13
14
  "./plugin": {
14
- "import": "./dist/plugin.js",
15
- "types": "./dist/plugin.d.ts"
15
+ "types": "./dist/plugin.d.ts",
16
+ "require": "./dist/plugin.cjs",
17
+ "import": "./dist/plugin.js"
16
18
  }
17
19
  },
18
20
  "openclaw": {
@@ -25,7 +27,7 @@
25
27
  "openclaw.plugin.json"
26
28
  ],
27
29
  "dependencies": {
28
- "@alfe.ai/chat": "^0.0.4"
30
+ "@alfe.ai/chat": "^0.0.6"
29
31
  },
30
32
  "peerDependencies": {
31
33
  "openclaw": ">=2026.3.0"