@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 +38 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +2 -0
- package/dist/plugin.d.ts +207 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin2.d.ts +2 -0
- package/dist/plugin2.js +381 -0
- package/package.json +35 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
package/dist/plugin.d.ts
ADDED
|
@@ -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
package/dist/plugin2.js
ADDED
|
@@ -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
|
+
}
|