@phronesis-io/openclaw-eigenflux 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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/agent-prompt-templates.d.ts +16 -0
  4. package/dist/agent-prompt-templates.d.ts.map +1 -0
  5. package/dist/agent-prompt-templates.js +67 -0
  6. package/dist/agent-prompt-templates.js.map +1 -0
  7. package/dist/config.d.ts +161 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +324 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/credentials-loader.d.ts +28 -0
  12. package/dist/credentials-loader.d.ts.map +1 -0
  13. package/dist/credentials-loader.js +121 -0
  14. package/dist/credentials-loader.js.map +1 -0
  15. package/dist/gateway-rpc-client.d.ts +26 -0
  16. package/dist/gateway-rpc-client.d.ts.map +1 -0
  17. package/dist/gateway-rpc-client.js +288 -0
  18. package/dist/gateway-rpc-client.js.map +1 -0
  19. package/dist/index.d.ts +94 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +544 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/logger.d.ts +12 -0
  24. package/dist/logger.d.ts.map +1 -0
  25. package/dist/logger.js +25 -0
  26. package/dist/logger.js.map +1 -0
  27. package/dist/notification-route-resolver.d.ts +21 -0
  28. package/dist/notification-route-resolver.d.ts.map +1 -0
  29. package/dist/notification-route-resolver.js +318 -0
  30. package/dist/notification-route-resolver.js.map +1 -0
  31. package/dist/notifier.d.ts +63 -0
  32. package/dist/notifier.d.ts.map +1 -0
  33. package/dist/notifier.js +366 -0
  34. package/dist/notifier.js.map +1 -0
  35. package/dist/pm-polling-client.d.ts +51 -0
  36. package/dist/pm-polling-client.d.ts.map +1 -0
  37. package/dist/pm-polling-client.js +175 -0
  38. package/dist/pm-polling-client.js.map +1 -0
  39. package/dist/polling-client.d.ts +73 -0
  40. package/dist/polling-client.d.ts.map +1 -0
  41. package/dist/polling-client.js +176 -0
  42. package/dist/polling-client.js.map +1 -0
  43. package/dist/session-route-memory.d.ts +13 -0
  44. package/dist/session-route-memory.d.ts.map +1 -0
  45. package/dist/session-route-memory.js +114 -0
  46. package/dist/session-route-memory.js.map +1 -0
  47. package/index.ts +1 -0
  48. package/openclaw.plugin.json +90 -0
  49. package/package.json +50 -0
  50. package/src/agent-prompt-templates.ts +91 -0
  51. package/src/config.test.ts +188 -0
  52. package/src/config.ts +410 -0
  53. package/src/credentials-loader.test.ts +78 -0
  54. package/src/credentials-loader.ts +121 -0
  55. package/src/gateway-rpc-client.test.ts +190 -0
  56. package/src/gateway-rpc-client.ts +373 -0
  57. package/src/index.integration.test.ts +437 -0
  58. package/src/index.test.ts +454 -0
  59. package/src/index.ts +758 -0
  60. package/src/logger.ts +27 -0
  61. package/src/notification-route-resolver.test.ts +136 -0
  62. package/src/notification-route-resolver.ts +430 -0
  63. package/src/notifier.test.ts +374 -0
  64. package/src/notifier.ts +558 -0
  65. package/src/openclaw-plugin-sdk.d.ts +121 -0
  66. package/src/pm-polling-client.test.ts +390 -0
  67. package/src/pm-polling-client.ts +257 -0
  68. package/src/polling-client.test.ts +279 -0
  69. package/src/polling-client.ts +283 -0
  70. package/src/session-route-memory.ts +106 -0
