@alfe.ai/openclaw-chat 0.0.14 → 0.0.15

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,605 @@
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 both legacy and canonical OpenClaw formats.
142
+ *
143
+ * Canonical format (from resolveAgentRoute):
144
+ * agent:{agentId}:alfe:direct:{userId}
145
+ * agent:{agentId}:alfe:direct:{userId}:thread:{conversationId}
146
+ *
147
+ * Legacy format (from chat adapter):
148
+ * chat-{tenantId}-{agentId}-{suffix}
149
+ * 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
+ */
154
+ /**
155
+ * Check if a session key belongs to the Alfe chat channel.
156
+ * Handles both canonical and legacy formats.
157
+ */
158
+ function isAlfeSessionKey(key) {
159
+ if (key.includes(":alfe:")) return true;
160
+ if (key.includes("chat-")) return true;
161
+ return false;
162
+ }
163
+ //#endregion
164
+ //#region src/session-store.ts
165
+ /**
166
+ * Session Store — persists chat sessions to the local filesystem.
167
+ *
168
+ * Storage layout:
169
+ * ~/.alfe/sessions/chat/{sessionId}.json
170
+ *
171
+ * Each session file contains metadata and the full message history.
172
+ * Sessions are written on every message to ensure durability.
173
+ * Old sessions are cleaned up automatically (30-day TTL, 1000 max).
174
+ */
175
+ const SESSIONS_DIR = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "sessions", "chat");
176
+ const MAX_SESSIONS = 1e3;
177
+ const MAX_AGE_MS = 720 * 60 * 60 * 1e3;
178
+ const CLEANUP_INTERVAL_MS = 36e5;
179
+ let lastCleanupAt = 0;
180
+ async function ensureDir() {
181
+ if (!(0, node_fs.existsSync)(SESSIONS_DIR)) await (0, node_fs_promises.mkdir)(SESSIONS_DIR, { recursive: true });
182
+ }
183
+ function sessionPath(sessionId) {
184
+ return (0, node_path.join)(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
185
+ }
186
+ async function cleanupOldSessions() {
187
+ if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
188
+ lastCleanupAt = Date.now();
189
+ try {
190
+ const jsonFiles = (await (0, node_fs_promises.readdir)(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
191
+ if (jsonFiles.length <= MAX_SESSIONS) {
192
+ const now = Date.now();
193
+ for (const file of jsonFiles) try {
194
+ const filePath = (0, node_path.join)(SESSIONS_DIR, file);
195
+ if (now - (await (0, node_fs_promises.stat)(filePath)).mtimeMs > MAX_AGE_MS) await (0, node_fs_promises.unlink)(filePath);
196
+ } catch {}
197
+ return;
198
+ }
199
+ const fileStats = [];
200
+ for (const file of jsonFiles) try {
201
+ const filePath = (0, node_path.join)(SESSIONS_DIR, file);
202
+ const fileStat = await (0, node_fs_promises.stat)(filePath);
203
+ fileStats.push({
204
+ path: filePath,
205
+ mtimeMs: fileStat.mtimeMs
206
+ });
207
+ } catch {}
208
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
209
+ const now = Date.now();
210
+ let remaining = fileStats.length;
211
+ for (const entry of fileStats) {
212
+ if (!(now - entry.mtimeMs > MAX_AGE_MS) && !(remaining > MAX_SESSIONS)) break;
213
+ try {
214
+ await (0, node_fs_promises.unlink)(entry.path);
215
+ remaining--;
216
+ } catch {}
217
+ }
218
+ } catch {}
219
+ }
220
+ async function getSession(sessionId) {
221
+ try {
222
+ const data = await (0, node_fs_promises.readFile)(sessionPath(sessionId), "utf-8");
223
+ return JSON.parse(data);
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+ async function saveSession(session) {
229
+ await ensureDir();
230
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
231
+ await (0, node_fs_promises.writeFile)(sessionPath(session.sessionId), JSON.stringify(session, null, 2), "utf-8");
232
+ }
233
+ async function createSession(sessionId, agentId, channel, tenantId, userId) {
234
+ const now = (/* @__PURE__ */ new Date()).toISOString();
235
+ const session = {
236
+ sessionId,
237
+ agentId,
238
+ channel,
239
+ tenantId,
240
+ userId,
241
+ createdAt: now,
242
+ updatedAt: now,
243
+ messages: []
244
+ };
245
+ await saveSession(session);
246
+ cleanupOldSessions();
247
+ return session;
248
+ }
249
+ async function addMessage(sessionId, role, content, senderId, senderName) {
250
+ const session = await getSession(sessionId);
251
+ if (!session) return;
252
+ session.messages.push({
253
+ role,
254
+ content,
255
+ timestamp: Date.now(),
256
+ ...senderId ? { senderId } : {},
257
+ ...senderName ? { senderName } : {}
258
+ });
259
+ await saveSession(session);
260
+ }
261
+ async function listSessions(filters, limit = 50) {
262
+ await ensureDir();
263
+ let files;
264
+ try {
265
+ files = await (0, node_fs_promises.readdir)(SESSIONS_DIR);
266
+ } catch {
267
+ return [];
268
+ }
269
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
270
+ const summaries = [];
271
+ for (const file of jsonFiles) try {
272
+ const data = await (0, node_fs_promises.readFile)((0, node_path.join)(SESSIONS_DIR, file), "utf-8");
273
+ const session = JSON.parse(data);
274
+ if (filters?.agentId && session.agentId !== filters.agentId) continue;
275
+ if (filters?.channel && session.channel !== filters.channel) continue;
276
+ if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
277
+ if (filters?.userId && session.userId !== filters.userId) continue;
278
+ const lastMsg = session.messages.at(-1);
279
+ summaries.push({
280
+ sessionId: session.sessionId,
281
+ agentId: session.agentId,
282
+ channel: session.channel,
283
+ createdAt: session.createdAt,
284
+ lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
285
+ preview: lastMsg?.content.slice(0, 100),
286
+ messageCount: session.messages.length
287
+ });
288
+ } catch {}
289
+ summaries.sort((a, b) => {
290
+ const aTime = a.lastMessageAt ?? a.createdAt;
291
+ return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
292
+ });
293
+ return summaries.slice(0, limit);
294
+ }
295
+ //#endregion
296
+ //#region src/plugin.ts
297
+ /**
298
+ * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
299
+ *
300
+ * Registers the 'alfe' channel with OpenClaw. Messages are dispatched
301
+ * through OpenClaw's auto-reply pipeline via dispatchInboundDirectDmWithRuntime(),
302
+ * the same API built-in channels (Slack, Discord, Telegram) use.
303
+ *
304
+ * Architecture:
305
+ * Chat Service (Fly.io) ←WS→ ChatServiceClient
306
+ * → dispatchInboundDirectDmWithRuntime() → Agent (auto-reply pipeline)
307
+ * ← onAgentEvent streaming ← (real-time deltas)
308
+ * ← deliver() callback ← (final response)
309
+ */
310
+ let dispatchInbound = null;
311
+ /**
312
+ * Resolve OpenClaw SDK functions from the global install.
313
+ * The openclaw package is installed globally (/usr/lib/node_modules/openclaw/)
314
+ * but is NOT in the plugin's node_modules. Built-in extensions can import
315
+ * directly; external plugins must use createRequire.
316
+ */
317
+ function resolveOpenClawSdk(log) {
318
+ for (const globalPath of ["/usr/lib/node_modules/openclaw/package.json", "/usr/local/lib/node_modules/openclaw/package.json"]) try {
319
+ const channelInbound = (0, node_module.createRequire)(globalPath)("openclaw/plugin-sdk/channel-inbound");
320
+ if (channelInbound.dispatchInboundDirectDmWithRuntime) {
321
+ dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
322
+ log.info(`Resolved OpenClaw SDK from ${globalPath}`);
323
+ return;
324
+ }
325
+ } catch {}
326
+ log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
327
+ }
328
+ let pluginRuntime = null;
329
+ let chatClient = null;
330
+ let connectingPromise = null;
331
+ async function handleAgentRequest(request, log) {
332
+ const runtime = pluginRuntime;
333
+ if (!runtime) {
334
+ chatClient?.sendResponse(request.id, false, { message: "Plugin runtime not initialized" });
335
+ return;
336
+ }
337
+ if (!dispatchInbound) {
338
+ chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
339
+ return;
340
+ }
341
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName } = request.params;
342
+ if (!message) {
343
+ chatClient?.sendResponse(request.id, false, { message: "Missing message" });
344
+ return;
345
+ }
346
+ const senderId = displayName ?? userId ?? "anon";
347
+ const cfg = runtime.config.loadConfig();
348
+ const sessionId = conversationId ?? legacySessionKey;
349
+ if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
350
+ await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
351
+ let resolvedOpenClawKey = null;
352
+ const unsubscribe = runtime.events.onAgentEvent((evt) => {
353
+ if (!evt.sessionKey) return;
354
+ if (evt.stream !== "assistant") return;
355
+ resolvedOpenClawKey ??= evt.sessionKey;
356
+ if (evt.sessionKey !== resolvedOpenClawKey) return;
357
+ chatClient?.sendEvent("chat", {
358
+ runId: evt.runId,
359
+ sessionKey: legacySessionKey,
360
+ seq: evt.seq,
361
+ state: "delta",
362
+ message: evt.data
363
+ });
364
+ });
365
+ try {
366
+ const shortConvId = conversationId?.slice(-8) ?? "";
367
+ const userLabel = displayName ?? userId ?? senderId;
368
+ const conversationLabel = conversationType === "group" ? shortConvId ? `[Alfe] Group (${shortConvId})` : "[Alfe] Group" : shortConvId ? `[Alfe] ${userLabel} (${shortConvId})` : `[Alfe] ${userLabel}`;
369
+ resolvedOpenClawKey = (await dispatchInbound({
370
+ cfg,
371
+ runtime: { channel: runtime.channel },
372
+ channel: "alfe",
373
+ channelLabel: "Alfe",
374
+ accountId: "default",
375
+ peer: conversationType === "group" ? {
376
+ kind: "group",
377
+ id: conversationId ?? senderId
378
+ } : {
379
+ kind: "direct",
380
+ id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
381
+ },
382
+ senderId,
383
+ senderAddress: `user:${senderId}`,
384
+ recipientAddress: "agent",
385
+ conversationLabel,
386
+ rawBody: message,
387
+ messageId: request.id,
388
+ timestamp: Date.now(),
389
+ extraContext: {
390
+ ...tenantId ? { TenantId: tenantId } : {},
391
+ ...clientType ? { ClientType: clientType } : {},
392
+ ...conversationId ? { ConversationId: conversationId } : {},
393
+ ...displayName ? { SenderName: displayName } : {}
394
+ },
395
+ deliver: async (payload) => {
396
+ const responseText = payload.text ?? "";
397
+ await addMessage(sessionId, "assistant", responseText);
398
+ chatClient?.sendResponse(request.id, true, {
399
+ text: responseText,
400
+ sessionKey: resolvedOpenClawKey ?? legacySessionKey
401
+ });
402
+ },
403
+ onRecordError: (err) => {
404
+ log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
405
+ },
406
+ onDispatchError: (err, info) => {
407
+ log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
408
+ }
409
+ })).route.sessionKey;
410
+ log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
411
+ } catch (err) {
412
+ const errMsg = err instanceof Error ? err.message : String(err);
413
+ log.error(`Agent dispatch failed: ${errMsg}`);
414
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
415
+ } finally {
416
+ unsubscribe();
417
+ }
418
+ }
419
+ async function handleSessionsList(request, log) {
420
+ try {
421
+ const params = request.params;
422
+ const sessions = await listSessions({
423
+ channel: params.channel,
424
+ tenantId: params.tenantId,
425
+ userId: params.userId
426
+ });
427
+ chatClient?.sendResponse(request.id, true, { sessions });
428
+ } catch (err) {
429
+ const errMsg = err instanceof Error ? err.message : String(err);
430
+ log.error(`sessions.list failed: ${errMsg}`);
431
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
432
+ }
433
+ }
434
+ async function handleSessionsGet(request, log) {
435
+ try {
436
+ const params = request.params;
437
+ const session = await getSession(params.sessionId);
438
+ if (!session) {
439
+ chatClient?.sendResponse(request.id, true, {
440
+ ok: false,
441
+ error: "Session not found"
442
+ });
443
+ return;
444
+ }
445
+ if (params.userId && session.userId && session.userId !== params.userId) {
446
+ chatClient?.sendResponse(request.id, true, {
447
+ ok: false,
448
+ error: "Session not found"
449
+ });
450
+ return;
451
+ }
452
+ chatClient?.sendResponse(request.id, true, {
453
+ sessionId: session.sessionId,
454
+ agentId: session.agentId,
455
+ channel: session.channel,
456
+ createdAt: session.createdAt,
457
+ messages: session.messages.map((m) => ({
458
+ id: `msg-${String(m.timestamp)}`,
459
+ role: m.role,
460
+ content: m.content,
461
+ timestamp: m.timestamp
462
+ }))
463
+ });
464
+ } catch (err) {
465
+ const errMsg = err instanceof Error ? err.message : String(err);
466
+ log.error(`sessions.get failed: ${errMsg}`);
467
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
468
+ }
469
+ }
470
+ const plugin = {
471
+ id: "@alfe.ai/openclaw-chat",
472
+ name: "Chat Plugin",
473
+ description: "Chat conversation channel — web widget and mobile app share unified chat sessions",
474
+ version: "0.0.8",
475
+ activate(api) {
476
+ const log = api.logger;
477
+ const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
478
+ globalThis.__alfeChatPluginActivated = true;
479
+ const alfeChannel = createAlfeChannelPlugin();
480
+ api.registerChannel(alfeChannel);
481
+ log.info(`Registered channel: ${alfeChannel.id}`);
482
+ if (!alreadyActivated) {
483
+ log.info("Chat plugin registering...");
484
+ resolveOpenClawSdk(log);
485
+ pluginRuntime = api.runtime ?? null;
486
+ }
487
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
488
+ if (!alreadyActivated) connectingPromise = Promise.resolve().then(() => {
489
+ try {
490
+ const { apiKey, chatWsUrl } = (0, _alfe_ai_chat.resolveAlfeChat)({
491
+ apiKey: pluginConfig.apiKey,
492
+ chatWsUrl: pluginConfig.chatWsUrl
493
+ });
494
+ if (chatWsUrl && apiKey) {
495
+ log.info(`Connecting to chat service: ${chatWsUrl}`);
496
+ chatClient = new _alfe_ai_chat.ChatServiceClient({
497
+ wsUrl: chatWsUrl,
498
+ apiKey,
499
+ onRequest: (request) => {
500
+ if (request.method === "agent") handleAgentRequest(request, log);
501
+ else if (request.method === "sessions.list") handleSessionsList(request, log);
502
+ else if (request.method === "sessions.get") handleSessionsGet(request, log);
503
+ else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
504
+ },
505
+ onConnectionChange: (connected) => {
506
+ log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
507
+ },
508
+ logger: log
509
+ });
510
+ chatClient.start();
511
+ log.info("Chat service relay started");
512
+ } else log.info("Chat service URL not configured — running without chat service relay");
513
+ } catch (err) {
514
+ log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
515
+ }
516
+ });
517
+ if (typeof api.registerGatewayMethod === "function") {
518
+ api.registerGatewayMethod("sessions.list", async (...args) => {
519
+ const params = args[0];
520
+ return { sessions: await listSessions({
521
+ channel: params.channel,
522
+ tenantId: params.tenantId,
523
+ userId: params.userId
524
+ }) };
525
+ });
526
+ api.registerGatewayMethod("sessions.get", async (...args) => {
527
+ const params = args[0];
528
+ const session = await getSession(params.sessionId);
529
+ if (!session) return {
530
+ ok: false,
531
+ error: "Session not found"
532
+ };
533
+ if (params.userId && session.userId && session.userId !== params.userId) return {
534
+ ok: false,
535
+ error: "Session not found"
536
+ };
537
+ return {
538
+ sessionId: session.sessionId,
539
+ agentId: session.agentId,
540
+ channel: session.channel,
541
+ createdAt: session.createdAt,
542
+ messages: session.messages.map((m) => ({
543
+ id: `msg-${String(m.timestamp)}`,
544
+ role: m.role,
545
+ content: m.content,
546
+ timestamp: m.timestamp
547
+ }))
548
+ };
549
+ });
550
+ log.info("Registered gateway RPC methods: sessions.list, sessions.get");
551
+ }
552
+ if (!alreadyActivated) {
553
+ api.on("session_start", async (...eventArgs) => {
554
+ const key = eventArgs[0].sessionKey;
555
+ if (!key || isAlfeSessionKey(key)) return;
556
+ log.info(`Chat session starting: ${key}`);
557
+ await createSession(key, "", "alfe");
558
+ }, { priority: 50 });
559
+ api.on("message", async (...eventArgs) => {
560
+ const event = eventArgs[0];
561
+ const key = event.sessionKey;
562
+ if (!key || isAlfeSessionKey(key)) return;
563
+ await addMessage(key, event.role, event.content);
564
+ });
565
+ api.on("session_end", (...eventArgs) => {
566
+ const key = eventArgs[0].sessionKey;
567
+ if (!key || !isAlfeSessionKey(key)) return;
568
+ log.info(`Chat session ending: ${key}`);
569
+ });
570
+ }
571
+ log.info("Chat plugin registered");
572
+ },
573
+ async deactivate(api) {
574
+ globalThis.__alfeChatPluginActivated = false;
575
+ const log = api.logger;
576
+ log.info("Chat plugin deactivating...");
577
+ if (connectingPromise) {
578
+ await connectingPromise.catch((err) => {
579
+ api.logger.debug(`Connection attempt failed: ${err instanceof Error ? err.message : String(err)}`);
580
+ });
581
+ connectingPromise = null;
582
+ }
583
+ if (chatClient) {
584
+ chatClient.stop();
585
+ chatClient = null;
586
+ log.info("Chat service client stopped");
587
+ }
588
+ pluginRuntime = null;
589
+ dispatchInbound = null;
590
+ log.info("Chat plugin deactivated");
591
+ }
592
+ };
593
+ //#endregion
594
+ Object.defineProperty(exports, "createAlfeChannelPlugin", {
595
+ enumerable: true,
596
+ get: function() {
597
+ return createAlfeChannelPlugin;
598
+ }
599
+ });
600
+ Object.defineProperty(exports, "plugin", {
601
+ enumerable: true,
602
+ get: function() {
603
+ return plugin;
604
+ }
605
+ });
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.cjs";
2
+ export { plugin as default };
package/dist/plugin2.js CHANGED
@@ -274,6 +274,7 @@ async function listSessions(filters, limit = 50) {
274
274
  if (filters?.agentId && session.agentId !== filters.agentId) continue;
275
275
  if (filters?.channel && session.channel !== filters.channel) continue;
276
276
  if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
277
+ if (filters?.userId && session.userId !== filters.userId) continue;
277
278
  const lastMsg = session.messages.at(-1);
278
279
  summaries.push({
279
280
  sessionId: session.sessionId,
@@ -420,7 +421,8 @@ async function handleSessionsList(request, log) {
420
421
  const params = request.params;
421
422
  const sessions = await listSessions({
422
423
  channel: params.channel,
423
- tenantId: params.tenantId
424
+ tenantId: params.tenantId,
425
+ userId: params.userId
424
426
  });
425
427
  chatClient?.sendResponse(request.id, true, { sessions });
426
428
  } catch (err) {
@@ -440,6 +442,13 @@ async function handleSessionsGet(request, log) {
440
442
  });
441
443
  return;
442
444
  }
445
+ if (params.userId && session.userId && session.userId !== params.userId) {
446
+ chatClient?.sendResponse(request.id, true, {
447
+ ok: false,
448
+ error: "Session not found"
449
+ });
450
+ return;
451
+ }
443
452
  chatClient?.sendResponse(request.id, true, {
444
453
  sessionId: session.sessionId,
445
454
  agentId: session.agentId,
@@ -510,7 +519,8 @@ const plugin = {
510
519
  const params = args[0];
511
520
  return { sessions: await listSessions({
512
521
  channel: params.channel,
513
- tenantId: params.tenantId
522
+ tenantId: params.tenantId,
523
+ userId: params.userId
514
524
  }) };
515
525
  });
516
526
  api.registerGatewayMethod("sessions.get", async (...args) => {
@@ -520,6 +530,10 @@ const plugin = {
520
530
  ok: false,
521
531
  error: "Session not found"
522
532
  };
533
+ if (params.userId && session.userId && session.userId !== params.userId) return {
534
+ ok: false,
535
+ error: "Session not found"
536
+ };
523
537
  return {
524
538
  sessionId: session.sessionId,
525
539
  agentId: session.agentId,
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.15",
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.5"
29
31
  },
30
32
  "peerDependencies": {
31
33
  "openclaw": ">=2026.3.0"