@alfe.ai/openclaw-chat 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # packages/openclaw-chat (`@alfe.ai/openclaw-chat`)
2
+
3
+ OpenClaw chat plugin for Alfe — web widget and mobile app channels
4
+
5
+ ## What It Does
6
+
7
+ OpenClaw plugin that registers the `alfe-chat` channel with the OpenClaw agent runtime. Follows the standard OpenClaw plugin pattern:
8
+
9
+ - Exports a plugin object with `id`, `name`, `activate`, `deactivate`
10
+ - Connects to the Alfe daemon IPC for capability registration
11
+ - Gracefully degrades if the daemon is unavailable
12
+
13
+ ## Key Files
14
+
15
+ ```
16
+ src/
17
+ ├── plugin.ts # Plugin entry point (activate/deactivate lifecycle)
18
+ ├── index.ts # Public re-exports
19
+ └── types.ts # Plugin-specific type definitions
20
+ ```
21
+
22
+ ## Development
23
+
24
+ ```bash
25
+ pnpm install
26
+ pnpm --filter @alfe.ai/openclaw-chat build
27
+ ```
28
+
29
+ ## Testing
30
+
31
+ ```bash
32
+ pnpm --filter @alfe.ai/openclaw-chat test
33
+ ```
34
+
35
+ ## Dependencies
36
+
37
+ - **@alfe.ai/openclaw** — OpenClaw runtime plugin API
38
+ - **openclaw** — OpenClaw runtime plugin API
@@ -0,0 +1,39 @@
1
+ import { a as AlfeResolvedAccount, c as OpenClawConfig, d as createAlfeChannelPlugin, i as AlfePluginConfig, l as OpenClawModule, n as AlfeChannelAccountConfig, o as IPCClient, r as AlfeChannelConfig, s as Logger, t as plugin, u as OpenClawPluginApi } from "./plugin.js";
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
+ */
14
+ interface ChatMessage {
15
+ role: 'user' | 'assistant';
16
+ content: string;
17
+ timestamp: number;
18
+ }
19
+ interface SessionData {
20
+ sessionId: string;
21
+ agentId: string;
22
+ channel: string;
23
+ tenantId?: string;
24
+ userId?: string;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ messages: ChatMessage[];
28
+ }
29
+ interface SessionSummary {
30
+ sessionId: string;
31
+ agentId: string;
32
+ channel: string;
33
+ createdAt: string;
34
+ lastMessageAt?: string;
35
+ preview?: string;
36
+ messageCount: number;
37
+ }
38
+ //#endregion
39
+ export { AlfeChannelAccountConfig, AlfeChannelConfig, AlfePluginConfig, AlfeResolvedAccount, type ChatMessage, IPCClient, Logger, OpenClawConfig, OpenClawModule, OpenClawPluginApi, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default, plugin };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { n as createAlfeChannelPlugin, t as plugin } from "./plugin2.js";
2
+ export { createAlfeChannelPlugin, plugin as default, plugin };
@@ -0,0 +1,207 @@
1
+ //#region src/alfe-channel.d.ts
2
+
3
+ /**
4
+ * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
5
+ *
6
+ * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
7
+ * but is registered dynamically via api.registerChannel().
8
+ */
9
+ declare function createAlfeChannelPlugin(): {
10
+ id: string;
11
+ meta: {
12
+ id: string;
13
+ label: string;
14
+ selectionLabel: string;
15
+ detailLabel: string;
16
+ docsPath: string;
17
+ docsLabel: string;
18
+ blurb: string;
19
+ systemImage: string;
20
+ order: number;
21
+ aliases: string[];
22
+ forceAccountBinding: boolean;
23
+ showConfigured: boolean;
24
+ };
25
+ capabilities: {
26
+ chatTypes: ("direct" | "group")[];
27
+ reactions: boolean;
28
+ edit: boolean;
29
+ unsend: boolean;
30
+ reply: boolean;
31
+ effects: boolean;
32
+ groupManagement: boolean;
33
+ threads: boolean;
34
+ media: boolean;
35
+ nativeCommands: boolean;
36
+ polls: boolean;
37
+ };
38
+ config: {
39
+ /**
40
+ * List configured account IDs.
41
+ * Supports multi-account via channels.alfe.accounts, with a
42
+ * default account derived from the top-level channels.alfe section.
43
+ */
44
+ listAccountIds(cfg: OpenClawConfig): string[];
45
+ /**
46
+ * Resolve account config for a given account ID.
47
+ */
48
+ resolveAccount(cfg: OpenClawConfig, accountId?: string | null): AlfeResolvedAccount;
49
+ /**
50
+ * Default account ID.
51
+ */
52
+ defaultAccountId(): string;
53
+ /**
54
+ * Check if account is enabled.
55
+ */
56
+ isEnabled(account: AlfeResolvedAccount): boolean;
57
+ /**
58
+ * Check if account is configured (always true for Alfe — no external tokens needed).
59
+ */
60
+ isConfigured(): boolean;
61
+ /**
62
+ * Describe the account state for status display.
63
+ */
64
+ describeAccount(account: AlfeResolvedAccount): {
65
+ accountId: string;
66
+ enabled: boolean;
67
+ configured: boolean;
68
+ dmPolicy: string | undefined;
69
+ };
70
+ /**
71
+ * Resolve allow-from list for an account.
72
+ */
73
+ resolveAllowFrom(params: {
74
+ cfg: OpenClawConfig;
75
+ accountId?: string | null;
76
+ }): string[];
77
+ /**
78
+ * Resolve default outbound target.
79
+ */
80
+ resolveDefaultTo(params: {
81
+ cfg: OpenClawConfig;
82
+ accountId?: string | null;
83
+ }): string | undefined;
84
+ };
85
+ /**
86
+ * Outbound delivery via gateway.
87
+ * The chat relay service on Fly.io handles actual delivery
88
+ * to connected web/mobile clients via the gateway.
89
+ */
90
+ outbound: {
91
+ deliveryMode: "gateway";
92
+ textChunkLimit: number;
93
+ };
94
+ /**
95
+ * Setup adapter — minimal for Alfe since no external tokens are needed.
96
+ */
97
+ setup: {
98
+ resolveAccountId(params: {
99
+ cfg: OpenClawConfig;
100
+ accountId?: string;
101
+ input?: Record<string, unknown>;
102
+ }): string;
103
+ applyAccountConfig(params: {
104
+ cfg: OpenClawConfig;
105
+ accountId: string;
106
+ input: Record<string, unknown>;
107
+ }): OpenClawConfig;
108
+ };
109
+ };
110
+ //#endregion
111
+ //#region src/types.d.ts
112
+ /**
113
+ * Types for the Alfe chat channel plugin.
114
+ *
115
+ * The Alfe channel registers with OpenClaw as a first-class channel,
116
+ * allowing web and mobile clients to share conversation sessions.
117
+ */
118
+ interface AlfeChannelAccountConfig {
119
+ /** Whether this account is enabled. */
120
+ enabled?: boolean;
121
+ /** Allowed sender identifiers (user IDs, email addresses). */
122
+ allowFrom?: string | string[];
123
+ /** Default delivery target. */
124
+ defaultTo?: string;
125
+ /** DM policy (open, allowlist, etc.). */
126
+ dmPolicy?: string;
127
+ }
128
+ interface AlfeChannelConfig {
129
+ /** Whether the Alfe channel is enabled. */
130
+ enabled?: boolean;
131
+ /** Allowed sender identifiers. */
132
+ allowFrom?: string | string[];
133
+ /** Default delivery target for outbound messages. */
134
+ defaultTo?: string;
135
+ /** DM policy. */
136
+ dmPolicy?: string;
137
+ /** Named accounts (multi-account support). */
138
+ accounts?: Record<string, AlfeChannelAccountConfig>;
139
+ }
140
+ interface AlfeResolvedAccount {
141
+ accountId: string;
142
+ enabled: boolean;
143
+ allowFrom: string[];
144
+ defaultTo?: string;
145
+ dmPolicy?: string;
146
+ }
147
+ interface AlfePluginConfig {
148
+ /** Alfe daemon IPC socket path override. */
149
+ daemonSocket?: string;
150
+ /** Agent ID this plugin is associated with. */
151
+ agentId?: string;
152
+ }
153
+ interface Logger {
154
+ info(msg: string, ...args: unknown[]): void;
155
+ warn(msg: string, ...args: unknown[]): void;
156
+ error(msg: string, ...args: unknown[]): void;
157
+ debug(msg: string, ...args: unknown[]): void;
158
+ }
159
+ interface OpenClawConfig {
160
+ channels?: {
161
+ alfe?: AlfeChannelConfig;
162
+ [key: string]: unknown;
163
+ };
164
+ plugins?: {
165
+ entries?: Record<string, {
166
+ config?: AlfePluginConfig;
167
+ [key: string]: unknown;
168
+ }>;
169
+ [key: string]: unknown;
170
+ };
171
+ [key: string]: unknown;
172
+ }
173
+ interface OpenClawPluginApi {
174
+ logger: Logger;
175
+ config?: OpenClawConfig;
176
+ registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
177
+ registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
178
+ on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
179
+ priority?: number;
180
+ }): void;
181
+ }
182
+ interface IPCClient {
183
+ on(event: string, handler: (...args: unknown[]) => void | Promise<void>): void;
184
+ start(): void;
185
+ stop(): void;
186
+ request(method: string, params: Record<string, unknown>): Promise<{
187
+ ok: boolean;
188
+ error?: {
189
+ message: string;
190
+ };
191
+ }>;
192
+ }
193
+ interface OpenClawModule {
194
+ IPCClient: new (socketPath: string, log: Logger) => IPCClient;
195
+ }
196
+ //#endregion
197
+ //#region src/plugin.d.ts
198
+ declare const plugin: {
199
+ id: string;
200
+ name: string;
201
+ description: string;
202
+ version: string;
203
+ activate(api: OpenClawPluginApi): Promise<void>;
204
+ deactivate(api: OpenClawPluginApi): void;
205
+ };
206
+ //#endregion
207
+ export { AlfeResolvedAccount as a, OpenClawConfig as c, createAlfeChannelPlugin as d, AlfePluginConfig as i, OpenClawModule as l, AlfeChannelAccountConfig as n, IPCClient as o, AlfeChannelConfig as r, Logger as s, plugin as t, OpenClawPluginApi as u };
package/dist/plugin.js ADDED
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin2.js";
2
+ export { plugin as default };
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.js";
2
+ export { plugin as default };
@@ -0,0 +1,381 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ //#region src/alfe-channel.ts
6
+ const CHANNEL_ID = "alfe";
7
+ const DEFAULT_ACCOUNT_ID = "default";
8
+ function getChannelSection(cfg) {
9
+ return cfg.channels?.alfe ?? {};
10
+ }
11
+ function normalizeAllowFrom(raw) {
12
+ if (!raw) return [];
13
+ if (typeof raw === "string") return [raw];
14
+ return raw;
15
+ }
16
+ /**
17
+ * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
18
+ *
19
+ * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
20
+ * but is registered dynamically via api.registerChannel().
21
+ */
22
+ function createAlfeChannelPlugin() {
23
+ return {
24
+ id: CHANNEL_ID,
25
+ meta: {
26
+ id: CHANNEL_ID,
27
+ label: "Alfe",
28
+ selectionLabel: "Alfe (Web & Mobile)",
29
+ detailLabel: "Alfe Chat",
30
+ docsPath: "/channels/alfe",
31
+ docsLabel: "alfe",
32
+ blurb: "Alfe native chat — web widget and mobile app conversations.",
33
+ systemImage: "bubble.left.and.text.bubble.right",
34
+ order: 100,
35
+ aliases: ["alfe-web", "alfe-mobile"],
36
+ forceAccountBinding: false,
37
+ showConfigured: true
38
+ },
39
+ capabilities: {
40
+ chatTypes: ["direct", "group"],
41
+ reactions: false,
42
+ edit: false,
43
+ unsend: false,
44
+ reply: true,
45
+ effects: false,
46
+ groupManagement: false,
47
+ threads: false,
48
+ media: true,
49
+ nativeCommands: false,
50
+ polls: false
51
+ },
52
+ config: {
53
+ listAccountIds(cfg) {
54
+ const section = getChannelSection(cfg);
55
+ const ids = [];
56
+ if (section.enabled !== false && (section.allowFrom ?? section.defaultTo ?? section.dmPolicy)) ids.push(DEFAULT_ACCOUNT_ID);
57
+ if (section.accounts) {
58
+ for (const id of Object.keys(section.accounts)) if (!ids.includes(id)) ids.push(id);
59
+ }
60
+ if (ids.length === 0 && section.enabled !== false) ids.push(DEFAULT_ACCOUNT_ID);
61
+ return ids;
62
+ },
63
+ resolveAccount(cfg, accountId) {
64
+ const section = getChannelSection(cfg);
65
+ const id = accountId ?? DEFAULT_ACCOUNT_ID;
66
+ const accountSection = section.accounts?.[id];
67
+ if (accountSection) return {
68
+ accountId: id,
69
+ enabled: accountSection.enabled !== false,
70
+ allowFrom: normalizeAllowFrom(accountSection.allowFrom),
71
+ defaultTo: accountSection.defaultTo,
72
+ dmPolicy: accountSection.dmPolicy
73
+ };
74
+ return {
75
+ accountId: id,
76
+ enabled: section.enabled !== false,
77
+ allowFrom: normalizeAllowFrom(section.allowFrom),
78
+ defaultTo: section.defaultTo,
79
+ dmPolicy: section.dmPolicy
80
+ };
81
+ },
82
+ defaultAccountId() {
83
+ return DEFAULT_ACCOUNT_ID;
84
+ },
85
+ isEnabled(account) {
86
+ return account.enabled;
87
+ },
88
+ isConfigured() {
89
+ return true;
90
+ },
91
+ describeAccount(account) {
92
+ return {
93
+ accountId: account.accountId,
94
+ enabled: account.enabled,
95
+ configured: true,
96
+ dmPolicy: account.dmPolicy
97
+ };
98
+ },
99
+ resolveAllowFrom(params) {
100
+ const section = getChannelSection(params.cfg);
101
+ const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
102
+ return normalizeAllowFrom((section.accounts?.[id])?.allowFrom ?? section.allowFrom);
103
+ },
104
+ resolveDefaultTo(params) {
105
+ const section = getChannelSection(params.cfg);
106
+ const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
107
+ return section.accounts?.[id]?.defaultTo ?? section.defaultTo;
108
+ }
109
+ },
110
+ outbound: {
111
+ deliveryMode: "gateway",
112
+ textChunkLimit: 4e3
113
+ },
114
+ setup: {
115
+ resolveAccountId(params) {
116
+ return params.accountId ?? DEFAULT_ACCOUNT_ID;
117
+ },
118
+ applyAccountConfig(params) {
119
+ const cfg = { ...params.cfg };
120
+ cfg.channels ??= {};
121
+ cfg.channels.alfe ??= {};
122
+ const section = cfg.channels.alfe;
123
+ if (params.accountId === DEFAULT_ACCOUNT_ID) section.enabled = true;
124
+ else {
125
+ section.accounts ??= {};
126
+ section.accounts[params.accountId] = {
127
+ enabled: true,
128
+ ...params.input
129
+ };
130
+ }
131
+ return cfg;
132
+ }
133
+ }
134
+ };
135
+ }
136
+ //#endregion
137
+ //#region src/session-store.ts
138
+ /**
139
+ * Session Store — persists chat sessions to the local filesystem.
140
+ *
141
+ * Storage layout:
142
+ * ~/.alfe/sessions/chat/{sessionId}.json
143
+ *
144
+ * Each session file contains metadata and the full message history.
145
+ * Sessions are written on every message to ensure durability.
146
+ */
147
+ const SESSIONS_DIR = join(homedir(), ".alfe", "sessions", "chat");
148
+ async function ensureDir() {
149
+ if (!existsSync(SESSIONS_DIR)) await mkdir(SESSIONS_DIR, { recursive: true });
150
+ }
151
+ function sessionPath(sessionId) {
152
+ return join(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
153
+ }
154
+ async function getSession(sessionId) {
155
+ try {
156
+ const data = await readFile(sessionPath(sessionId), "utf-8");
157
+ return JSON.parse(data);
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ async function saveSession(session) {
163
+ await ensureDir();
164
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
165
+ await writeFile(sessionPath(session.sessionId), JSON.stringify(session, null, 2), "utf-8");
166
+ }
167
+ async function createSession(sessionId, agentId, channel, tenantId, userId) {
168
+ const now = (/* @__PURE__ */ new Date()).toISOString();
169
+ const session = {
170
+ sessionId,
171
+ agentId,
172
+ channel,
173
+ tenantId,
174
+ userId,
175
+ createdAt: now,
176
+ updatedAt: now,
177
+ messages: []
178
+ };
179
+ await saveSession(session);
180
+ return session;
181
+ }
182
+ async function addMessage(sessionId, role, content) {
183
+ const session = await getSession(sessionId);
184
+ if (!session) return;
185
+ session.messages.push({
186
+ role,
187
+ content,
188
+ timestamp: Date.now()
189
+ });
190
+ await saveSession(session);
191
+ }
192
+ async function listSessions(filters) {
193
+ await ensureDir();
194
+ let files;
195
+ try {
196
+ files = await readdir(SESSIONS_DIR);
197
+ } catch {
198
+ return [];
199
+ }
200
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
201
+ const summaries = [];
202
+ for (const file of jsonFiles) try {
203
+ const data = await readFile(join(SESSIONS_DIR, file), "utf-8");
204
+ const session = JSON.parse(data);
205
+ if (filters?.agentId && session.agentId !== filters.agentId) continue;
206
+ if (filters?.channel && session.channel !== filters.channel) continue;
207
+ if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
208
+ const lastMsg = session.messages.at(-1);
209
+ summaries.push({
210
+ sessionId: session.sessionId,
211
+ agentId: session.agentId,
212
+ channel: session.channel,
213
+ createdAt: session.createdAt,
214
+ lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
215
+ preview: lastMsg?.content.slice(0, 100),
216
+ messageCount: session.messages.length
217
+ });
218
+ } catch {}
219
+ summaries.sort((a, b) => {
220
+ const aTime = a.lastMessageAt ?? a.createdAt;
221
+ return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
222
+ });
223
+ return summaries;
224
+ }
225
+ //#endregion
226
+ //#region src/plugin.ts
227
+ /**
228
+ * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
229
+ *
230
+ * Registers the 'alfe' channel with OpenClaw, creating a first-class
231
+ * channel for Alfe conversations. Web and mobile clients share the
232
+ * same channel and conversation sessions.
233
+ *
234
+ * This follows the same pattern as the voice-gateway plugin:
235
+ * - Registers channel via api.registerChannel()
236
+ * - Connects to the Alfe daemon IPC for capability registration
237
+ * - Registers gateway RPC methods for message delivery and session queries
238
+ * - Hooks into session lifecycle events
239
+ * - Persists chat sessions to ~/.alfe/sessions/chat/
240
+ * - Gracefully degrades if the daemon is unavailable
241
+ */
242
+ const DEFAULT_SOCKET_PATH = join(homedir(), ".alfe", "gateway.sock");
243
+ const CHAT_CAPABILITIES = [
244
+ "chat.web",
245
+ "chat.mobile",
246
+ "chat.sessions"
247
+ ];
248
+ let daemonIpcClient = null;
249
+ /**
250
+ * Attempt to connect to the Alfe daemon IPC socket.
251
+ * Returns null if @alfe.ai/openclaw is not available or daemon is unreachable.
252
+ */
253
+ async function connectToDaemon(socketPath, log) {
254
+ try {
255
+ const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
256
+ const client = new IPCClientCtor(socketPath, log);
257
+ client.on("connected", async () => {
258
+ log.info("Connected to Alfe daemon — registering chat capabilities...");
259
+ const response = await client.request("capability.register", {
260
+ plugin: "@alfe.ai/openclaw-chat",
261
+ capabilities: [...CHAT_CAPABILITIES]
262
+ });
263
+ if (response.ok) log.info("Chat capabilities registered with daemon");
264
+ else log.warn(`Failed to register chat capabilities: ${response.error?.message ?? "unknown"}`);
265
+ });
266
+ client.on("disconnected", (reason) => {
267
+ log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
268
+ });
269
+ client.on("error", (err) => {
270
+ log.debug(`Daemon IPC error: ${err.message}`);
271
+ });
272
+ client.start();
273
+ return client;
274
+ } catch {
275
+ log.info("Alfe daemon not available — chat plugin running standalone");
276
+ return null;
277
+ }
278
+ }
279
+ const plugin = {
280
+ id: "@alfe.ai/openclaw-chat",
281
+ name: "Alfe Chat Plugin",
282
+ description: "Alfe conversation channel — web widget and mobile app share unified chat sessions",
283
+ version: "0.3.0",
284
+ async activate(api) {
285
+ if (globalThis.__alfeChatPluginActivated) {
286
+ api.logger.debug("Alfe Chat plugin already activated, skipping re-init");
287
+ return;
288
+ }
289
+ globalThis.__alfeChatPluginActivated = true;
290
+ const log = api.logger;
291
+ log.info("Alfe Chat plugin activating...");
292
+ const pluginConfig = (api.config ?? {}).plugins?.entries?.["@alfe.ai/openclaw-chat"]?.config ?? {};
293
+ const alfeChannel = createAlfeChannelPlugin();
294
+ api.registerChannel(alfeChannel);
295
+ log.info(`Registered channel: ${alfeChannel.id} (${alfeChannel.meta.label})`);
296
+ daemonIpcClient = await connectToDaemon(pluginConfig.daemonSocket ?? process.env.ALFE_GATEWAY_SOCKET ?? DEFAULT_SOCKET_PATH, log);
297
+ if (typeof api.registerGatewayMethod === "function") {
298
+ api.registerGatewayMethod("chat.send", (...args) => {
299
+ const { sessionId, content, clientType } = args[0];
300
+ log.info(`chat.send RPC: session=${sessionId}, client=${clientType ?? "unknown"}, content=${content.slice(0, 50)}...`);
301
+ return Promise.resolve({
302
+ ok: true,
303
+ sessionId,
304
+ channel: "alfe"
305
+ });
306
+ });
307
+ log.info("Registered gateway RPC method: chat.send");
308
+ api.registerGatewayMethod("sessions.list", async (...args) => {
309
+ const params = args[0];
310
+ log.info(`sessions.list RPC: agentId=${params.agentId ?? "*"}, channel=${params.channel ?? "*"}`);
311
+ return { sessions: await listSessions({
312
+ agentId: params.agentId,
313
+ channel: params.channel,
314
+ tenantId: params.tenantId
315
+ }) };
316
+ });
317
+ log.info("Registered gateway RPC method: sessions.list");
318
+ api.registerGatewayMethod("sessions.get", async (...args) => {
319
+ const params = args[0];
320
+ log.info(`sessions.get RPC: sessionId=${params.sessionId}`);
321
+ const session = await getSession(params.sessionId);
322
+ if (!session) return {
323
+ ok: false,
324
+ error: "Session not found"
325
+ };
326
+ return {
327
+ sessionId: session.sessionId,
328
+ agentId: session.agentId,
329
+ channel: session.channel,
330
+ createdAt: session.createdAt,
331
+ messages: session.messages.map((m) => ({
332
+ id: `msg-${String(m.timestamp)}`,
333
+ role: m.role,
334
+ content: m.content,
335
+ timestamp: m.timestamp
336
+ }))
337
+ };
338
+ });
339
+ log.info("Registered gateway RPC method: sessions.get");
340
+ }
341
+ api.on("session_start", async (...eventArgs) => {
342
+ const key = eventArgs[0].sessionKey;
343
+ if (!key) return;
344
+ if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
345
+ log.info(`Alfe chat session starting: ${key}`);
346
+ const parts = key.split("-");
347
+ await createSession(key, parts.length >= 3 ? parts[2] : "", "alfe", parts.length >= 2 ? parts[1] : "");
348
+ }, { priority: 50 });
349
+ api.on("message", async (...eventArgs) => {
350
+ const event = eventArgs[0];
351
+ const key = event.sessionKey;
352
+ if (!key) return;
353
+ if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
354
+ await addMessage(key, event.role, event.content);
355
+ });
356
+ api.on("session_end", (...eventArgs) => {
357
+ const key = eventArgs[0].sessionKey;
358
+ if (!key) return;
359
+ if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
360
+ log.info(`Alfe chat session ending: ${key}`);
361
+ });
362
+ log.info("Alfe Chat plugin activated");
363
+ },
364
+ deactivate(api) {
365
+ globalThis.__alfeChatPluginActivated = false;
366
+ const log = api.logger;
367
+ log.info("Alfe Chat plugin deactivating...");
368
+ if (daemonIpcClient) {
369
+ try {
370
+ daemonIpcClient.stop();
371
+ log.info("Disconnected from Alfe daemon");
372
+ } catch (err) {
373
+ log.debug(`Error disconnecting from daemon: ${err.message}`);
374
+ }
375
+ daemonIpcClient = null;
376
+ }
377
+ log.info("Alfe Chat plugin deactivated");
378
+ }
379
+ };
380
+ //#endregion
381
+ export { createAlfeChannelPlugin as n, plugin as t };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@alfe.ai/openclaw-chat",
3
+ "version": "0.0.1",
4
+ "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
+ "type": "module",
6
+ "main": "./dist/plugin.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./plugin": {
14
+ "import": "./dist/plugin.js",
15
+ "types": "./dist/plugin.d.ts"
16
+ }
17
+ },
18
+ "openclaw": {
19
+ "extensions": [
20
+ "./dist/plugin.js"
21
+ ]
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "license": "UNLICENSED",
27
+ "scripts": {
28
+ "build": "tsdown",
29
+ "dev": "tsdown --watch",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "typecheck": "tsc --noEmit",
33
+ "lint": "eslint ."
34
+ }
35
+ }