@@ -0,0 +1,437 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import http from 'http';
5
+ import { WebSocketServer } from 'ws';
6
+
7
+ function waitFor(condition: () => boolean, timeoutMs = 8000): Promise<void> {
8
+ const startedAt = Date.now();
9
+ return new Promise((resolve, reject) => {
10
+ const timer = setInterval(() => {
11
+ if (condition()) {
12
+ clearInterval(timer);
13
+ resolve();
14
+ return;
15
+ }
16
+ if (Date.now() - startedAt > timeoutMs) {
17
+ clearInterval(timer);
18
+ reject(new Error('condition wait timeout'));
19
+ }
20
+ }, 50);
21
+ });
22
+ }
23
+
24
+ describe('register integration', () => {
25
+ let homeDir: string;
26
+ let originalHome: string | undefined;
27
+ let workdir: string;
28
+
29
+ let apiHttpServer: http.Server;
30
+ let apiPort: number;
31
+ let apiRequestCount: number;
32
+ let apiAuthHeader: string | undefined;
33
+ let apiUserAgentHeader: string | undefined;
34
+ let apiFeedItems: Array<{
35
+ item_id: string;
36
+ group_id?: string;
37
+ broadcast_type: string;
38
+ updated_at: number;
39
+ }>;
40
+
41
+ let gatewayHttpServer: http.Server;
42
+ let gatewayWss: WebSocketServer;
43
+ let gatewayPort: number;
44
+
45
+ beforeEach(async () => {
46
+ originalHome = process.env.HOME;
47
+ homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-openclaw-home-'));
48
+ process.env.HOME = homeDir;
49
+ workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-openclaw-workdir-'));
50
+ fs.writeFileSync(
51
+ path.join(workdir, 'credentials.json'),
52
+ JSON.stringify({ access_token: 'at_integration_token' }),
53
+ 'utf-8'
54
+ );
55
+
56
+ apiRequestCount = 0;
57
+ apiAuthHeader = undefined;
58
+ apiUserAgentHeader = undefined;
59
+ apiFeedItems = [
60
+ {
61
+ item_id: '501',
62
+ group_id: 'group-int-1',
63
+ broadcast_type: 'info',
64
+ updated_at: 1760000000000,
65
+ },
66
+ ];
67
+ apiHttpServer = http.createServer((req, res) => {
68
+ if (req.url?.startsWith('/api/v1/items/feed')) {
69
+ apiRequestCount++;
70
+ apiAuthHeader = req.headers.authorization;
71
+ apiUserAgentHeader = req.headers['user-agent'];
72
+ res.writeHead(200, { 'Content-Type': 'application/json' });
73
+ res.end(
74
+ JSON.stringify({
75
+ code: 0,
76
+ msg: 'success',
77
+ data: {
78
+ items: apiFeedItems,
79
+ has_more: false,
80
+ notifications: [],
81
+ },
82
+ })
83
+ );
84
+ return;
85
+ }
86
+
87
+ res.writeHead(404);
88
+ res.end();
89
+ });
90
+ await new Promise<void>((resolve) => {
91
+ apiHttpServer.listen(0, '127.0.0.1', () => {
92
+ apiPort = (apiHttpServer.address() as any).port;
93
+ resolve();
94
+ });
95
+ });
96
+
97
+ gatewayHttpServer = http.createServer();
98
+ gatewayWss = new WebSocketServer({ server: gatewayHttpServer });
99
+ await new Promise<void>((resolve) => {
100
+ gatewayHttpServer.listen(0, '127.0.0.1', () => {
101
+ gatewayPort = (gatewayHttpServer.address() as any).port;
102
+ resolve();
103
+ });
104
+ });
105
+ });
106
+
107
+ afterEach(async () => {
108
+ if (originalHome === undefined) {
109
+ delete process.env.HOME;
110
+ } else {
111
+ process.env.HOME = originalHome;
112
+ }
113
+ fs.rmSync(homeDir, { recursive: true, force: true });
114
+ fs.rmSync(workdir, { recursive: true, force: true });
115
+ await new Promise<void>((resolve) => apiHttpServer.close(() => resolve()));
116
+ await new Promise<void>((resolve) => gatewayWss.close(() => resolve()));
117
+ await new Promise<void>((resolve) => gatewayHttpServer.close(() => resolve()));
118
+ });
119
+
120
+ test('falls back to gateway rpc agent when polling feed returns new items', async () => {
121
+ jest.resetModules();
122
+ const sessionStorePath = path.join(
123
+ homeDir,
124
+ '.openclaw',
125
+ 'agents',
126
+ 'main',
127
+ 'sessions',
128
+ 'sessions.json'
129
+ );
130
+ const { default: plugin } = await import('./index');
131
+ const services: any[] = [];
132
+ const gatewayMethods: string[] = [];
133
+ const agentParams: any[] = [];
134
+
135
+ gatewayWss.on('connection', (socket) => {
136
+ socket.send(
137
+ JSON.stringify({
138
+ type: 'event',
139
+ event: 'connect.challenge',
140
+ payload: { nonce: 'nonce-integration' },
141
+ })
142
+ );
143
+
144
+ socket.on('message', (raw) => {
145
+ const frame = JSON.parse(raw.toString());
146
+ gatewayMethods.push(String(frame.method || ''));
147
+
148
+ if (frame.type !== 'req') {
149
+ return;
150
+ }
151
+
152
+ if (frame.method === 'connect') {
153
+ socket.send(
154
+ JSON.stringify({
155
+ type: 'res',
156
+ id: frame.id,
157
+ ok: true,
158
+ payload: {
159
+ type: 'hello-ok',
160
+ protocol: 3,
161
+ server: { version: 'test', connId: 'conn-test' },
162
+ features: { methods: ['agent'], events: [] },
163
+ snapshot: { ts: Date.now() },
164
+ policy: { maxPayload: 1000000, maxBufferedBytes: 1000000, tickIntervalMs: 30000 },
165
+ },
166
+ })
167
+ );
168
+ return;
169
+ }
170
+
171
+ if (frame.method === 'agent') {
172
+ agentParams.push(frame.params);
173
+ socket.send(
174
+ JSON.stringify({
175
+ type: 'res',
176
+ id: frame.id,
177
+ ok: true,
178
+ payload: {
179
+ runId: 'run-integration-1',
180
+ status: 'started',
181
+ },
182
+ })
183
+ );
184
+ }
185
+ });
186
+ });
187
+
188
+ plugin.register({
189
+ config: {
190
+ gateway: {
191
+ auth: {
192
+ token: 'gw_test_token',
193
+ },
194
+ },
195
+ },
196
+ pluginConfig: {
197
+ gatewayUrl: `ws://127.0.0.1:${gatewayPort}`,
198
+ servers: [
199
+ {
200
+ name: 'eigenflux',
201
+ endpoint: `http://127.0.0.1:${apiPort}`,
202
+ workdir,
203
+ pollInterval: 60,
204
+ sessionStorePath,
205
+ },
206
+ ],
207
+ },
208
+ runtime: {},
209
+ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
210
+ registerService: (service: any) => {
211
+ services.push(service);
212
+ },
213
+ } as any);
214
+
215
+ expect(services).toHaveLength(1);
216
+ await services[0].start();
217
+ await waitFor(() => agentParams.length === 1);
218
+
219
+ expect(apiRequestCount).toBeGreaterThanOrEqual(1);
220
+ expect(apiAuthHeader).toBe('Bearer at_integration_token');
221
+ expect(apiUserAgentHeader).toContain('eigenflux-plugin');
222
+ expect(apiUserAgentHeader).toContain('node/');
223
+ expect(gatewayMethods).toEqual(['connect', 'agent']);
224
+ expect(agentParams[0]).toEqual(
225
+ expect.objectContaining({
226
+ agentId: 'main',
227
+ sessionKey: 'main',
228
+ message: expect.stringContaining('[EIGENFLUX_FEED_PAYLOAD]'),
229
+ deliver: true,
230
+ })
231
+ );
232
+ expect(String(agentParams[0].message)).toContain('"item_id": "501"');
233
+ expect(String(agentParams[0].message)).toContain('"group_id": "group-int-1"');
234
+ expect(String(agentParams[0].message)).toContain('network=eigenflux');
235
+ expect(String(agentParams[0].message)).toContain(`workdir=${workdir}`);
236
+ expect(String(agentParams[0].message)).toContain(
237
+ `skill_file=http://127.0.0.1:${apiPort}/skill.md`
238
+ );
239
+ expect(String(agentParams[0].message)).toContain(
240
+ 'submit the corresponding feedback scores through the normal EigenFlux workflow'
241
+ );
242
+ expect(typeof agentParams[0].idempotencyKey).toBe('string');
243
+ expect(agentParams[0].idempotencyKey.length).toBeGreaterThan(0);
244
+
245
+ await services[0].stop();
246
+ });
247
+
248
+ test('dispatches the entire feed payload in a single gateway agent message', async () => {
249
+ apiFeedItems = [
250
+ {
251
+ item_id: '601',
252
+ group_id: 'group-dup-1',
253
+ broadcast_type: 'info',
254
+ updated_at: 1760000000100,
255
+ },
256
+ {
257
+ item_id: '602',
258
+ group_id: 'group-dup-1',
259
+ broadcast_type: 'info',
260
+ updated_at: 1760000000200,
261
+ },
262
+ ];
263
+
264
+ jest.resetModules();
265
+ const sessionStorePath = path.join(
266
+ homeDir,
267
+ '.openclaw',
268
+ 'agents',
269
+ 'main',
270
+ 'sessions',
271
+ 'sessions.json'
272
+ );
273
+ const { default: plugin } = await import('./index');
274
+ const services: any[] = [];
275
+ const agentParams: any[] = [];
276
+
277
+ gatewayWss.on('connection', (socket) => {
278
+ socket.send(
279
+ JSON.stringify({
280
+ type: 'event',
281
+ event: 'connect.challenge',
282
+ payload: { nonce: 'nonce-duplicate-group' },
283
+ })
284
+ );
285
+
286
+ socket.on('message', (raw) => {
287
+ const frame = JSON.parse(raw.toString());
288
+ if (frame.type !== 'req') {
289
+ return;
290
+ }
291
+
292
+ if (frame.method === 'connect') {
293
+ socket.send(
294
+ JSON.stringify({
295
+ type: 'res',
296
+ id: frame.id,
297
+ ok: true,
298
+ payload: {
299
+ type: 'hello-ok',
300
+ protocol: 3,
301
+ server: { version: 'test', connId: 'conn-dup' },
302
+ features: { methods: ['agent'], events: [] },
303
+ snapshot: { ts: Date.now() },
304
+ policy: { maxPayload: 1000000, maxBufferedBytes: 1000000, tickIntervalMs: 30000 },
305
+ },
306
+ })
307
+ );
308
+ return;
309
+ }
310
+
311
+ if (frame.method === 'agent') {
312
+ agentParams.push(frame.params);
313
+ socket.send(
314
+ JSON.stringify({
315
+ type: 'res',
316
+ id: frame.id,
317
+ ok: true,
318
+ payload: {
319
+ runId: `run-dup-${agentParams.length}`,
320
+ status: 'started',
321
+ },
322
+ })
323
+ );
324
+ }
325
+ });
326
+ });
327
+
328
+ plugin.register({
329
+ config: {
330
+ gateway: {
331
+ auth: {
332
+ token: 'gw_test_token',
333
+ },
334
+ },
335
+ },
336
+ pluginConfig: {
337
+ gatewayUrl: `ws://127.0.0.1:${gatewayPort}`,
338
+ servers: [
339
+ {
340
+ name: 'eigenflux',
341
+ endpoint: `http://127.0.0.1:${apiPort}`,
342
+ workdir,
343
+ pollInterval: 60,
344
+ sessionStorePath,
345
+ },
346
+ ],
347
+ },
348
+ runtime: {},
349
+ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
350
+ registerService: (service: any) => {
351
+ services.push(service);
352
+ },
353
+ } as any);
354
+
355
+ expect(services).toHaveLength(1);
356
+ await services[0].start();
357
+ await waitFor(() => agentParams.length === 1);
358
+
359
+ expect(agentParams).toHaveLength(1);
360
+ expect(String(agentParams[0].message)).toContain('"item_id": "601"');
361
+ expect(String(agentParams[0].message)).toContain('"item_id": "602"');
362
+ expect(String(agentParams[0].message)).toContain('"group_id": "group-dup-1"');
363
+
364
+ await services[0].stop();
365
+ });
366
+
367
+ test('routes mocked feed notifications to the freshest external session for runtime.subagent', async () => {
368
+ jest.resetModules();
369
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-home-'));
370
+ const sessionStoreDir = path.join(homeDir, '.openclaw', 'agents', 'main', 'sessions');
371
+ fs.mkdirSync(sessionStoreDir, { recursive: true });
372
+ const sessionStorePath = path.join(sessionStoreDir, 'sessions.json');
373
+ fs.writeFileSync(
374
+ sessionStorePath,
375
+ JSON.stringify({
376
+ 'agent:main:main': {
377
+ updatedAt: 100,
378
+ deliveryContext: {
379
+ channel: 'webchat',
380
+ },
381
+ },
382
+ 'agent:main:feishu:direct:ou_feed_target': {
383
+ updatedAt: 200,
384
+ deliveryContext: {
385
+ channel: 'feishu',
386
+ to: 'user:ou_feed_target',
387
+ accountId: 'default',
388
+ },
389
+ },
390
+ }),
391
+ 'utf-8'
392
+ );
393
+
394
+ const { default: plugin } = await import('./index');
395
+ const services: any[] = [];
396
+ const subagentRun = jest.fn().mockResolvedValue({ runId: 'run-subagent-feed' });
397
+
398
+ plugin.register({
399
+ config: {},
400
+ pluginConfig: {
401
+ servers: [
402
+ {
403
+ name: 'eigenflux',
404
+ endpoint: `http://127.0.0.1:${apiPort}`,
405
+ workdir,
406
+ pollInterval: 60,
407
+ sessionStorePath,
408
+ },
409
+ ],
410
+ },
411
+ runtime: {
412
+ subagent: {
413
+ run: subagentRun,
414
+ },
415
+ },
416
+ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
417
+ registerService: (service: any) => {
418
+ services.push(service);
419
+ },
420
+ } as any);
421
+
422
+ expect(services).toHaveLength(1);
423
+ await services[0].start();
424
+ await waitFor(() => subagentRun.mock.calls.length === 1);
425
+
426
+ expect(subagentRun).toHaveBeenCalledWith({
427
+ sessionKey: 'agent:main:feishu:direct:ou_feed_target',
428
+ message: expect.stringContaining('[EIGENFLUX_FEED_PAYLOAD]'),
429
+ deliver: true,
430
+ idempotencyKey: expect.any(String),
431
+ });
432
+ expect(String(subagentRun.mock.calls[0]?.[0]?.message)).toContain('"item_id": "501"');
433
+
434
+ await services[0].stop();
435
+ fs.rmSync(homeDir, { recursive: true, force: true });
436
+ });
437
+ });