@alfe.ai/openclaw-chat 0.0.7 → 0.0.9

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/plugin.d.ts CHANGED
@@ -192,7 +192,25 @@ interface PluginRuntime {
192
192
  error?: string;
193
193
  }>;
194
194
  };
195
- channel: unknown;
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
+ };
196
214
  }
197
215
  interface AgentEventPayload {
198
216
  runId: string;
@@ -218,7 +236,7 @@ declare const plugin: {
218
236
  description: string;
219
237
  version: string;
220
238
  activate(api: PluginApi): void;
221
- deactivate(api: PluginApi): void;
239
+ deactivate(api: PluginApi): Promise<void>;
222
240
  };
223
241
  //#endregion
224
242
  export { AlfeResolvedAccount as a, AlfePluginConfig as i, createAlfeChannelPlugin as n, AlfeChannelConfig as r, plugin as t };
package/dist/plugin2.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
2
3
  import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
3
4
  import { join } from "node:path";
@@ -137,33 +138,27 @@ function createAlfeChannelPlugin() {
137
138
  //#endregion
138
139
  //#region src/session-keys.ts
139
140
  /**
140
- * Session key helpers — handles both raw and prefixed formats.
141
+ * Session key helpers — handles both legacy and canonical OpenClaw formats.
141
142
  *
142
- * Raw format: "chat-{tenantId}-{agentId}-{suffix}"
143
- * Prefixed format: "agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}"
143
+ * Canonical format (from resolveAgentRoute):
144
+ * agent:{agentId}:alfe:direct:{userId}
145
+ * agent:{agentId}:alfe:direct:{userId}:thread:{conversationId}
144
146
  *
145
- * The chat adapter wraps keys with "agent:{agentId}:" before sending
146
- * to the daemon. The plugin may receive either format depending on
147
- * which OpenClaw event fires.
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.
148
153
  */
149
154
  /**
150
155
  * Check if a session key belongs to the Alfe chat channel.
151
- * Handles both raw and prefixed formats.
156
+ * Handles both canonical and legacy formats.
152
157
  */
153
158
  function isAlfeSessionKey(key) {
154
- return key.includes("chat-") || key.includes("alfe:") || key.includes(":alfe:");
155
- }
156
- /**
157
- * Strip the "agent:{agentId}:" prefix and extract tenantId + agentId.
158
- * Returns empty strings if the key doesn't match the expected format.
159
- */
160
- function parseAlfeSessionKey(key) {
161
- const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
162
- const match = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
163
- return {
164
- tenantId: match?.[1] ?? "",
165
- agentId: match?.[2] ?? ""
166
- };
159
+ if (key.includes(":alfe:")) return true;
160
+ if (key.includes("chat-")) return true;
161
+ return false;
167
162
  }
168
163
  //#endregion
169
164
  //#region src/session-store.ts
@@ -300,51 +295,104 @@ async function listSessions(filters, limit = 50) {
300
295
  * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
301
296
  *
302
297
  * Registers the 'alfe' channel with OpenClaw. Messages are dispatched
303
- * in-process via runtime.subagent.run() — no separate WebSocket to the
304
- * runtime needed.
298
+ * through OpenClaw's auto-reply pipeline via dispatchInboundDirectDmWithRuntime(),
299
+ * the same API built-in channels (Slack, Discord, Telegram) use.
305
300
  *
306
301
  * Architecture:
307
- * Chat Service (Fly.io) ←WS→ ChatServiceClient → runtime.subagent.run() → Agent (in-process)
308
- * onAgentEvent streaming
302
+ * Chat Service (Fly.io) ←WS→ ChatServiceClient
303
+ * dispatchInboundDirectDmWithRuntime() Agent (auto-reply pipeline)
304
+ * ← onAgentEvent streaming ← (real-time deltas)
305
+ * ← deliver() callback ← (final response)
309
306
  */
307
+ let dispatchInbound = null;
308
+ /**
309
+ * Resolve OpenClaw SDK functions from the global install.
310
+ * The openclaw package is installed globally (/usr/lib/node_modules/openclaw/)
311
+ * but is NOT in the plugin's node_modules. Built-in extensions can import
312
+ * directly; external plugins must use createRequire.
313
+ */
314
+ function resolveOpenClawSdk(log) {
315
+ for (const globalPath of ["/usr/lib/node_modules/openclaw/package.json", "/usr/local/lib/node_modules/openclaw/package.json"]) try {
316
+ const channelInbound = createRequire(globalPath)("openclaw/plugin-sdk/channel-inbound");
317
+ if (channelInbound.dispatchInboundDirectDmWithRuntime) {
318
+ dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
319
+ log.info(`Resolved OpenClaw SDK from ${globalPath}`);
320
+ return;
321
+ }
322
+ } catch {}
323
+ log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
324
+ }
310
325
  let pluginRuntime = null;
311
326
  let chatClient = null;
327
+ let connectingPromise = null;
312
328
  async function handleAgentRequest(request, log) {
313
329
  const runtime = pluginRuntime;
314
330
  if (!runtime) {
315
331
  chatClient?.sendResponse(request.id, false, { message: "Plugin runtime not initialized" });
316
332
  return;
317
333
  }
318
- const { message, sessionKey } = request.params;
319
- if (!message || !sessionKey) {
320
- chatClient?.sendResponse(request.id, false, { message: "Missing message or sessionKey" });
334
+ if (!dispatchInbound) {
335
+ chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
321
336
  return;
322
337
  }
338
+ const { message, sessionKey: legacySessionKey, userId, conversationId, tenantId, clientType } = request.params;
339
+ if (!message) {
340
+ chatClient?.sendResponse(request.id, false, { message: "Missing message" });
341
+ return;
342
+ }
343
+ const senderId = userId ?? `anon-${legacySessionKey.slice(0, 12)}`;
344
+ const cfg = runtime.config.loadConfig();
345
+ let resolvedSessionKey = legacySessionKey;
323
346
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
324
- if (evt.sessionKey !== sessionKey) return;
347
+ if (!evt.sessionKey) return;
348
+ if (resolvedSessionKey && evt.sessionKey !== resolvedSessionKey) return;
325
349
  if (evt.stream === "assistant") chatClient?.sendEvent("chat", {
326
350
  runId: evt.runId,
327
- sessionKey,
351
+ sessionKey: evt.sessionKey,
328
352
  seq: evt.seq,
329
353
  state: "delta",
330
354
  message: evt.data
331
355
  });
332
356
  });
333
357
  try {
334
- const { runId } = await runtime.subagent.run({
335
- sessionKey,
336
- message,
337
- deliver: true
338
- });
339
- const result = await runtime.subagent.waitForRun({
340
- runId,
341
- timeoutMs: 12e4
342
- });
343
- if (result.status === "ok") chatClient?.sendResponse(request.id, true, {
344
- text: "",
345
- sessionKey
346
- });
347
- else chatClient?.sendResponse(request.id, false, { message: result.error ?? `Agent run ${result.status}` });
358
+ const conversationLabel = userId ? `Chat with ${userId}` : `Alfe chat`;
359
+ resolvedSessionKey = (await dispatchInbound({
360
+ cfg,
361
+ runtime: { channel: runtime.channel },
362
+ channel: "alfe",
363
+ channelLabel: "Alfe",
364
+ accountId: "default",
365
+ peer: {
366
+ kind: "direct",
367
+ id: senderId
368
+ },
369
+ senderId,
370
+ senderAddress: `user:${senderId}`,
371
+ recipientAddress: "agent",
372
+ conversationLabel,
373
+ rawBody: message,
374
+ messageId: request.id,
375
+ timestamp: Date.now(),
376
+ extraContext: {
377
+ ...tenantId ? { TenantId: tenantId } : {},
378
+ ...clientType ? { ClientType: clientType } : {},
379
+ ...conversationId ? { ConversationId: conversationId } : {}
380
+ },
381
+ deliver: (payload) => {
382
+ chatClient?.sendResponse(request.id, true, {
383
+ text: payload.text ?? "",
384
+ sessionKey: resolvedSessionKey
385
+ });
386
+ return Promise.resolve();
387
+ },
388
+ onRecordError: (err) => {
389
+ log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
390
+ },
391
+ onDispatchError: (err, info) => {
392
+ log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
393
+ }
394
+ })).route.sessionKey;
395
+ log.info(`Agent dispatch complete: sessionKey=${resolvedSessionKey}`);
348
396
  } catch (err) {
349
397
  const errMsg = err instanceof Error ? err.message : String(err);
350
398
  log.error(`Agent dispatch failed: ${errMsg}`);
@@ -353,25 +401,70 @@ async function handleAgentRequest(request, log) {
353
401
  unsubscribe();
354
402
  }
355
403
  }
404
+ async function handleSessionsList(request, log) {
405
+ try {
406
+ const params = request.params;
407
+ const sessions = await listSessions({
408
+ agentId: params.agentId,
409
+ channel: params.channel,
410
+ tenantId: params.tenantId
411
+ });
412
+ chatClient?.sendResponse(request.id, true, { sessions });
413
+ } catch (err) {
414
+ const errMsg = err instanceof Error ? err.message : String(err);
415
+ log.error(`sessions.list failed: ${errMsg}`);
416
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
417
+ }
418
+ }
419
+ async function handleSessionsGet(request, log) {
420
+ try {
421
+ const params = request.params;
422
+ const session = await getSession(params.sessionId);
423
+ if (!session) {
424
+ chatClient?.sendResponse(request.id, true, {
425
+ ok: false,
426
+ error: "Session not found"
427
+ });
428
+ return;
429
+ }
430
+ chatClient?.sendResponse(request.id, true, {
431
+ sessionId: session.sessionId,
432
+ agentId: session.agentId,
433
+ channel: session.channel,
434
+ createdAt: session.createdAt,
435
+ messages: session.messages.map((m) => ({
436
+ id: `msg-${String(m.timestamp)}`,
437
+ role: m.role,
438
+ content: m.content,
439
+ timestamp: m.timestamp
440
+ }))
441
+ });
442
+ } catch (err) {
443
+ const errMsg = err instanceof Error ? err.message : String(err);
444
+ log.error(`sessions.get failed: ${errMsg}`);
445
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
446
+ }
447
+ }
356
448
  const plugin = {
357
449
  id: "@alfe.ai/openclaw-chat",
358
450
  name: "Alfe Chat Plugin",
359
451
  description: "Alfe conversation channel — web widget and mobile app share unified chat sessions",
360
- version: "0.3.0",
452
+ version: "0.0.8",
361
453
  activate(api) {
362
454
  if (globalThis.__alfeChatPluginActivated) {
363
455
  api.logger.debug("Alfe Chat plugin already activated, skipping re-init");
364
456
  return;
365
457
  }
366
458
  globalThis.__alfeChatPluginActivated = true;
367
- pluginRuntime = api.runtime ?? null;
368
459
  const log = api.logger;
369
460
  log.info("Alfe Chat plugin registering...");
461
+ resolveOpenClawSdk(log);
462
+ pluginRuntime = api.runtime ?? null;
370
463
  const alfeChannel = createAlfeChannelPlugin();
371
464
  api.registerChannel(alfeChannel);
372
465
  log.info(`Registered channel: ${alfeChannel.id}`);
373
466
  const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
374
- (async () => {
467
+ connectingPromise = (async () => {
375
468
  try {
376
469
  const { apiKey, chatWsUrl } = await resolveAlfeChat({
377
470
  apiKey: pluginConfig.apiKey,
@@ -384,6 +477,9 @@ const plugin = {
384
477
  apiKey,
385
478
  onRequest: (request) => {
386
479
  if (request.method === "agent") handleAgentRequest(request, log);
480
+ else if (request.method === "sessions.list") handleSessionsList(request, log);
481
+ else if (request.method === "sessions.get") handleSessionsGet(request, log);
482
+ else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
387
483
  },
388
484
  onConnectionChange: (connected) => {
389
485
  log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
@@ -432,8 +528,7 @@ const plugin = {
432
528
  const key = eventArgs[0].sessionKey;
433
529
  if (!key || !isAlfeSessionKey(key)) return;
434
530
  log.info(`Alfe chat session starting: ${key}`);
435
- const { tenantId, agentId: parsedAgentId } = parseAlfeSessionKey(key);
436
- await createSession(key, parsedAgentId, "alfe", tenantId);
531
+ await createSession(key, "", "alfe");
437
532
  }, { priority: 50 });
438
533
  api.on("message", async (...eventArgs) => {
439
534
  const event = eventArgs[0];
@@ -448,16 +543,23 @@ const plugin = {
448
543
  });
449
544
  log.info("Alfe Chat plugin registered");
450
545
  },
451
- deactivate(api) {
546
+ async deactivate(api) {
452
547
  globalThis.__alfeChatPluginActivated = false;
453
548
  const log = api.logger;
454
549
  log.info("Alfe Chat plugin deactivating...");
550
+ if (connectingPromise) {
551
+ await connectingPromise.catch((err) => {
552
+ api.logger.debug(`Connection attempt failed: ${err instanceof Error ? err.message : String(err)}`);
553
+ });
554
+ connectingPromise = null;
555
+ }
455
556
  if (chatClient) {
456
557
  chatClient.stop();
457
558
  chatClient = null;
458
559
  log.info("Chat service client stopped");
459
560
  }
460
561
  pluginRuntime = null;
562
+ dispatchInbound = null;
461
563
  log.info("Alfe Chat plugin deactivated");
462
564
  }
463
565
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -27,6 +27,14 @@
27
27
  "dependencies": {
28
28
  "@alfe.ai/chat": "^0.0.3"
29
29
  },
30
+ "peerDependencies": {
31
+ "openclaw": ">=2026.3.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "openclaw": {
35
+ "optional": true
36
+ }
37
+ },
30
38
  "license": "UNLICENSED",
31
39
  "scripts": {
32
40
  "build": "tsdown",