@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.3
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 +16 -3
- package/dist/config.d.ts +12 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +47 -10
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/dist/notifier.d.ts +1 -17
- package/dist/notifier.d.ts.map +1 -1
- package/dist/notifier.js +1 -94
- package/dist/notifier.js.map +1 -1
- package/dist/pm-polling-client.d.ts +1 -0
- package/dist/pm-polling-client.d.ts.map +1 -1
- package/dist/pm-polling-client.js +64 -57
- package/dist/pm-polling-client.js.map +1 -1
- package/dist/polling-client.d.ts +1 -0
- package/dist/polling-client.d.ts.map +1 -1
- package/dist/polling-client.js +65 -58
- package/dist/polling-client.js.map +1 -1
- package/openclaw.plugin.json +8 -6
- package/package.json +2 -2
- package/src/agent-prompt-templates.ts +0 -91
- package/src/config.test.ts +0 -188
- package/src/config.ts +0 -410
- package/src/credentials-loader.test.ts +0 -78
- package/src/credentials-loader.ts +0 -121
- package/src/gateway-rpc-client.test.ts +0 -190
- package/src/gateway-rpc-client.ts +0 -373
- package/src/index.integration.test.ts +0 -437
- package/src/index.test.ts +0 -454
- package/src/index.ts +0 -758
- package/src/logger.ts +0 -27
- package/src/notification-route-resolver.test.ts +0 -136
- package/src/notification-route-resolver.ts +0 -430
- package/src/notifier.test.ts +0 -374
- package/src/notifier.ts +0 -558
- package/src/openclaw-plugin-sdk.d.ts +0 -121
- package/src/pm-polling-client.test.ts +0 -390
- package/src/pm-polling-client.ts +0 -257
- package/src/polling-client.test.ts +0 -279
- package/src/polling-client.ts +0 -283
- package/src/session-route-memory.ts +0 -106
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phronesis-io/openclaw-eigenflux",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "OpenClaw plugin for EigenFlux periodic polling delivery",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
|
-
"src",
|
|
9
8
|
"index.ts",
|
|
10
9
|
"openclaw.plugin.json",
|
|
11
10
|
"README.md",
|
|
12
11
|
"LICENSE"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
14
|
+
"bump-version": "node scripts/set-version.mjs",
|
|
15
15
|
"build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc && echo success",
|
|
16
16
|
"build:watch": "tsc --watch",
|
|
17
17
|
"test": "jest --runInBand",
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { AuthRequiredEvent, FeedResponse } from './polling-client';
|
|
2
|
-
import type { PmFetchResponse } from './pm-polling-client';
|
|
3
|
-
|
|
4
|
-
const AUTH_REQUIRED_REASON_TEXT: Record<AuthRequiredEvent['reason'], string> = {
|
|
5
|
-
missing_token: 'No EigenFlux auth token is available.',
|
|
6
|
-
expired_token: 'The EigenFlux auth token has expired.',
|
|
7
|
-
unauthorized: 'The EigenFlux feed request returned HTTP 401.',
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type EigenFluxPromptServerContext = {
|
|
11
|
-
serverName: string;
|
|
12
|
-
workdir: string;
|
|
13
|
-
skillPath: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type BuildAuthRequiredPromptParams = EigenFluxPromptServerContext & {
|
|
17
|
-
authEvent: AuthRequiredEvent;
|
|
18
|
-
maskedToken?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function buildServerContextLines(context: EigenFluxPromptServerContext): string[] {
|
|
22
|
-
return [
|
|
23
|
-
`network=${context.serverName}`,
|
|
24
|
-
`workdir=${context.workdir}`,
|
|
25
|
-
`skill_file=${context.skillPath}`,
|
|
26
|
-
];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function buildAuthRequiredPromptTemplate({
|
|
30
|
-
authEvent,
|
|
31
|
-
maskedToken,
|
|
32
|
-
...context
|
|
33
|
-
}: BuildAuthRequiredPromptParams): string {
|
|
34
|
-
const lines = [
|
|
35
|
-
'[EIGENFLUX_AUTH_REQUIRED]',
|
|
36
|
-
...buildServerContextLines(context),
|
|
37
|
-
AUTH_REQUIRED_REASON_TEXT[authEvent.reason],
|
|
38
|
-
'Guide the user to complete the EigenFlux login or registration flow for this server.',
|
|
39
|
-
'Persist the resulting token to the credentials path below.',
|
|
40
|
-
`credentials_path=${authEvent.credentialsPath}`,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
if (authEvent.source) {
|
|
44
|
-
lines.push(`source=${authEvent.source}`);
|
|
45
|
-
}
|
|
46
|
-
if (authEvent.expiresAt) {
|
|
47
|
-
lines.push(`expires_at=${authEvent.expiresAt}`);
|
|
48
|
-
}
|
|
49
|
-
if (authEvent.statusCode) {
|
|
50
|
-
lines.push(`status_code=${authEvent.statusCode}`);
|
|
51
|
-
}
|
|
52
|
-
if (maskedToken) {
|
|
53
|
-
lines.push(`token=${maskedToken}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return lines.join('\n');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function buildFeedPayloadPromptTemplate(
|
|
60
|
-
payload: FeedResponse,
|
|
61
|
-
context: EigenFluxPromptServerContext
|
|
62
|
-
): string {
|
|
63
|
-
return [
|
|
64
|
-
'[EIGENFLUX_FEED_PAYLOAD]',
|
|
65
|
-
...buildServerContextLines(context),
|
|
66
|
-
'EigenFlux feed payload received.',
|
|
67
|
-
'You must do two things:',
|
|
68
|
-
'1. Decide whether any of these items should be surfaced to the user.',
|
|
69
|
-
'2. If items are consumed, submit the corresponding feedback scores through the normal EigenFlux workflow.',
|
|
70
|
-
'Payload:',
|
|
71
|
-
'```json',
|
|
72
|
-
JSON.stringify(payload, null, 2),
|
|
73
|
-
'```',
|
|
74
|
-
].join('\n');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function buildPmPayloadPromptTemplate(
|
|
78
|
-
payload: PmFetchResponse,
|
|
79
|
-
context: EigenFluxPromptServerContext
|
|
80
|
-
): string {
|
|
81
|
-
return [
|
|
82
|
-
'[EIGENFLUX_PM_PAYLOAD]',
|
|
83
|
-
...buildServerContextLines(context),
|
|
84
|
-
'EigenFlux private messages received.',
|
|
85
|
-
'Review these messages, surface them to the user when appropriate, and respond using the normal EigenFlux workflow when needed.',
|
|
86
|
-
'Payload:',
|
|
87
|
-
'```json',
|
|
88
|
-
JSON.stringify(payload, null, 2),
|
|
89
|
-
'```',
|
|
90
|
-
].join('\n');
|
|
91
|
-
}
|
package/src/config.test.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
PLUGIN_CONFIG,
|
|
7
|
-
resolvePluginConfig,
|
|
8
|
-
resolveServerSkillPath,
|
|
9
|
-
} from './config';
|
|
10
|
-
|
|
11
|
-
describe('resolvePluginConfig', () => {
|
|
12
|
-
test('prepends the default eigenflux server when servers is omitted', () => {
|
|
13
|
-
const config = resolvePluginConfig({});
|
|
14
|
-
|
|
15
|
-
expect(config.gatewayUrl).toBe(PLUGIN_CONFIG.DEFAULT_GATEWAY_URL);
|
|
16
|
-
expect(config.openclawCliBin).toBe(PLUGIN_CONFIG.DEFAULT_OPENCLAW_CLI_BIN);
|
|
17
|
-
expect(config.servers).toHaveLength(1);
|
|
18
|
-
expect(config.servers[0]).toEqual({
|
|
19
|
-
enabled: true,
|
|
20
|
-
name: 'eigenflux',
|
|
21
|
-
endpoint: PLUGIN_CONFIG.DEFAULT_ENDPOINT,
|
|
22
|
-
workdir: path.join(os.homedir(), '.openclaw/eigenflux'),
|
|
23
|
-
pollIntervalSec: PLUGIN_CONFIG.DEFAULT_POLL_INTERVAL_SEC,
|
|
24
|
-
pmPollIntervalSec: PLUGIN_CONFIG.DEFAULT_PM_POLL_INTERVAL_SEC,
|
|
25
|
-
sessionKey: PLUGIN_CONFIG.DEFAULT_SESSION_KEY,
|
|
26
|
-
agentId: PLUGIN_CONFIG.DEFAULT_AGENT_ID,
|
|
27
|
-
replyChannel: undefined,
|
|
28
|
-
replyTo: undefined,
|
|
29
|
-
replyAccountId: undefined,
|
|
30
|
-
routeOverrides: {
|
|
31
|
-
sessionKey: false,
|
|
32
|
-
agentId: false,
|
|
33
|
-
replyChannel: false,
|
|
34
|
-
replyTo: false,
|
|
35
|
-
replyAccountId: false,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('prepends the default eigenflux server when no explicit eigenflux server exists', () => {
|
|
41
|
-
const config = resolvePluginConfig({
|
|
42
|
-
gatewayUrl: 'ws://127.0.0.1:29999',
|
|
43
|
-
openclawCliBin: '/opt/bin/openclaw',
|
|
44
|
-
servers: [
|
|
45
|
-
{
|
|
46
|
-
name: 'alpha',
|
|
47
|
-
endpoint: 'https://alpha.example.com',
|
|
48
|
-
workdir: '~/custom/alpha',
|
|
49
|
-
pollInterval: 45,
|
|
50
|
-
pmPollInterval: 30,
|
|
51
|
-
sessionKey: 'agent:ops:feishu:direct:ou_alpha',
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(config.gatewayUrl).toBe('ws://127.0.0.1:29999');
|
|
57
|
-
expect(config.openclawCliBin).toBe('/opt/bin/openclaw');
|
|
58
|
-
expect(config.servers).toHaveLength(2);
|
|
59
|
-
expect(config.servers[0]).toEqual(
|
|
60
|
-
expect.objectContaining({
|
|
61
|
-
name: 'eigenflux',
|
|
62
|
-
endpoint: PLUGIN_CONFIG.DEFAULT_ENDPOINT,
|
|
63
|
-
workdir: path.join(os.homedir(), '.openclaw/eigenflux'),
|
|
64
|
-
})
|
|
65
|
-
);
|
|
66
|
-
expect(config.servers[1]).toEqual({
|
|
67
|
-
enabled: true,
|
|
68
|
-
name: 'alpha',
|
|
69
|
-
endpoint: 'https://alpha.example.com',
|
|
70
|
-
workdir: path.join(os.homedir(), 'custom/alpha'),
|
|
71
|
-
pollIntervalSec: 45,
|
|
72
|
-
pmPollIntervalSec: 30,
|
|
73
|
-
sessionKey: 'agent:ops:feishu:direct:ou_alpha',
|
|
74
|
-
agentId: 'ops',
|
|
75
|
-
replyChannel: 'feishu',
|
|
76
|
-
replyTo: 'ou_alpha',
|
|
77
|
-
replyAccountId: undefined,
|
|
78
|
-
routeOverrides: {
|
|
79
|
-
sessionKey: true,
|
|
80
|
-
agentId: false,
|
|
81
|
-
replyChannel: false,
|
|
82
|
-
replyTo: false,
|
|
83
|
-
replyAccountId: false,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('does not prepend another default server when eigenflux is explicitly configured', () => {
|
|
89
|
-
const config = resolvePluginConfig({
|
|
90
|
-
servers: [
|
|
91
|
-
{
|
|
92
|
-
name: 'eigenflux',
|
|
93
|
-
workdir: '~/custom/eigenflux',
|
|
94
|
-
pollInterval: 15,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: 'alpha',
|
|
98
|
-
endpoint: 'https://alpha.example.com',
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
expect(config.servers).toHaveLength(2);
|
|
104
|
-
expect(config.servers[0]).toEqual(
|
|
105
|
-
expect.objectContaining({
|
|
106
|
-
name: 'eigenflux',
|
|
107
|
-
endpoint: 'https://www.eigenflux.ai',
|
|
108
|
-
workdir: path.join(os.homedir(), 'custom/eigenflux'),
|
|
109
|
-
pollIntervalSec: 15,
|
|
110
|
-
})
|
|
111
|
-
);
|
|
112
|
-
expect(config.servers[1]).toEqual(
|
|
113
|
-
expect.objectContaining({
|
|
114
|
-
name: 'alpha',
|
|
115
|
-
endpoint: 'https://alpha.example.com',
|
|
116
|
-
})
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('creates unique names when duplicate server names are configured', () => {
|
|
121
|
-
const config = resolvePluginConfig({
|
|
122
|
-
servers: [{ name: 'eigenflux' }, { name: 'eigenflux' }, {}],
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(config.servers.map((server) => server.name)).toEqual([
|
|
126
|
-
'eigenflux',
|
|
127
|
-
'eigenflux-2',
|
|
128
|
-
'server-3',
|
|
129
|
-
]);
|
|
130
|
-
expect(config.servers[2].workdir).toBe(path.join(os.homedir(), '.openclaw/server-3'));
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('uses gateway auth token from host config when plugin config omits it', () => {
|
|
134
|
-
const config = resolvePluginConfig(
|
|
135
|
-
{
|
|
136
|
-
servers: [{ name: 'eigenflux' }],
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
gateway: {
|
|
140
|
-
auth: {
|
|
141
|
-
token: 'gw_host_token',
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
expect(config.gatewayToken).toBe('gw_host_token');
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('resolveServerSkillPath', () => {
|
|
152
|
-
test('prefers local workdir skill.md when it exists', () => {
|
|
153
|
-
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-skill-path-'));
|
|
154
|
-
const localSkillPath = path.join(workdir, 'skill.md');
|
|
155
|
-
fs.writeFileSync(localSkillPath, '# local skill\n', 'utf-8');
|
|
156
|
-
|
|
157
|
-
expect(
|
|
158
|
-
resolveServerSkillPath({
|
|
159
|
-
endpoint: 'https://example.com/root',
|
|
160
|
-
workdir,
|
|
161
|
-
})
|
|
162
|
-
).toBe(localSkillPath);
|
|
163
|
-
|
|
164
|
-
fs.rmSync(workdir, { recursive: true, force: true });
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('falls back to endpoint skill.md when local file is absent', () => {
|
|
168
|
-
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-skill-path-'));
|
|
169
|
-
|
|
170
|
-
expect(
|
|
171
|
-
resolveServerSkillPath({
|
|
172
|
-
endpoint: 'https://example.com/root',
|
|
173
|
-
workdir,
|
|
174
|
-
})
|
|
175
|
-
).toBe('https://example.com/root/skill.md');
|
|
176
|
-
|
|
177
|
-
fs.rmSync(workdir, { recursive: true, force: true });
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('PLUGIN_CONFIG USER_AGENT', () => {
|
|
182
|
-
test('includes eigenflux plugin version', () => {
|
|
183
|
-
expect(PLUGIN_CONFIG.USER_AGENT).toContain('eigenflux-plugin');
|
|
184
|
-
expect(PLUGIN_CONFIG.USER_AGENT).toContain('node/');
|
|
185
|
-
expect(PLUGIN_CONFIG.USER_AGENT).toMatch(/\(.*;\s*.*;\s*.*\)/);
|
|
186
|
-
expect(PLUGIN_CONFIG.PLUGIN_VERSION).toBe('0.0.1-alpha.0');
|
|
187
|
-
});
|
|
188
|
-
});
|
package/src/config.ts
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal configuration for the EigenFlux plugin.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
|
|
9
|
-
const PLUGIN_VERSION = '0.0.1-alpha.0';
|
|
10
|
-
const DEFAULT_SERVER_NAME = 'eigenflux';
|
|
11
|
-
const DEFAULT_ENDPOINT = 'https://www.eigenflux.ai';
|
|
12
|
-
const DEFAULT_GATEWAY_URL = 'ws://127.0.0.1:18789';
|
|
13
|
-
const DEFAULT_SESSION_KEY = 'main';
|
|
14
|
-
const DEFAULT_AGENT_ID = 'main';
|
|
15
|
-
const DEFAULT_OPENCLAW_CLI_BIN = 'openclaw';
|
|
16
|
-
const DEFAULT_POLL_INTERVAL_SEC = 300;
|
|
17
|
-
const DEFAULT_PM_POLL_INTERVAL_SEC = 60;
|
|
18
|
-
|
|
19
|
-
export type NotificationRouteOverrides = {
|
|
20
|
-
sessionKey: boolean;
|
|
21
|
-
agentId: boolean;
|
|
22
|
-
replyChannel: boolean;
|
|
23
|
-
replyTo: boolean;
|
|
24
|
-
replyAccountId: boolean;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type EigenFluxServerConfig = {
|
|
28
|
-
enabled?: boolean;
|
|
29
|
-
name?: string;
|
|
30
|
-
endpoint?: string;
|
|
31
|
-
workdir?: string;
|
|
32
|
-
pollInterval?: number;
|
|
33
|
-
pmPollInterval?: number;
|
|
34
|
-
sessionKey?: string;
|
|
35
|
-
agentId?: string;
|
|
36
|
-
replyChannel?: string;
|
|
37
|
-
replyTo?: string;
|
|
38
|
-
replyAccountId?: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type EigenFluxPluginConfig = {
|
|
42
|
-
gatewayUrl?: string;
|
|
43
|
-
gatewayToken?: string;
|
|
44
|
-
openclawCliBin?: string;
|
|
45
|
-
servers?: EigenFluxServerConfig[];
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export type ResolvedEigenFluxServerConfig = {
|
|
49
|
-
enabled: boolean;
|
|
50
|
-
name: string;
|
|
51
|
-
endpoint: string;
|
|
52
|
-
workdir: string;
|
|
53
|
-
pollIntervalSec: number;
|
|
54
|
-
pmPollIntervalSec: number;
|
|
55
|
-
sessionKey: string;
|
|
56
|
-
agentId: string;
|
|
57
|
-
replyChannel?: string;
|
|
58
|
-
replyTo?: string;
|
|
59
|
-
replyAccountId?: string;
|
|
60
|
-
routeOverrides: NotificationRouteOverrides;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export type ResolvedEigenFluxPluginConfig = {
|
|
64
|
-
gatewayUrl: string;
|
|
65
|
-
gatewayToken?: string;
|
|
66
|
-
openclawCliBin: string;
|
|
67
|
-
servers: ResolvedEigenFluxServerConfig[];
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
type GlobalGatewayConfig = {
|
|
71
|
-
gateway?: {
|
|
72
|
-
auth?: {
|
|
73
|
-
token?: string;
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
type DerivedNotificationRoute = {
|
|
79
|
-
agentId?: string;
|
|
80
|
-
replyChannel?: string;
|
|
81
|
-
replyTo?: string;
|
|
82
|
-
replyAccountId?: string;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
function detectOpenClawVersion(): string | undefined {
|
|
86
|
-
try {
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
88
|
-
const pkg = require('openclaw/package.json') as { version?: string };
|
|
89
|
-
return pkg.version;
|
|
90
|
-
} catch {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildUserAgent(): string {
|
|
96
|
-
const parts: string[] = [];
|
|
97
|
-
|
|
98
|
-
parts.push(`node/${process.version.replace(/^v/, '')}`);
|
|
99
|
-
parts.push(`(${os.platform()}; ${os.arch()}; ${os.release()})`);
|
|
100
|
-
|
|
101
|
-
const openclawVersion = detectOpenClawVersion();
|
|
102
|
-
if (openclawVersion) {
|
|
103
|
-
parts.push(`openclaw/${openclawVersion}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
parts.push(`eigenflux-plugin/${PLUGIN_VERSION}`);
|
|
107
|
-
|
|
108
|
-
return parts.join(' ');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
112
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function readNonEmptyString(value: unknown): string | undefined {
|
|
116
|
-
if (typeof value !== 'string') {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
const trimmed = value.trim();
|
|
120
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function parsePositiveInteger(value: unknown, fallback: number): number {
|
|
124
|
-
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
125
|
-
return Math.floor(value);
|
|
126
|
-
}
|
|
127
|
-
if (typeof value === 'string') {
|
|
128
|
-
const parsed = parseInt(value, 10);
|
|
129
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
130
|
-
return parsed;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return fallback;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isSessionPeerShape(value: string | undefined): boolean {
|
|
137
|
-
const normalized = value?.trim().toLowerCase();
|
|
138
|
-
return (
|
|
139
|
-
normalized === 'direct' ||
|
|
140
|
-
normalized === 'dm' ||
|
|
141
|
-
normalized === 'group' ||
|
|
142
|
-
normalized === 'channel'
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function deriveNotificationRoute(sessionKey: string | undefined): DerivedNotificationRoute {
|
|
147
|
-
const trimmed = readNonEmptyString(sessionKey);
|
|
148
|
-
if (!trimmed) {
|
|
149
|
-
return {};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const parts = trimmed.split(':').filter((part) => part.length > 0);
|
|
153
|
-
if (parts.length < 3 || parts[0]?.toLowerCase() !== 'agent') {
|
|
154
|
-
return {};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const agentId = readNonEmptyString(parts[1]);
|
|
158
|
-
if (parts.length >= 6 && isSessionPeerShape(parts[4])) {
|
|
159
|
-
return {
|
|
160
|
-
agentId,
|
|
161
|
-
replyChannel: readNonEmptyString(parts[2]),
|
|
162
|
-
replyAccountId: readNonEmptyString(parts[3]),
|
|
163
|
-
replyTo: readNonEmptyString(parts.slice(5).join(':')),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (parts.length >= 5 && isSessionPeerShape(parts[3])) {
|
|
168
|
-
return {
|
|
169
|
-
agentId,
|
|
170
|
-
replyChannel: readNonEmptyString(parts[2]),
|
|
171
|
-
replyTo: readNonEmptyString(parts.slice(4).join(':')),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { agentId };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function createRouteOverrides(
|
|
179
|
-
normalized: Record<string, unknown>
|
|
180
|
-
): NotificationRouteOverrides {
|
|
181
|
-
return {
|
|
182
|
-
sessionKey: readNonEmptyString(normalized.sessionKey) !== undefined,
|
|
183
|
-
agentId: readNonEmptyString(normalized.agentId) !== undefined,
|
|
184
|
-
replyChannel: readNonEmptyString(normalized.replyChannel) !== undefined,
|
|
185
|
-
replyTo: readNonEmptyString(normalized.replyTo) !== undefined,
|
|
186
|
-
replyAccountId: readNonEmptyString(normalized.replyAccountId) !== undefined,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function hasExplicitDefaultServer(servers: Record<string, unknown>[]): boolean {
|
|
191
|
-
return servers.some(
|
|
192
|
-
(server) => readNonEmptyString(server.name)?.toLowerCase() === DEFAULT_SERVER_NAME
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function normalizeServersInput(config: Record<string, unknown>): EigenFluxServerConfig[] {
|
|
197
|
-
const explicitServers = Array.isArray(config.servers)
|
|
198
|
-
? config.servers.filter(isRecord)
|
|
199
|
-
: [];
|
|
200
|
-
|
|
201
|
-
if (!hasExplicitDefaultServer(explicitServers)) {
|
|
202
|
-
return [{} as EigenFluxServerConfig, ...explicitServers];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return explicitServers;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function createServerName(baseName: string, usedNames: Set<string>): string {
|
|
209
|
-
if (!usedNames.has(baseName)) {
|
|
210
|
-
usedNames.add(baseName);
|
|
211
|
-
return baseName;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
let suffix = 2;
|
|
215
|
-
while (usedNames.has(`${baseName}-${suffix}`)) {
|
|
216
|
-
suffix += 1;
|
|
217
|
-
}
|
|
218
|
-
const uniqueName = `${baseName}-${suffix}`;
|
|
219
|
-
usedNames.add(uniqueName);
|
|
220
|
-
return uniqueName;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function resolveServerConfig(
|
|
224
|
-
serverConfig: unknown,
|
|
225
|
-
index: number,
|
|
226
|
-
usedNames: Set<string>
|
|
227
|
-
): ResolvedEigenFluxServerConfig {
|
|
228
|
-
const normalized = isRecord(serverConfig) ? serverConfig : {};
|
|
229
|
-
const rawName =
|
|
230
|
-
readNonEmptyString(normalized.name) ??
|
|
231
|
-
(index === 0 ? DEFAULT_SERVER_NAME : `server-${index + 1}`);
|
|
232
|
-
const name = createServerName(rawName, usedNames);
|
|
233
|
-
const sessionKey = readNonEmptyString(normalized.sessionKey) ?? DEFAULT_SESSION_KEY;
|
|
234
|
-
const derivedRoute = deriveNotificationRoute(sessionKey);
|
|
235
|
-
const workdir = expandHomeDir(
|
|
236
|
-
readNonEmptyString(normalized.workdir) ?? `~/.openclaw/${name}`
|
|
237
|
-
);
|
|
238
|
-
const sessionStorePath = readNonEmptyString(normalized.sessionStorePath);
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
enabled: normalized.enabled !== false,
|
|
242
|
-
name,
|
|
243
|
-
endpoint: readNonEmptyString(normalized.endpoint) ?? DEFAULT_ENDPOINT,
|
|
244
|
-
workdir,
|
|
245
|
-
pollIntervalSec: parsePositiveInteger(normalized.pollInterval, DEFAULT_POLL_INTERVAL_SEC),
|
|
246
|
-
pmPollIntervalSec: parsePositiveInteger(
|
|
247
|
-
normalized.pmPollInterval,
|
|
248
|
-
DEFAULT_PM_POLL_INTERVAL_SEC
|
|
249
|
-
),
|
|
250
|
-
sessionKey,
|
|
251
|
-
agentId: readNonEmptyString(normalized.agentId) ?? derivedRoute.agentId ?? DEFAULT_AGENT_ID,
|
|
252
|
-
replyChannel: readNonEmptyString(normalized.replyChannel) ?? derivedRoute.replyChannel,
|
|
253
|
-
replyTo: readNonEmptyString(normalized.replyTo) ?? derivedRoute.replyTo,
|
|
254
|
-
replyAccountId:
|
|
255
|
-
readNonEmptyString(normalized.replyAccountId) ?? derivedRoute.replyAccountId,
|
|
256
|
-
routeOverrides: createRouteOverrides(normalized),
|
|
257
|
-
...(sessionStorePath ? { sessionStorePath: expandHomeDir(sessionStorePath) } : {}),
|
|
258
|
-
} as ResolvedEigenFluxServerConfig;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function expandHomeDir(input: string): string {
|
|
262
|
-
if (input === '~') {
|
|
263
|
-
return os.homedir();
|
|
264
|
-
}
|
|
265
|
-
if (input.startsWith('~/')) {
|
|
266
|
-
return path.join(os.homedir(), input.slice(2));
|
|
267
|
-
}
|
|
268
|
-
return input;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function buildSkillUrl(endpoint: string): string {
|
|
272
|
-
try {
|
|
273
|
-
return new URL('skill.md', endpoint.endsWith('/') ? endpoint : `${endpoint}/`).toString();
|
|
274
|
-
} catch {
|
|
275
|
-
return `${endpoint.replace(/\/+$/u, '')}/skill.md`;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function resolveServerSkillPath(server: {
|
|
280
|
-
endpoint: string;
|
|
281
|
-
workdir: string;
|
|
282
|
-
}): string {
|
|
283
|
-
const localSkillPath = path.join(server.workdir, 'skill.md');
|
|
284
|
-
if (fs.existsSync(localSkillPath)) {
|
|
285
|
-
return localSkillPath;
|
|
286
|
-
}
|
|
287
|
-
return buildSkillUrl(server.endpoint);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function resolvePluginConfig(
|
|
291
|
-
pluginConfig: unknown,
|
|
292
|
-
globalConfig?: GlobalGatewayConfig
|
|
293
|
-
): ResolvedEigenFluxPluginConfig {
|
|
294
|
-
const normalized = isRecord(pluginConfig) ? pluginConfig : {};
|
|
295
|
-
const usedNames = new Set<string>();
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
gatewayUrl: readNonEmptyString(normalized.gatewayUrl) ?? DEFAULT_GATEWAY_URL,
|
|
299
|
-
gatewayToken:
|
|
300
|
-
readNonEmptyString(normalized.gatewayToken) ??
|
|
301
|
-
readNonEmptyString(globalConfig?.gateway?.auth?.token),
|
|
302
|
-
openclawCliBin:
|
|
303
|
-
readNonEmptyString(normalized.openclawCliBin) ?? DEFAULT_OPENCLAW_CLI_BIN,
|
|
304
|
-
servers: normalizeServersInput(normalized).map((server, index) =>
|
|
305
|
-
resolveServerConfig(server, index, usedNames)
|
|
306
|
-
),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const SERVER_CONFIG_SCHEMA = {
|
|
311
|
-
type: 'object',
|
|
312
|
-
additionalProperties: false,
|
|
313
|
-
properties: {
|
|
314
|
-
enabled: {
|
|
315
|
-
type: 'boolean',
|
|
316
|
-
description: 'Enable or disable background polling for this server',
|
|
317
|
-
default: true,
|
|
318
|
-
},
|
|
319
|
-
name: {
|
|
320
|
-
type: 'string',
|
|
321
|
-
description: 'Server name used for routing, workdir defaults, and diagnostics',
|
|
322
|
-
default: DEFAULT_SERVER_NAME,
|
|
323
|
-
},
|
|
324
|
-
endpoint: {
|
|
325
|
-
type: 'string',
|
|
326
|
-
description: 'EigenFlux API base URL for this server',
|
|
327
|
-
default: DEFAULT_ENDPOINT,
|
|
328
|
-
},
|
|
329
|
-
workdir: {
|
|
330
|
-
type: 'string',
|
|
331
|
-
description: 'Directory used to store server credentials and remembered session state',
|
|
332
|
-
},
|
|
333
|
-
pollInterval: {
|
|
334
|
-
type: 'integer',
|
|
335
|
-
minimum: 1,
|
|
336
|
-
description: 'Feed polling interval in seconds for this server',
|
|
337
|
-
default: DEFAULT_POLL_INTERVAL_SEC,
|
|
338
|
-
},
|
|
339
|
-
pmPollInterval: {
|
|
340
|
-
type: 'integer',
|
|
341
|
-
minimum: 1,
|
|
342
|
-
description: 'Private message polling interval in seconds for this server',
|
|
343
|
-
default: DEFAULT_PM_POLL_INTERVAL_SEC,
|
|
344
|
-
},
|
|
345
|
-
sessionKey: {
|
|
346
|
-
type: 'string',
|
|
347
|
-
description: 'Target session key used by runtime.subagent and heartbeat fallback',
|
|
348
|
-
default: DEFAULT_SESSION_KEY,
|
|
349
|
-
},
|
|
350
|
-
agentId: {
|
|
351
|
-
type: 'string',
|
|
352
|
-
description: 'Agent id used by Gateway agent and CLI fallbacks',
|
|
353
|
-
default: DEFAULT_AGENT_ID,
|
|
354
|
-
},
|
|
355
|
-
replyChannel: {
|
|
356
|
-
type: 'string',
|
|
357
|
-
description: 'Explicit reply channel used by Gateway agent and CLI fallbacks',
|
|
358
|
-
},
|
|
359
|
-
replyTo: {
|
|
360
|
-
type: 'string',
|
|
361
|
-
description: 'Explicit reply target used by Gateway agent and CLI fallbacks',
|
|
362
|
-
},
|
|
363
|
-
replyAccountId: {
|
|
364
|
-
type: 'string',
|
|
365
|
-
description: 'Optional reply account id for multi-account channel delivery',
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
} as const;
|
|
369
|
-
|
|
370
|
-
export const PLUGIN_CONFIG = {
|
|
371
|
-
DEFAULT_SERVER_NAME,
|
|
372
|
-
DEFAULT_ENDPOINT,
|
|
373
|
-
DEFAULT_GATEWAY_URL,
|
|
374
|
-
DEFAULT_SESSION_KEY,
|
|
375
|
-
DEFAULT_AGENT_ID,
|
|
376
|
-
DEFAULT_OPENCLAW_CLI_BIN,
|
|
377
|
-
DEFAULT_POLL_INTERVAL_SEC,
|
|
378
|
-
DEFAULT_PM_POLL_INTERVAL_SEC,
|
|
379
|
-
CREDENTIALS_FILE: 'credentials.json',
|
|
380
|
-
PLUGIN_VERSION,
|
|
381
|
-
USER_AGENT: buildUserAgent(),
|
|
382
|
-
} as const;
|
|
383
|
-
|
|
384
|
-
export const PLUGIN_CONFIG_SCHEMA = {
|
|
385
|
-
type: 'object',
|
|
386
|
-
additionalProperties: false,
|
|
387
|
-
properties: {
|
|
388
|
-
gatewayUrl: {
|
|
389
|
-
type: 'string',
|
|
390
|
-
description: 'OpenClaw Gateway WebSocket URL used for Gateway RPC fallback',
|
|
391
|
-
default: DEFAULT_GATEWAY_URL,
|
|
392
|
-
},
|
|
393
|
-
gatewayToken: {
|
|
394
|
-
type: 'string',
|
|
395
|
-
description: 'Optional gateway token override used for Gateway RPC fallback',
|
|
396
|
-
},
|
|
397
|
-
openclawCliBin: {
|
|
398
|
-
type: 'string',
|
|
399
|
-
description: 'OpenClaw CLI binary used by runtime command and spawn fallbacks',
|
|
400
|
-
default: DEFAULT_OPENCLAW_CLI_BIN,
|
|
401
|
-
},
|
|
402
|
-
servers: {
|
|
403
|
-
type: 'array',
|
|
404
|
-
description:
|
|
405
|
-
'Server list. When empty or when no server named eigenflux is provided, the plugin prepends a default eigenflux server.',
|
|
406
|
-
default: [],
|
|
407
|
-
items: SERVER_CONFIG_SCHEMA,
|
|
408
|
-
},
|
|
409
|
-
},
|
|
410
|
-
} as const;
|