@alfe.ai/openclaw-chat 0.0.33 → 0.1.0

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.d.cts CHANGED
@@ -33,6 +33,8 @@ interface SessionSummary {
33
33
  sessionId: string;
34
34
  agentId: string;
35
35
  channel: string;
36
+ tenantId?: string;
37
+ userId?: string;
36
38
  createdAt: string;
37
39
  lastMessageAt?: string;
38
40
  preview?: string;
package/dist/index.d.ts CHANGED
@@ -33,6 +33,8 @@ interface SessionSummary {
33
33
  sessionId: string;
34
34
  agentId: string;
35
35
  channel: string;
36
+ tenantId?: string;
37
+ userId?: string;
36
38
  createdAt: string;
37
39
  lastMessageAt?: string;
38
40
  preview?: string;
package/dist/plugin.d.cts CHANGED
@@ -9,31 +9,21 @@
9
9
  interface AlfeChannelAccountConfig {
10
10
  /** Whether this account is enabled. */
11
11
  enabled?: boolean;
12
- /** Allowed sender identifiers (user IDs, email addresses). */
13
- allowFrom?: string | string[];
14
12
  /** Default delivery target. */
15
13
  defaultTo?: string;
16
- /** DM policy (open, allowlist, etc.). */
17
- dmPolicy?: string;
18
14
  }
19
15
  interface AlfeChannelConfig {
20
16
  /** Whether the Alfe channel is enabled. */
21
17
  enabled?: boolean;
22
- /** Allowed sender identifiers. */
23
- allowFrom?: string | string[];
24
18
  /** Default delivery target for outbound messages. */
25
19
  defaultTo?: string;
26
- /** DM policy. */
27
- dmPolicy?: string;
28
20
  /** Named accounts (multi-account support). */
29
21
  accounts?: Record<string, AlfeChannelAccountConfig>;
30
22
  }
31
23
  interface AlfeResolvedAccount {
32
24
  accountId: string;
33
25
  enabled: boolean;
34
- allowFrom: string[];
35
26
  defaultTo?: string;
36
- dmPolicy?: string;
37
27
  }
38
28
  interface AlfePluginConfig {
39
29
  /** Agent ID this plugin is associated with. */
@@ -43,6 +33,28 @@ interface AlfePluginConfig {
43
33
  /** API key for chat service auth */
44
34
  apiKey?: string;
45
35
  }
36
+ interface AlfeOutboundChatClient {
37
+ notify(event: string, payload: Record<string, unknown>): void;
38
+ }
39
+ interface AlfeOutboundSessionSummary {
40
+ sessionId: string;
41
+ userId?: string;
42
+ createdAt: string;
43
+ lastMessageAt?: string;
44
+ }
45
+ interface AlfeOutboundSession {
46
+ sessionId: string;
47
+ userId?: string;
48
+ }
49
+ interface AlfeChannelOutboundDeps {
50
+ getChatClient: () => AlfeOutboundChatClient | null;
51
+ listSessions: (filters?: {
52
+ userId?: string;
53
+ }) => Promise<AlfeOutboundSessionSummary[]>;
54
+ getSession: (sessionId: string) => Promise<AlfeOutboundSession | null>;
55
+ createSession: (sessionId: string, agentId: string, channel: string, tenantId?: string, userId?: string) => Promise<unknown>;
56
+ addMessage: (sessionId: string, role: 'user' | 'assistant', content: string, senderId?: string, senderName?: string) => Promise<void>;
57
+ }
46
58
  //#endregion
47
59
  //#region src/alfe-channel.d.ts
48
60
  /** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
@@ -53,13 +65,23 @@ interface OpenClawConfig {
53
65
  };
54
66
  [key: string]: unknown;
55
67
  }
68
+ interface OutboundDeliveryResult {
69
+ channel: 'alfe';
70
+ messageId: string;
71
+ conversationId: string;
72
+ timestamp: number;
73
+ }
56
74
  /**
57
75
  * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
58
76
  *
59
77
  * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
60
78
  * but is registered dynamically via api.registerChannel().
79
+ *
80
+ * `deps` enables outbound delivery and peer discovery. When omitted (e.g.
81
+ * during CLI metadata registration), outbound calls throw and the directory
82
+ * is empty — but the channel is still registered for inbound use.
61
83
  */
62
- declare function createAlfeChannelPlugin(): {
84
+ declare function createAlfeChannelPlugin(deps?: AlfeChannelOutboundDeps): {
63
85
  id: string;
64
86
  meta: {
65
87
  id: string;
@@ -118,15 +140,7 @@ declare function createAlfeChannelPlugin(): {
118
140
  accountId: string;
119
141
  enabled: boolean;
120
142
  configured: boolean;
121
- dmPolicy: string | undefined;
122
143
  };
123
- /**
124
- * Resolve allow-from list for an account.
125
- */
126
- resolveAllowFrom(params: {
127
- cfg: OpenClawConfig;
128
- accountId?: string | null;
129
- }): string[];
130
144
  /**
131
145
  * Resolve default outbound target.
132
146
  */
@@ -136,13 +150,47 @@ declare function createAlfeChannelPlugin(): {
136
150
  }): string | undefined;
137
151
  };
138
152
  /**
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.
153
+ * Outbound delivery via the plugin (`deliveryMode: 'direct'`).
154
+ *
155
+ * OpenClaw calls `resolveTarget` to validate the agent-supplied `to`,
156
+ * then `sendText` / `sendMedia` per chunk. We push each chunk to the
157
+ * chat relay service via `chatClient.notify('agent-message', …)`,
158
+ * which broadcasts to all connected web/mobile clients on that
159
+ * conversation and notifies offline participants.
142
160
  */
143
161
  outbound: {
144
- deliveryMode: "gateway";
162
+ deliveryMode: "direct";
145
163
  textChunkLimit: number;
164
+ resolveTarget(params: {
165
+ to?: string;
166
+ }): {
167
+ ok: true;
168
+ to: string;
169
+ } | {
170
+ ok: false;
171
+ error: Error;
172
+ };
173
+ sendText(ctx: {
174
+ to: string;
175
+ text: string;
176
+ }): Promise<OutboundDeliveryResult>;
177
+ sendMedia(ctx: {
178
+ to: string;
179
+ text: string;
180
+ mediaUrl?: string;
181
+ }): Promise<OutboundDeliveryResult>;
182
+ };
183
+ /**
184
+ * Directory adapter — exposes users the agent has chatted with so the
185
+ * `message` tool can suggest valid targets. Backed by the on-disk
186
+ * session store; live-list is the same as static-list since sessions
187
+ * are the only source of truth.
188
+ */
189
+ directory: {
190
+ listPeers(): Promise<{
191
+ kind: "user";
192
+ id: string;
193
+ }[]>;
146
194
  };
147
195
  /**
148
196
  * Setup adapter — minimal for Alfe since no external tokens are needed.
package/dist/plugin.d.ts CHANGED
@@ -9,31 +9,21 @@
9
9
  interface AlfeChannelAccountConfig {
10
10
  /** Whether this account is enabled. */
11
11
  enabled?: boolean;
12
- /** Allowed sender identifiers (user IDs, email addresses). */
13
- allowFrom?: string | string[];
14
12
  /** Default delivery target. */
15
13
  defaultTo?: string;
16
- /** DM policy (open, allowlist, etc.). */
17
- dmPolicy?: string;
18
14
  }
19
15
  interface AlfeChannelConfig {
20
16
  /** Whether the Alfe channel is enabled. */
21
17
  enabled?: boolean;
22
- /** Allowed sender identifiers. */
23
- allowFrom?: string | string[];
24
18
  /** Default delivery target for outbound messages. */
25
19
  defaultTo?: string;
26
- /** DM policy. */
27
- dmPolicy?: string;
28
20
  /** Named accounts (multi-account support). */
29
21
  accounts?: Record<string, AlfeChannelAccountConfig>;
30
22
  }
31
23
  interface AlfeResolvedAccount {
32
24
  accountId: string;
33
25
  enabled: boolean;
34
- allowFrom: string[];
35
26
  defaultTo?: string;
36
- dmPolicy?: string;
37
27
  }
38
28
  interface AlfePluginConfig {
39
29
  /** Agent ID this plugin is associated with. */
@@ -43,6 +33,28 @@ interface AlfePluginConfig {
43
33
  /** API key for chat service auth */
44
34
  apiKey?: string;
45
35
  }
36
+ interface AlfeOutboundChatClient {
37
+ notify(event: string, payload: Record<string, unknown>): void;
38
+ }
39
+ interface AlfeOutboundSessionSummary {
40
+ sessionId: string;
41
+ userId?: string;
42
+ createdAt: string;
43
+ lastMessageAt?: string;
44
+ }
45
+ interface AlfeOutboundSession {
46
+ sessionId: string;
47
+ userId?: string;
48
+ }
49
+ interface AlfeChannelOutboundDeps {
50
+ getChatClient: () => AlfeOutboundChatClient | null;
51
+ listSessions: (filters?: {
52
+ userId?: string;
53
+ }) => Promise<AlfeOutboundSessionSummary[]>;
54
+ getSession: (sessionId: string) => Promise<AlfeOutboundSession | null>;
55
+ createSession: (sessionId: string, agentId: string, channel: string, tenantId?: string, userId?: string) => Promise<unknown>;
56
+ addMessage: (sessionId: string, role: 'user' | 'assistant', content: string, senderId?: string, senderName?: string) => Promise<void>;
57
+ }
46
58
  //#endregion
47
59
  //#region src/alfe-channel.d.ts
48
60
  /** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
@@ -53,13 +65,23 @@ interface OpenClawConfig {
53
65
  };
54
66
  [key: string]: unknown;
55
67
  }
68
+ interface OutboundDeliveryResult {
69
+ channel: 'alfe';
70
+ messageId: string;
71
+ conversationId: string;
72
+ timestamp: number;
73
+ }
56
74
  /**
57
75
  * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
58
76
  *
59
77
  * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
60
78
  * but is registered dynamically via api.registerChannel().
79
+ *
80
+ * `deps` enables outbound delivery and peer discovery. When omitted (e.g.
81
+ * during CLI metadata registration), outbound calls throw and the directory
82
+ * is empty — but the channel is still registered for inbound use.
61
83
  */
62
- declare function createAlfeChannelPlugin(): {
84
+ declare function createAlfeChannelPlugin(deps?: AlfeChannelOutboundDeps): {
63
85
  id: string;
64
86
  meta: {
65
87
  id: string;
@@ -118,15 +140,7 @@ declare function createAlfeChannelPlugin(): {
118
140
  accountId: string;
119
141
  enabled: boolean;
120
142
  configured: boolean;
121
- dmPolicy: string | undefined;
122
143
  };
123
- /**
124
- * Resolve allow-from list for an account.
125
- */
126
- resolveAllowFrom(params: {
127
- cfg: OpenClawConfig;
128
- accountId?: string | null;
129
- }): string[];
130
144
  /**
131
145
  * Resolve default outbound target.
132
146
  */
@@ -136,13 +150,47 @@ declare function createAlfeChannelPlugin(): {
136
150
  }): string | undefined;
137
151
  };
138
152
  /**
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.
153
+ * Outbound delivery via the plugin (`deliveryMode: 'direct'`).
154
+ *
155
+ * OpenClaw calls `resolveTarget` to validate the agent-supplied `to`,
156
+ * then `sendText` / `sendMedia` per chunk. We push each chunk to the
157
+ * chat relay service via `chatClient.notify('agent-message', …)`,
158
+ * which broadcasts to all connected web/mobile clients on that
159
+ * conversation and notifies offline participants.
142
160
  */
143
161
  outbound: {
144
- deliveryMode: "gateway";
162
+ deliveryMode: "direct";
145
163
  textChunkLimit: number;
164
+ resolveTarget(params: {
165
+ to?: string;
166
+ }): {
167
+ ok: true;
168
+ to: string;
169
+ } | {
170
+ ok: false;
171
+ error: Error;
172
+ };
173
+ sendText(ctx: {
174
+ to: string;
175
+ text: string;
176
+ }): Promise<OutboundDeliveryResult>;
177
+ sendMedia(ctx: {
178
+ to: string;
179
+ text: string;
180
+ mediaUrl?: string;
181
+ }): Promise<OutboundDeliveryResult>;
182
+ };
183
+ /**
184
+ * Directory adapter — exposes users the agent has chatted with so the
185
+ * `message` tool can suggest valid targets. Backed by the on-disk
186
+ * session store; live-list is the same as static-list since sessions
187
+ * are the only source of truth.
188
+ */
189
+ directory: {
190
+ listPeers(): Promise<{
191
+ kind: "user";
192
+ id: string;
193
+ }[]>;
146
194
  };
147
195
  /**
148
196
  * Setup adapter — minimal for Alfe since no external tokens are needed.
package/dist/plugin2.cjs CHANGED
@@ -3,27 +3,70 @@ let node_fs_promises = require("node:fs/promises");
3
3
  let node_path = require("node:path");
4
4
  let node_os = require("node:os");
5
5
  let _alfe_ai_chat = require("@alfe.ai/chat");
6
- let _alfe_ai_config = require("@alfe.ai/config");
7
- let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
6
+ let node_crypto = require("node:crypto");
8
7
  let node_fs = require("node:fs");
9
8
  //#region src/alfe-channel.ts
9
+ /**
10
+ * Alfe channel plugin definition — registers 'alfe' as an OpenClaw channel.
11
+ *
12
+ * Web and mobile clients share the same channel and conversation sessions.
13
+ * Outbound delivery uses `deliveryMode: 'direct'` — the plugin's sendText /
14
+ * sendMedia handlers push messages through the chat relay service.
15
+ *
16
+ * Target formats accepted by `resolveTarget`:
17
+ * user:{clerkUserId} — most recent conversation for that user (created on demand)
18
+ * conv:{conversationId} — a specific existing conversation
19
+ *
20
+ * Config section: channels.alfe in openclaw.yaml
21
+ */
10
22
  const CHANNEL_ID = "alfe";
11
23
  const DEFAULT_ACCOUNT_ID = "default";
24
+ async function sendViaChat(deps, ctx, mediaUrl) {
25
+ const client = deps.getChatClient();
26
+ if (!client) throw new Error("Chat service not connected — cannot deliver");
27
+ let conversationId;
28
+ if (ctx.to.startsWith("conv:")) {
29
+ conversationId = ctx.to.slice(5);
30
+ if (!await deps.getSession(conversationId)) throw new Error(`Conversation not found: ${conversationId}`);
31
+ } else {
32
+ const userId = ctx.to.startsWith("user:") ? ctx.to.slice(5) : ctx.to;
33
+ if (!userId) throw new Error("Empty userId in target");
34
+ const sessions = await deps.listSessions({ userId });
35
+ if (sessions.length > 0) conversationId = sessions[0].sessionId;
36
+ else {
37
+ conversationId = `alfe:chat:${userId}:${(0, node_crypto.randomUUID)()}`;
38
+ await deps.createSession(conversationId, "", "alfe", void 0, userId);
39
+ }
40
+ }
41
+ await deps.addMessage(conversationId, "assistant", ctx.text);
42
+ const messageId = (0, node_crypto.randomUUID)();
43
+ client.notify("agent-message", {
44
+ conversationId,
45
+ text: ctx.text,
46
+ sessionKey: conversationId,
47
+ ...mediaUrl ? { mediaUrls: [mediaUrl] } : {}
48
+ });
49
+ return {
50
+ channel: "alfe",
51
+ messageId,
52
+ conversationId,
53
+ timestamp: Date.now()
54
+ };
55
+ }
12
56
  function getChannelSection(cfg) {
13
57
  return cfg.channels?.alfe ?? {};
14
58
  }
15
- function normalizeAllowFrom(raw) {
16
- if (!raw) return [];
17
- if (typeof raw === "string") return [raw];
18
- return raw;
19
- }
20
59
  /**
21
60
  * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
22
61
  *
23
62
  * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
24
63
  * but is registered dynamically via api.registerChannel().
64
+ *
65
+ * `deps` enables outbound delivery and peer discovery. When omitted (e.g.
66
+ * during CLI metadata registration), outbound calls throw and the directory
67
+ * is empty — but the channel is still registered for inbound use.
25
68
  */
26
- function createAlfeChannelPlugin() {
69
+ function createAlfeChannelPlugin(deps) {
27
70
  return {
28
71
  id: CHANNEL_ID,
29
72
  meta: {
@@ -57,7 +100,7 @@ function createAlfeChannelPlugin() {
57
100
  listAccountIds(cfg) {
58
101
  const section = getChannelSection(cfg);
59
102
  const ids = [];
60
- if (section.enabled !== false && (section.allowFrom ?? section.defaultTo ?? section.dmPolicy)) ids.push(DEFAULT_ACCOUNT_ID);
103
+ if (section.enabled !== false && section.defaultTo) ids.push(DEFAULT_ACCOUNT_ID);
61
104
  if (section.accounts) {
62
105
  for (const id of Object.keys(section.accounts)) if (!ids.includes(id)) ids.push(id);
63
106
  }
@@ -71,16 +114,12 @@ function createAlfeChannelPlugin() {
71
114
  if (accountSection) return {
72
115
  accountId: id,
73
116
  enabled: accountSection.enabled !== false,
74
- allowFrom: normalizeAllowFrom(accountSection.allowFrom),
75
- defaultTo: accountSection.defaultTo,
76
- dmPolicy: accountSection.dmPolicy
117
+ defaultTo: accountSection.defaultTo
77
118
  };
78
119
  return {
79
120
  accountId: id,
80
121
  enabled: section.enabled !== false,
81
- allowFrom: normalizeAllowFrom(section.allowFrom),
82
- defaultTo: section.defaultTo,
83
- dmPolicy: section.dmPolicy
122
+ defaultTo: section.defaultTo
84
123
  };
85
124
  },
86
125
  defaultAccountId() {
@@ -96,15 +135,9 @@ function createAlfeChannelPlugin() {
96
135
  return {
97
136
  accountId: account.accountId,
98
137
  enabled: account.enabled,
99
- configured: true,
100
- dmPolicy: account.dmPolicy
138
+ configured: true
101
139
  };
102
140
  },
103
- resolveAllowFrom(params) {
104
- const section = getChannelSection(params.cfg);
105
- const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
106
- return normalizeAllowFrom((section.accounts?.[id])?.allowFrom ?? section.allowFrom);
107
- },
108
141
  resolveDefaultTo(params) {
109
142
  const section = getChannelSection(params.cfg);
110
143
  const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
@@ -112,9 +145,58 @@ function createAlfeChannelPlugin() {
112
145
  }
113
146
  },
114
147
  outbound: {
115
- deliveryMode: "gateway",
116
- textChunkLimit: 4e3
148
+ deliveryMode: "direct",
149
+ textChunkLimit: 4e3,
150
+ resolveTarget(params) {
151
+ const to = params.to;
152
+ if (!to) return {
153
+ ok: false,
154
+ error: /* @__PURE__ */ new Error("Missing target — use user:{userId} or conv:{conversationId}")
155
+ };
156
+ if (to.startsWith("conv:")) {
157
+ if (!to.slice(5)) return {
158
+ ok: false,
159
+ error: /* @__PURE__ */ new Error("Empty conversation ID")
160
+ };
161
+ return {
162
+ ok: true,
163
+ to
164
+ };
165
+ }
166
+ const userId = to.startsWith("user:") ? to.slice(5) : to;
167
+ if (!userId || userId === "anon") return {
168
+ ok: false,
169
+ error: /* @__PURE__ */ new Error("Invalid target: userId is required")
170
+ };
171
+ return {
172
+ ok: true,
173
+ to: `user:${userId}`
174
+ };
175
+ },
176
+ async sendText(ctx) {
177
+ if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
178
+ return await sendViaChat(deps, ctx, void 0);
179
+ },
180
+ async sendMedia(ctx) {
181
+ if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
182
+ return await sendViaChat(deps, ctx, ctx.mediaUrl);
183
+ }
117
184
  },
185
+ directory: { async listPeers() {
186
+ if (!deps) return [];
187
+ const sessions = await deps.listSessions();
188
+ const seen = /* @__PURE__ */ new Set();
189
+ const peers = [];
190
+ for (const s of sessions) {
191
+ if (!s.userId || seen.has(s.userId)) continue;
192
+ seen.add(s.userId);
193
+ peers.push({
194
+ kind: "user",
195
+ id: s.userId
196
+ });
197
+ }
198
+ return peers;
199
+ } },
118
200
  setup: {
119
201
  resolveAccountId(params) {
120
202
  return params.accountId ?? DEFAULT_ACCOUNT_ID;
@@ -283,6 +365,8 @@ async function listSessions(filters, limit = 50) {
283
365
  sessionId: session.sessionId,
284
366
  agentId: session.agentId,
285
367
  channel: session.channel,
368
+ ...session.tenantId ? { tenantId: session.tenantId } : {},
369
+ ...session.userId ? { userId: session.userId } : {},
286
370
  createdAt: session.createdAt,
287
371
  lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
288
372
  preview: lastMsg?.content.slice(0, 100),
@@ -552,7 +636,6 @@ function resolveOpenClawSdk(log) {
552
636
  let pluginRuntime = null;
553
637
  let chatClient = null;
554
638
  let connectingPromise = null;
555
- let metricsClient = null;
556
639
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
557
640
  const DOWNLOAD_TIMEOUT_MS = 3e4;
558
641
  const MAX_REDIRECTS = 5;
@@ -644,13 +727,6 @@ async function handleAgentRequest(request, log) {
644
727
  const sessionId = conversationId ?? legacySessionKey;
645
728
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
646
729
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
647
- if (metricsClient && userId) metricsClient.recordActivity({
648
- userId,
649
- channel: "alfe",
650
- role: "user"
651
- }).catch((err) => {
652
- log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
653
- });
654
730
  let resolvedOpenClawKey = null;
655
731
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
656
732
  if (!evt.sessionKey) return;
@@ -732,13 +808,6 @@ async function handleAgentRequest(request, log) {
732
808
  sessionKey: resolvedOpenClawKey ?? legacySessionKey,
733
809
  ...mediaUrls.length ? { mediaUrls } : {}
734
810
  });
735
- if (metricsClient && userId) metricsClient.recordActivity({
736
- userId,
737
- channel: "alfe",
738
- role: "assistant"
739
- }).catch((err) => {
740
- log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
741
- });
742
811
  },
743
812
  onRecordError: (err) => {
744
813
  log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
@@ -822,7 +891,13 @@ const plugin = {
822
891
  activate(api) {
823
892
  const log = api.logger;
824
893
  const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
825
- const alfeChannel = createAlfeChannelPlugin();
894
+ const alfeChannel = createAlfeChannelPlugin({
895
+ getChatClient: () => chatClient,
896
+ listSessions,
897
+ getSession,
898
+ createSession,
899
+ addMessage
900
+ });
826
901
  api.registerChannel(alfeChannel);
827
902
  log.info(`Registered channel: ${alfeChannel.id}`);
828
903
  const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
@@ -835,15 +910,6 @@ const plugin = {
835
910
  log.info("Chat plugin registering...");
836
911
  resolveOpenClawSdk(log);
837
912
  pluginRuntime = api.runtime ?? null;
838
- try {
839
- const cfg = (0, _alfe_ai_config.resolveConfig)();
840
- metricsClient = new _alfe_ai_agent_api_client.AgentApiClient({
841
- apiKey: cfg.apiKey,
842
- apiUrl: cfg.apiUrl
843
- });
844
- } catch {
845
- log.debug("Metrics client not initialized — activity tracking disabled");
846
- }
847
913
  connectingPromise = Promise.resolve().then(() => {
848
914
  try {
849
915
  const { apiKey, chatWsUrl } = (0, _alfe_ai_chat.resolveAlfeChat)({
@@ -923,7 +989,6 @@ const plugin = {
923
989
  }
924
990
  pluginRuntime = null;
925
991
  dispatchInbound = null;
926
- metricsClient = null;
927
992
  log.info("Chat plugin deactivated");
928
993
  };
929
994
  if (typeof api.registerGatewayMethod === "function") {
@@ -990,7 +1055,6 @@ const plugin = {
990
1055
  }
991
1056
  pluginRuntime = null;
992
1057
  dispatchInbound = null;
993
- metricsClient = null;
994
1058
  log.info("Chat plugin deactivated");
995
1059
  }
996
1060
  };
package/dist/plugin2.js CHANGED
@@ -3,27 +3,70 @@ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promi
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
6
- import { resolveConfig } from "@alfe.ai/config";
7
- import { AgentApiClient } from "@alfe.ai/agent-api-client";
6
+ import { randomUUID } from "node:crypto";
8
7
  import { existsSync } from "node:fs";
9
8
  //#region src/alfe-channel.ts
9
+ /**
10
+ * Alfe channel plugin definition — registers 'alfe' as an OpenClaw channel.
11
+ *
12
+ * Web and mobile clients share the same channel and conversation sessions.
13
+ * Outbound delivery uses `deliveryMode: 'direct'` — the plugin's sendText /
14
+ * sendMedia handlers push messages through the chat relay service.
15
+ *
16
+ * Target formats accepted by `resolveTarget`:
17
+ * user:{clerkUserId} — most recent conversation for that user (created on demand)
18
+ * conv:{conversationId} — a specific existing conversation
19
+ *
20
+ * Config section: channels.alfe in openclaw.yaml
21
+ */
10
22
  const CHANNEL_ID = "alfe";
11
23
  const DEFAULT_ACCOUNT_ID = "default";
24
+ async function sendViaChat(deps, ctx, mediaUrl) {
25
+ const client = deps.getChatClient();
26
+ if (!client) throw new Error("Chat service not connected — cannot deliver");
27
+ let conversationId;
28
+ if (ctx.to.startsWith("conv:")) {
29
+ conversationId = ctx.to.slice(5);
30
+ if (!await deps.getSession(conversationId)) throw new Error(`Conversation not found: ${conversationId}`);
31
+ } else {
32
+ const userId = ctx.to.startsWith("user:") ? ctx.to.slice(5) : ctx.to;
33
+ if (!userId) throw new Error("Empty userId in target");
34
+ const sessions = await deps.listSessions({ userId });
35
+ if (sessions.length > 0) conversationId = sessions[0].sessionId;
36
+ else {
37
+ conversationId = `alfe:chat:${userId}:${randomUUID()}`;
38
+ await deps.createSession(conversationId, "", "alfe", void 0, userId);
39
+ }
40
+ }
41
+ await deps.addMessage(conversationId, "assistant", ctx.text);
42
+ const messageId = randomUUID();
43
+ client.notify("agent-message", {
44
+ conversationId,
45
+ text: ctx.text,
46
+ sessionKey: conversationId,
47
+ ...mediaUrl ? { mediaUrls: [mediaUrl] } : {}
48
+ });
49
+ return {
50
+ channel: "alfe",
51
+ messageId,
52
+ conversationId,
53
+ timestamp: Date.now()
54
+ };
55
+ }
12
56
  function getChannelSection(cfg) {
13
57
  return cfg.channels?.alfe ?? {};
14
58
  }
15
- function normalizeAllowFrom(raw) {
16
- if (!raw) return [];
17
- if (typeof raw === "string") return [raw];
18
- return raw;
19
- }
20
59
  /**
21
60
  * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
22
61
  *
23
62
  * This follows the same pattern as built-in channels (Telegram, Discord, etc.)
24
63
  * but is registered dynamically via api.registerChannel().
64
+ *
65
+ * `deps` enables outbound delivery and peer discovery. When omitted (e.g.
66
+ * during CLI metadata registration), outbound calls throw and the directory
67
+ * is empty — but the channel is still registered for inbound use.
25
68
  */
26
- function createAlfeChannelPlugin() {
69
+ function createAlfeChannelPlugin(deps) {
27
70
  return {
28
71
  id: CHANNEL_ID,
29
72
  meta: {
@@ -57,7 +100,7 @@ function createAlfeChannelPlugin() {
57
100
  listAccountIds(cfg) {
58
101
  const section = getChannelSection(cfg);
59
102
  const ids = [];
60
- if (section.enabled !== false && (section.allowFrom ?? section.defaultTo ?? section.dmPolicy)) ids.push(DEFAULT_ACCOUNT_ID);
103
+ if (section.enabled !== false && section.defaultTo) ids.push(DEFAULT_ACCOUNT_ID);
61
104
  if (section.accounts) {
62
105
  for (const id of Object.keys(section.accounts)) if (!ids.includes(id)) ids.push(id);
63
106
  }
@@ -71,16 +114,12 @@ function createAlfeChannelPlugin() {
71
114
  if (accountSection) return {
72
115
  accountId: id,
73
116
  enabled: accountSection.enabled !== false,
74
- allowFrom: normalizeAllowFrom(accountSection.allowFrom),
75
- defaultTo: accountSection.defaultTo,
76
- dmPolicy: accountSection.dmPolicy
117
+ defaultTo: accountSection.defaultTo
77
118
  };
78
119
  return {
79
120
  accountId: id,
80
121
  enabled: section.enabled !== false,
81
- allowFrom: normalizeAllowFrom(section.allowFrom),
82
- defaultTo: section.defaultTo,
83
- dmPolicy: section.dmPolicy
122
+ defaultTo: section.defaultTo
84
123
  };
85
124
  },
86
125
  defaultAccountId() {
@@ -96,15 +135,9 @@ function createAlfeChannelPlugin() {
96
135
  return {
97
136
  accountId: account.accountId,
98
137
  enabled: account.enabled,
99
- configured: true,
100
- dmPolicy: account.dmPolicy
138
+ configured: true
101
139
  };
102
140
  },
103
- resolveAllowFrom(params) {
104
- const section = getChannelSection(params.cfg);
105
- const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
106
- return normalizeAllowFrom((section.accounts?.[id])?.allowFrom ?? section.allowFrom);
107
- },
108
141
  resolveDefaultTo(params) {
109
142
  const section = getChannelSection(params.cfg);
110
143
  const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
@@ -112,9 +145,58 @@ function createAlfeChannelPlugin() {
112
145
  }
113
146
  },
114
147
  outbound: {
115
- deliveryMode: "gateway",
116
- textChunkLimit: 4e3
148
+ deliveryMode: "direct",
149
+ textChunkLimit: 4e3,
150
+ resolveTarget(params) {
151
+ const to = params.to;
152
+ if (!to) return {
153
+ ok: false,
154
+ error: /* @__PURE__ */ new Error("Missing target — use user:{userId} or conv:{conversationId}")
155
+ };
156
+ if (to.startsWith("conv:")) {
157
+ if (!to.slice(5)) return {
158
+ ok: false,
159
+ error: /* @__PURE__ */ new Error("Empty conversation ID")
160
+ };
161
+ return {
162
+ ok: true,
163
+ to
164
+ };
165
+ }
166
+ const userId = to.startsWith("user:") ? to.slice(5) : to;
167
+ if (!userId || userId === "anon") return {
168
+ ok: false,
169
+ error: /* @__PURE__ */ new Error("Invalid target: userId is required")
170
+ };
171
+ return {
172
+ ok: true,
173
+ to: `user:${userId}`
174
+ };
175
+ },
176
+ async sendText(ctx) {
177
+ if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
178
+ return await sendViaChat(deps, ctx, void 0);
179
+ },
180
+ async sendMedia(ctx) {
181
+ if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
182
+ return await sendViaChat(deps, ctx, ctx.mediaUrl);
183
+ }
117
184
  },
185
+ directory: { async listPeers() {
186
+ if (!deps) return [];
187
+ const sessions = await deps.listSessions();
188
+ const seen = /* @__PURE__ */ new Set();
189
+ const peers = [];
190
+ for (const s of sessions) {
191
+ if (!s.userId || seen.has(s.userId)) continue;
192
+ seen.add(s.userId);
193
+ peers.push({
194
+ kind: "user",
195
+ id: s.userId
196
+ });
197
+ }
198
+ return peers;
199
+ } },
118
200
  setup: {
119
201
  resolveAccountId(params) {
120
202
  return params.accountId ?? DEFAULT_ACCOUNT_ID;
@@ -283,6 +365,8 @@ async function listSessions(filters, limit = 50) {
283
365
  sessionId: session.sessionId,
284
366
  agentId: session.agentId,
285
367
  channel: session.channel,
368
+ ...session.tenantId ? { tenantId: session.tenantId } : {},
369
+ ...session.userId ? { userId: session.userId } : {},
286
370
  createdAt: session.createdAt,
287
371
  lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
288
372
  preview: lastMsg?.content.slice(0, 100),
@@ -552,7 +636,6 @@ function resolveOpenClawSdk(log) {
552
636
  let pluginRuntime = null;
553
637
  let chatClient = null;
554
638
  let connectingPromise = null;
555
- let metricsClient = null;
556
639
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
557
640
  const DOWNLOAD_TIMEOUT_MS = 3e4;
558
641
  const MAX_REDIRECTS = 5;
@@ -644,13 +727,6 @@ async function handleAgentRequest(request, log) {
644
727
  const sessionId = conversationId ?? legacySessionKey;
645
728
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
646
729
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
647
- if (metricsClient && userId) metricsClient.recordActivity({
648
- userId,
649
- channel: "alfe",
650
- role: "user"
651
- }).catch((err) => {
652
- log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
653
- });
654
730
  let resolvedOpenClawKey = null;
655
731
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
656
732
  if (!evt.sessionKey) return;
@@ -732,13 +808,6 @@ async function handleAgentRequest(request, log) {
732
808
  sessionKey: resolvedOpenClawKey ?? legacySessionKey,
733
809
  ...mediaUrls.length ? { mediaUrls } : {}
734
810
  });
735
- if (metricsClient && userId) metricsClient.recordActivity({
736
- userId,
737
- channel: "alfe",
738
- role: "assistant"
739
- }).catch((err) => {
740
- log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
741
- });
742
811
  },
743
812
  onRecordError: (err) => {
744
813
  log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
@@ -822,7 +891,13 @@ const plugin = {
822
891
  activate(api) {
823
892
  const log = api.logger;
824
893
  const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
825
- const alfeChannel = createAlfeChannelPlugin();
894
+ const alfeChannel = createAlfeChannelPlugin({
895
+ getChatClient: () => chatClient,
896
+ listSessions,
897
+ getSession,
898
+ createSession,
899
+ addMessage
900
+ });
826
901
  api.registerChannel(alfeChannel);
827
902
  log.info(`Registered channel: ${alfeChannel.id}`);
828
903
  const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
@@ -835,15 +910,6 @@ const plugin = {
835
910
  log.info("Chat plugin registering...");
836
911
  resolveOpenClawSdk(log);
837
912
  pluginRuntime = api.runtime ?? null;
838
- try {
839
- const cfg = resolveConfig();
840
- metricsClient = new AgentApiClient({
841
- apiKey: cfg.apiKey,
842
- apiUrl: cfg.apiUrl
843
- });
844
- } catch {
845
- log.debug("Metrics client not initialized — activity tracking disabled");
846
- }
847
913
  connectingPromise = Promise.resolve().then(() => {
848
914
  try {
849
915
  const { apiKey, chatWsUrl } = resolveAlfeChat({
@@ -923,7 +989,6 @@ const plugin = {
923
989
  }
924
990
  pluginRuntime = null;
925
991
  dispatchInbound = null;
926
- metricsClient = null;
927
992
  log.info("Chat plugin deactivated");
928
993
  };
929
994
  if (typeof api.registerGatewayMethod === "function") {
@@ -990,7 +1055,6 @@ const plugin = {
990
1055
  }
991
1056
  pluginRuntime = null;
992
1057
  dispatchInbound = null;
993
- metricsClient = null;
994
1058
  log.info("Chat plugin deactivated");
995
1059
  }
996
1060
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.0.33",
3
+ "version": "0.1.0",
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,7 +27,7 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/agent-api-client": "^0.0.13",
30
+ "@alfe.ai/agent-api-client": "^0.1.1",
31
31
  "@alfe.ai/chat": "^0.0.8",
32
32
  "@alfe.ai/config": "^0.0.8"
33
33
  },