@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.2
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 +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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/openclaw.plugin.json +4 -4
- package/package.json +1 -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/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;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
|
|
5
|
-
import { CredentialsLoader } from './credentials-loader';
|
|
6
|
-
import { Logger } from './logger';
|
|
7
|
-
|
|
8
|
-
function createLogger(): Logger {
|
|
9
|
-
return new Logger({
|
|
10
|
-
info: jest.fn(),
|
|
11
|
-
warn: jest.fn(),
|
|
12
|
-
error: jest.fn(),
|
|
13
|
-
debug: jest.fn(),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('CredentialsLoader', () => {
|
|
18
|
-
let workdir: string;
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-workdir-'));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
fs.rmSync(workdir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('loads access token from credentials.json', () => {
|
|
29
|
-
fs.writeFileSync(
|
|
30
|
-
path.join(workdir, 'credentials.json'),
|
|
31
|
-
JSON.stringify({ access_token: 'at_file_token' }),
|
|
32
|
-
'utf-8'
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const loader = new CredentialsLoader(createLogger(), workdir);
|
|
36
|
-
expect(loader.loadAccessToken()).toBe('at_file_token');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('returns null when credentials file is missing', () => {
|
|
40
|
-
const loader = new CredentialsLoader(createLogger(), workdir);
|
|
41
|
-
expect(loader.loadAccessToken()).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('returns expired auth state when credentials file token is stale', () => {
|
|
45
|
-
fs.writeFileSync(
|
|
46
|
-
path.join(workdir, 'credentials.json'),
|
|
47
|
-
JSON.stringify({
|
|
48
|
-
access_token: 'at_expired_token',
|
|
49
|
-
expires_at: Date.now() - 1_000,
|
|
50
|
-
}),
|
|
51
|
-
'utf-8'
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const loader = new CredentialsLoader(createLogger(), workdir);
|
|
55
|
-
expect(loader.loadAuthState()).toEqual(
|
|
56
|
-
expect.objectContaining({
|
|
57
|
-
status: 'expired',
|
|
58
|
-
source: 'file',
|
|
59
|
-
})
|
|
60
|
-
);
|
|
61
|
-
expect(loader.loadAccessToken()).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('saveAccessToken creates the workdir and writes credentials.json', () => {
|
|
65
|
-
const nestedWorkdir = path.join(workdir, 'nested/eigenflux');
|
|
66
|
-
const loader = new CredentialsLoader(createLogger(), nestedWorkdir);
|
|
67
|
-
|
|
68
|
-
loader.saveAccessToken('at_saved_token', 'bot@example.com', 1_760_000_000_000);
|
|
69
|
-
|
|
70
|
-
const credentialsPath = path.join(nestedWorkdir, 'credentials.json');
|
|
71
|
-
expect(fs.existsSync(credentialsPath)).toBe(true);
|
|
72
|
-
expect(JSON.parse(fs.readFileSync(credentialsPath, 'utf-8'))).toEqual({
|
|
73
|
-
access_token: 'at_saved_token',
|
|
74
|
-
email: 'bot@example.com',
|
|
75
|
-
expires_at: 1_760_000_000_000,
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|