@phronesis-io/openclaw-eigenflux 0.0.7 → 0.0.9-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +9 -23
- package/dist/index.js +2263 -463
- package/openclaw.plugin.json +5 -1
- package/package.json +21 -8
- package/skills/ef-communication/SKILL.md +1 -0
- package/skills/ef-communication/references/relations.md +13 -0
- package/dist/agent-prompt-templates.d.ts +0 -19
- package/dist/agent-prompt-templates.d.ts.map +0 -1
- package/dist/agent-prompt-templates.js +0 -56
- package/dist/agent-prompt-templates.js.map +0 -1
- package/dist/cli-executor.d.ts +0 -32
- package/dist/cli-executor.d.ts.map +0 -1
- package/dist/cli-executor.js +0 -75
- package/dist/cli-executor.js.map +0 -1
- package/dist/config.d.ts +0 -83
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -226
- package/dist/config.js.map +0 -1
- package/dist/credentials-loader.d.ts +0 -29
- package/dist/credentials-loader.d.ts.map +0 -1
- package/dist/credentials-loader.js +0 -117
- package/dist/credentials-loader.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -12
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -25
- package/dist/logger.js.map +0 -1
- package/dist/notification-route-resolver.d.ts +0 -43
- package/dist/notification-route-resolver.d.ts.map +0 -1
- package/dist/notification-route-resolver.js +0 -533
- package/dist/notification-route-resolver.js.map +0 -1
- package/dist/notifier.d.ts +0 -39
- package/dist/notifier.d.ts.map +0 -1
- package/dist/notifier.js +0 -340
- package/dist/notifier.js.map +0 -1
- package/dist/polling-client.d.ts +0 -86
- package/dist/polling-client.d.ts.map +0 -1
- package/dist/polling-client.js +0 -158
- package/dist/polling-client.js.map +0 -1
- package/dist/reply-target.d.ts +0 -8
- package/dist/reply-target.d.ts.map +0 -1
- package/dist/reply-target.js +0 -104
- package/dist/reply-target.js.map +0 -1
- package/dist/session-route-memory.d.ts +0 -22
- package/dist/session-route-memory.d.ts.map +0 -1
- package/dist/session-route-memory.js +0 -117
- package/dist/session-route-memory.js.map +0 -1
- package/dist/stream-client.d.ts +0 -48
- package/dist/stream-client.d.ts.map +0 -1
- package/dist/stream-client.js +0 -168
- package/dist/stream-client.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,528 +1,2328 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const notifier_1 = require("./notifier");
|
|
12
|
-
const reply_target_1 = require("./reply-target");
|
|
13
|
-
const session_route_memory_1 = require("./session-route-memory");
|
|
14
|
-
const COMMAND_NAMES = ['auth', 'profile', 'servers', 'feed', 'pm', 'here', 'version'];
|
|
15
|
-
const COMMAND_NAME_SET = new Set(COMMAND_NAMES);
|
|
16
|
-
const DEFAULT_ROUTING = {
|
|
17
|
-
sessionKey: config_1.PLUGIN_CONFIG.DEFAULT_SESSION_KEY,
|
|
18
|
-
agentId: config_1.PLUGIN_CONFIG.DEFAULT_AGENT_ID,
|
|
19
|
-
routeOverrides: {
|
|
20
|
-
sessionKey: false,
|
|
21
|
-
agentId: false,
|
|
22
|
-
replyChannel: false,
|
|
23
|
-
replyTo: false,
|
|
24
|
-
replyAccountId: false,
|
|
25
|
-
},
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
11
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => index_default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_plugin_entry = require("openclaw/plugin-sdk/plugin-entry");
|
|
37
|
+
|
|
38
|
+
// src/cli-executor.ts
|
|
39
|
+
var import_child_process = require("child_process");
|
|
40
|
+
var EXIT_AUTH_REQUIRED = 4;
|
|
41
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
42
|
+
function execEigenflux(bin, args, options) {
|
|
43
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
44
|
+
const logger = options?.logger;
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
logger?.debug(`execEigenflux: ${bin} ${args.join(" ")}`);
|
|
47
|
+
(0, import_child_process.execFile)(
|
|
48
|
+
bin,
|
|
49
|
+
args,
|
|
50
|
+
{
|
|
51
|
+
timeout,
|
|
52
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
...options?.cwd ? { cwd: options.cwd } : {}
|
|
55
|
+
},
|
|
56
|
+
(error, stdout, stderr) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
const exitCode = error.code;
|
|
59
|
+
if (exitCode === "ENOENT") {
|
|
60
|
+
logger?.warn(`execEigenflux: binary not found: ${bin}`);
|
|
61
|
+
resolve({ kind: "not_installed", bin });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const numericExit = typeof exitCode === "number" ? exitCode : error.killed ? null : error.status ?? null;
|
|
65
|
+
if (numericExit === EXIT_AUTH_REQUIRED) {
|
|
66
|
+
logger?.warn(`execEigenflux auth required: ${stderr.trim()}`);
|
|
67
|
+
resolve({ kind: "auth_required", stderr: stderr.trim() });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
logger?.error(`execEigenflux failed (exit=${numericExit}): ${stderr.trim() || error.message}`);
|
|
71
|
+
resolve({
|
|
72
|
+
kind: "error",
|
|
73
|
+
error: new Error(stderr.trim() || error.message),
|
|
74
|
+
exitCode: numericExit,
|
|
75
|
+
stderr: stderr.trim()
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const trimmed = stdout.trim();
|
|
80
|
+
if (!trimmed) {
|
|
81
|
+
resolve({
|
|
82
|
+
kind: "success",
|
|
83
|
+
data: void 0
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
if (options?.parseJson === false) {
|
|
88
|
+
resolve({ kind: "success", data: trimmed });
|
|
89
|
+
return;
|
|
86
90
|
}
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(trimmed);
|
|
93
|
+
resolve({ kind: "success", data });
|
|
94
|
+
} catch (parseError) {
|
|
95
|
+
logger?.error(`execEigenflux JSON parse error: ${parseError.message}`);
|
|
96
|
+
resolve({
|
|
97
|
+
kind: "error",
|
|
98
|
+
error: new Error(`Failed to parse CLI output: ${parseError.message}`),
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
stderr: ""
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/polling-client.ts
|
|
109
|
+
var POLL_INTERVAL_CONFIG_KEY = "feed_poll_interval";
|
|
110
|
+
var DEFAULT_POLL_INTERVAL_SEC = 600;
|
|
111
|
+
var MIN_POLL_INTERVAL_SEC = 10;
|
|
112
|
+
var MAX_POLL_INTERVAL_SEC = 24 * 60 * 60;
|
|
113
|
+
async function readPollIntervalSec(eigenfluxBin, serverName, logger) {
|
|
114
|
+
const result = await execEigenflux(
|
|
115
|
+
eigenfluxBin,
|
|
116
|
+
["config", "get", "--key", POLL_INTERVAL_CONFIG_KEY, "--server", serverName, "--format", "json"],
|
|
117
|
+
{ logger }
|
|
118
|
+
);
|
|
119
|
+
if (result.kind !== "success" || result.data === void 0 || result.data === null) {
|
|
120
|
+
return DEFAULT_POLL_INTERVAL_SEC;
|
|
121
|
+
}
|
|
122
|
+
let numeric;
|
|
123
|
+
if (typeof result.data === "number" && Number.isFinite(result.data)) {
|
|
124
|
+
numeric = result.data;
|
|
125
|
+
} else if (typeof result.data === "string") {
|
|
126
|
+
const parsed = Number(result.data.trim());
|
|
127
|
+
if (Number.isFinite(parsed)) {
|
|
128
|
+
numeric = parsed;
|
|
87
129
|
}
|
|
88
|
-
|
|
130
|
+
}
|
|
131
|
+
if (numeric === void 0) {
|
|
132
|
+
logger.warn(
|
|
133
|
+
`Ignoring non-numeric pollInterval from eigenflux config (server=${serverName}, value=${JSON.stringify(result.data)}); using ${DEFAULT_POLL_INTERVAL_SEC}s`
|
|
134
|
+
);
|
|
135
|
+
return DEFAULT_POLL_INTERVAL_SEC;
|
|
136
|
+
}
|
|
137
|
+
const floored = Math.floor(numeric);
|
|
138
|
+
if (floored < MIN_POLL_INTERVAL_SEC || floored > MAX_POLL_INTERVAL_SEC) {
|
|
139
|
+
logger.warn(
|
|
140
|
+
`pollInterval ${floored}s from eigenflux config (server=${serverName}) is outside [${MIN_POLL_INTERVAL_SEC}s, ${MAX_POLL_INTERVAL_SEC}s]; using ${DEFAULT_POLL_INTERVAL_SEC}s`
|
|
141
|
+
);
|
|
142
|
+
return DEFAULT_POLL_INTERVAL_SEC;
|
|
143
|
+
}
|
|
144
|
+
return floored;
|
|
89
145
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
146
|
+
var EigenFluxPollingClient = class {
|
|
147
|
+
constructor(config) {
|
|
148
|
+
this.timeoutId = null;
|
|
149
|
+
this.isRunning = false;
|
|
150
|
+
this.activePoll = null;
|
|
151
|
+
this.config = config;
|
|
152
|
+
}
|
|
153
|
+
async start() {
|
|
154
|
+
if (this.isRunning) {
|
|
155
|
+
this.config.logger.warn("Polling client already running");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.isRunning = true;
|
|
159
|
+
this.config.logger.info(
|
|
160
|
+
`Starting polling client for server=${this.config.serverName}`
|
|
161
|
+
);
|
|
162
|
+
await this.pollOnce();
|
|
163
|
+
this.scheduleNext();
|
|
164
|
+
}
|
|
165
|
+
stop() {
|
|
166
|
+
if (!this.isRunning) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.config.logger.info(`Stopping polling client for server=${this.config.serverName}`);
|
|
170
|
+
this.isRunning = false;
|
|
171
|
+
if (this.timeoutId) {
|
|
172
|
+
clearTimeout(this.timeoutId);
|
|
173
|
+
this.timeoutId = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async scheduleNext() {
|
|
177
|
+
if (!this.isRunning) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let intervalSec;
|
|
181
|
+
try {
|
|
182
|
+
intervalSec = await this.config.resolvePollIntervalSec();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.config.logger.warn(
|
|
185
|
+
`Failed to resolve pollInterval for server=${this.config.serverName}: ${error instanceof Error ? error.message : String(error)}; using ${DEFAULT_POLL_INTERVAL_SEC}s`
|
|
186
|
+
);
|
|
187
|
+
intervalSec = DEFAULT_POLL_INTERVAL_SEC;
|
|
188
|
+
}
|
|
189
|
+
if (!this.isRunning) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.config.logger.debug(
|
|
193
|
+
`Scheduling next feed poll for server=${this.config.serverName} in ${intervalSec}s`
|
|
194
|
+
);
|
|
195
|
+
this.timeoutId = setTimeout(() => {
|
|
196
|
+
this.timeoutId = null;
|
|
197
|
+
this.pollOnce().catch((err) => {
|
|
198
|
+
this.config.logger.error(
|
|
199
|
+
`Polling error: ${err instanceof Error ? err.message : String(err)}`
|
|
200
|
+
);
|
|
201
|
+
}).finally(() => {
|
|
202
|
+
this.scheduleNext();
|
|
203
|
+
});
|
|
204
|
+
}, intervalSec * 1e3);
|
|
205
|
+
}
|
|
206
|
+
async pollOnce(options = {}) {
|
|
207
|
+
if (this.activePoll) {
|
|
208
|
+
this.config.logger.warn("Skipping feed poll because a previous poll is still in progress");
|
|
209
|
+
return this.activePoll;
|
|
210
|
+
}
|
|
211
|
+
const run = async () => {
|
|
212
|
+
const notifyFeed = options.notifyFeed ?? true;
|
|
213
|
+
const notifyAuthRequired = options.notifyAuthRequired ?? true;
|
|
214
|
+
try {
|
|
215
|
+
this.config.logger.info(`Polling feed via CLI for server=${this.config.serverName}`);
|
|
216
|
+
const result = await execEigenflux(
|
|
217
|
+
this.config.eigenfluxBin,
|
|
218
|
+
["feed", "poll", "--limit", "20", "--action", "refresh", "-s", this.config.serverName, "-f", "json"],
|
|
219
|
+
{ logger: this.config.logger }
|
|
220
|
+
);
|
|
221
|
+
if (result.kind === "auth_required") {
|
|
222
|
+
const authEvent = { reason: "auth_required" };
|
|
223
|
+
if (notifyAuthRequired) {
|
|
224
|
+
await this.config.onAuthRequired(authEvent);
|
|
225
|
+
}
|
|
226
|
+
return { kind: "auth_required", authEvent };
|
|
227
|
+
}
|
|
228
|
+
if (result.kind === "not_installed") {
|
|
229
|
+
return {
|
|
230
|
+
kind: "error",
|
|
231
|
+
error: new Error(`eigenflux CLI not installed (bin=${result.bin})`)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (result.kind === "error") {
|
|
235
|
+
return { kind: "error", error: result.error };
|
|
236
|
+
}
|
|
237
|
+
const feedResponse = {
|
|
238
|
+
code: 0,
|
|
239
|
+
msg: "success",
|
|
240
|
+
data: result.data
|
|
241
|
+
};
|
|
242
|
+
const items = feedResponse.data.items ?? [];
|
|
243
|
+
const notifications = feedResponse.data.notifications ?? [];
|
|
244
|
+
this.config.logger.info(
|
|
245
|
+
`Polled feed: ${items.length} items, notifications=${notifications.length}, has_more=${feedResponse.data.has_more}`
|
|
246
|
+
);
|
|
247
|
+
if (notifyFeed && (items.length > 0 || notifications.length > 0)) {
|
|
248
|
+
await this.config.onFeedPolled(feedResponse);
|
|
249
|
+
}
|
|
250
|
+
return { kind: "success", payload: feedResponse };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
253
|
+
this.config.logger.error(
|
|
254
|
+
`Failed to poll feed for server=${this.config.serverName}: ${normalized.message}`
|
|
255
|
+
);
|
|
256
|
+
return { kind: "error", error: normalized };
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
this.activePoll = run().finally(() => {
|
|
260
|
+
this.activePoll = null;
|
|
261
|
+
});
|
|
262
|
+
return this.activePoll;
|
|
263
|
+
}
|
|
96
264
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
265
|
+
|
|
266
|
+
// src/stream-client.ts
|
|
267
|
+
var import_child_process2 = require("child_process");
|
|
268
|
+
var import_readline = require("readline");
|
|
269
|
+
var EXIT_AUTH_REQUIRED2 = 4;
|
|
270
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
271
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
272
|
+
var BACKOFF_MULTIPLIER = 2;
|
|
273
|
+
var STOP_GRACE_MS = 5e3;
|
|
274
|
+
var MAX_CONSECUTIVE_FAILURES = 20;
|
|
275
|
+
var EigenFluxStreamClient = class {
|
|
276
|
+
constructor(config) {
|
|
277
|
+
this.child = null;
|
|
278
|
+
this.readline = null;
|
|
279
|
+
this.stopping = false;
|
|
280
|
+
this.running = false;
|
|
281
|
+
this.lastCursor = null;
|
|
282
|
+
this.backoffMs = INITIAL_BACKOFF_MS;
|
|
283
|
+
this.consecutiveFailures = 0;
|
|
284
|
+
this.restartTimer = null;
|
|
285
|
+
this.config = config;
|
|
286
|
+
}
|
|
287
|
+
isRunning() {
|
|
288
|
+
return this.running;
|
|
289
|
+
}
|
|
290
|
+
getLastCursor() {
|
|
291
|
+
return this.lastCursor;
|
|
292
|
+
}
|
|
293
|
+
async start() {
|
|
294
|
+
if (this.running) {
|
|
295
|
+
this.config.logger.warn("Stream client already running");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.running = true;
|
|
299
|
+
this.stopping = false;
|
|
300
|
+
this.config.logger.info(`Starting stream client for server=${this.config.serverName}`);
|
|
301
|
+
this.spawnStreamProcess();
|
|
302
|
+
}
|
|
303
|
+
async stop() {
|
|
304
|
+
if (!this.running) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.config.logger.info(`Stopping stream client for server=${this.config.serverName}`);
|
|
308
|
+
this.stopping = true;
|
|
309
|
+
this.running = false;
|
|
310
|
+
if (this.restartTimer) {
|
|
311
|
+
clearTimeout(this.restartTimer);
|
|
312
|
+
this.restartTimer = null;
|
|
313
|
+
}
|
|
314
|
+
if (this.readline) {
|
|
315
|
+
this.readline.close();
|
|
316
|
+
this.readline = null;
|
|
317
|
+
}
|
|
318
|
+
if (this.child) {
|
|
319
|
+
const child = this.child;
|
|
320
|
+
this.child = null;
|
|
321
|
+
child.kill("SIGTERM");
|
|
322
|
+
await new Promise((resolve) => {
|
|
323
|
+
const forceKillTimer = setTimeout(() => {
|
|
324
|
+
try {
|
|
325
|
+
child.kill("SIGKILL");
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
resolve();
|
|
329
|
+
}, STOP_GRACE_MS);
|
|
330
|
+
child.once("exit", () => {
|
|
331
|
+
clearTimeout(forceKillTimer);
|
|
332
|
+
resolve();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
spawnStreamProcess() {
|
|
338
|
+
if (this.stopping || !this.running) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const args = ["stream", "-s", this.config.serverName, "-f", "json"];
|
|
342
|
+
if (this.lastCursor) {
|
|
343
|
+
args.push("--cursor", this.lastCursor);
|
|
344
|
+
}
|
|
345
|
+
this.config.logger.info(
|
|
346
|
+
`Spawning: ${this.config.eigenfluxBin} ${args.join(" ")}`
|
|
347
|
+
);
|
|
348
|
+
const child = (0, import_child_process2.spawn)(this.config.eigenfluxBin, args, {
|
|
349
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
110
350
|
});
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const notifier = new notifier_1.EigenFluxNotifier(api, logger, {
|
|
117
|
-
eigenfluxBin: pluginConfig.eigenfluxBin,
|
|
118
|
-
serverName: server.name,
|
|
119
|
-
sessionKey: routing.sessionKey,
|
|
120
|
-
agentId: routing.agentId,
|
|
121
|
-
replyChannel: routing.replyChannel,
|
|
122
|
-
replyTo: routing.replyTo,
|
|
123
|
-
replyAccountId: routing.replyAccountId,
|
|
124
|
-
openclawCliBin: pluginConfig.openclawCliBin,
|
|
125
|
-
routeOverrides: routing.routeOverrides,
|
|
351
|
+
this.child = child;
|
|
352
|
+
const rl = (0, import_readline.createInterface)({ input: child.stdout });
|
|
353
|
+
this.readline = rl;
|
|
354
|
+
rl.on("line", (line) => {
|
|
355
|
+
this.handleLine(line);
|
|
126
356
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
357
|
+
child.stderr?.on("data", (chunk) => {
|
|
358
|
+
const text = chunk.toString().trim();
|
|
359
|
+
if (text) {
|
|
360
|
+
this.config.logger.debug(`[stream stderr] ${text}`);
|
|
361
|
+
}
|
|
130
362
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const notifyAuthRequired = async (authEvent) => {
|
|
136
|
-
const promptKey = `auth_required:${server.name}`;
|
|
137
|
-
if (lastAuthPromptKey === promptKey) {
|
|
138
|
-
logger.debug(`Skipping duplicate auth prompt for server=${server.name}`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
lastAuthPromptKey = promptKey;
|
|
142
|
-
await notifier.deliver((0, agent_prompt_templates_1.buildAuthRequiredPromptTemplate)({ context: getPromptContext() }));
|
|
143
|
-
};
|
|
144
|
-
const feedPoller = new polling_client_1.EigenFluxPollingClient({
|
|
145
|
-
serverName: server.name,
|
|
146
|
-
eigenfluxBin: pluginConfig.eigenfluxBin,
|
|
147
|
-
resolvePollIntervalSec: () => (0, polling_client_1.readPollIntervalSec)(pluginConfig.eigenfluxBin, server.name, logger),
|
|
148
|
-
logger,
|
|
149
|
-
onFeedPolled: async (payload) => {
|
|
150
|
-
resetAuthPromptGate();
|
|
151
|
-
await notifier.deliver((0, agent_prompt_templates_1.buildFeedPayloadPromptTemplate)(payload, getPromptContext()));
|
|
152
|
-
},
|
|
153
|
-
onAuthRequired: notifyAuthRequired,
|
|
363
|
+
child.on("error", (err) => {
|
|
364
|
+
this.config.logger.error(`Stream process error: ${err.message}`);
|
|
365
|
+
this.config.onStreamError?.(err);
|
|
366
|
+
this.scheduleRestart();
|
|
154
367
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
368
|
+
child.on("exit", (code, signal) => {
|
|
369
|
+
this.config.logger.info(
|
|
370
|
+
`Stream process exited (code=${code}, signal=${signal})`
|
|
371
|
+
);
|
|
372
|
+
if (this.stopping) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (code === EXIT_AUTH_REQUIRED2) {
|
|
376
|
+
this.config.logger.warn("Stream auth required");
|
|
377
|
+
this.config.onAuthRequired().then(() => {
|
|
378
|
+
this.scheduleRestart();
|
|
379
|
+
}).catch((err) => {
|
|
380
|
+
this.config.logger.error(`Auth required handler error: ${err instanceof Error ? err.message : String(err)}`);
|
|
381
|
+
this.scheduleRestart();
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
this.scheduleRestart();
|
|
166
386
|
});
|
|
387
|
+
}
|
|
388
|
+
handleLine(line) {
|
|
389
|
+
const trimmed = line.trim();
|
|
390
|
+
if (!trimmed) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const event = JSON.parse(trimmed);
|
|
395
|
+
if (event.data?.next_cursor) {
|
|
396
|
+
this.lastCursor = event.data.next_cursor;
|
|
397
|
+
}
|
|
398
|
+
this.backoffMs = INITIAL_BACKOFF_MS;
|
|
399
|
+
this.consecutiveFailures = 0;
|
|
400
|
+
this.config.onPmEvent(event).catch((err) => {
|
|
401
|
+
this.config.logger.error(
|
|
402
|
+
`PM event handler error: ${err instanceof Error ? err.message : String(err)}`
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
this.config.logger.warn(
|
|
407
|
+
`Failed to parse stream line: ${err.message}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
scheduleRestart() {
|
|
412
|
+
if (this.stopping || !this.running) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.consecutiveFailures += 1;
|
|
416
|
+
if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
417
|
+
this.config.logger.error(
|
|
418
|
+
`Stream client giving up after ${MAX_CONSECUTIVE_FAILURES} consecutive failures for server=${this.config.serverName}`
|
|
419
|
+
);
|
|
420
|
+
this.running = false;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.config.logger.info(
|
|
424
|
+
`Stream reconnecting in ${this.backoffMs}ms (failure #${this.consecutiveFailures}) for server=${this.config.serverName}`
|
|
425
|
+
);
|
|
426
|
+
this.restartTimer = setTimeout(() => {
|
|
427
|
+
this.restartTimer = null;
|
|
428
|
+
this.spawnStreamProcess();
|
|
429
|
+
}, this.backoffMs);
|
|
430
|
+
this.backoffMs = Math.min(
|
|
431
|
+
this.backoffMs * BACKOFF_MULTIPLIER,
|
|
432
|
+
MAX_BACKOFF_MS
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/logger.ts
|
|
438
|
+
var Logger = class {
|
|
439
|
+
constructor(baseLogger) {
|
|
440
|
+
this.baseLogger = baseLogger;
|
|
441
|
+
}
|
|
442
|
+
info(message, ...args) {
|
|
443
|
+
const formatted = args.length ? `[EigenFlux] ${message} ${args.map(String).join(" ")}` : `[EigenFlux] ${message}`;
|
|
444
|
+
this.baseLogger.info(formatted);
|
|
445
|
+
}
|
|
446
|
+
warn(message, ...args) {
|
|
447
|
+
const formatted = args.length ? `[EigenFlux] ${message} ${args.map(String).join(" ")}` : `[EigenFlux] ${message}`;
|
|
448
|
+
this.baseLogger.warn(formatted);
|
|
449
|
+
}
|
|
450
|
+
error(message, ...args) {
|
|
451
|
+
const formatted = args.length ? `[EigenFlux] ${message} ${args.map(String).join(" ")}` : `[EigenFlux] ${message}`;
|
|
452
|
+
this.baseLogger.error(formatted);
|
|
453
|
+
}
|
|
454
|
+
debug(message, ...args) {
|
|
455
|
+
this.baseLogger.debug?.(`[EigenFlux] ${message}`, ...args);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/credentials-loader.ts
|
|
460
|
+
var fs = __toESM(require("fs"));
|
|
461
|
+
var path = __toESM(require("path"));
|
|
462
|
+
var CredentialsLoader = class {
|
|
463
|
+
constructor(logger, eigenfluxHome, serverName) {
|
|
464
|
+
this.logger = logger;
|
|
465
|
+
this.credentialsDir = path.join(eigenfluxHome, "servers", serverName);
|
|
466
|
+
this.credentialsPath = path.join(this.credentialsDir, "credentials.json");
|
|
467
|
+
}
|
|
468
|
+
loadAccessToken() {
|
|
469
|
+
const authState = this.loadAuthState();
|
|
470
|
+
if (authState.status !== "available") {
|
|
471
|
+
if (authState.status === "missing") {
|
|
472
|
+
this.logger.error(`No access token found in ${authState.credentialsPath}`);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
return authState.accessToken;
|
|
477
|
+
}
|
|
478
|
+
loadAuthState() {
|
|
479
|
+
if (fs.existsSync(this.credentialsPath)) {
|
|
480
|
+
try {
|
|
481
|
+
const content = fs.readFileSync(this.credentialsPath, "utf-8");
|
|
482
|
+
const credentials = JSON.parse(content);
|
|
483
|
+
if (credentials.access_token) {
|
|
484
|
+
if (credentials.expires_at) {
|
|
485
|
+
const now = Date.now();
|
|
486
|
+
if (now >= credentials.expires_at) {
|
|
487
|
+
this.logger.warn("Access token has expired");
|
|
488
|
+
return {
|
|
489
|
+
status: "expired",
|
|
490
|
+
credentialsPath: this.credentialsPath,
|
|
491
|
+
expiresAt: credentials.expires_at,
|
|
492
|
+
email: credentials.email
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
this.logger.info(`Loaded access token from ${this.credentialsPath}`);
|
|
497
|
+
return {
|
|
498
|
+
status: "available",
|
|
499
|
+
accessToken: credentials.access_token,
|
|
500
|
+
credentialsPath: this.credentialsPath,
|
|
501
|
+
expiresAt: credentials.expires_at,
|
|
502
|
+
email: credentials.email
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
this.logger.error(`Failed to read credentials file: ${this.credentialsPath}`, error);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
167
509
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
credentialsLoader,
|
|
171
|
-
notifier,
|
|
172
|
-
feedPoller,
|
|
173
|
-
streamClient,
|
|
174
|
-
getPromptContext,
|
|
510
|
+
status: "missing",
|
|
511
|
+
credentialsPath: this.credentialsPath
|
|
175
512
|
};
|
|
513
|
+
}
|
|
514
|
+
saveAccessToken(token, email, expiresAt) {
|
|
515
|
+
if (!fs.existsSync(this.credentialsDir)) {
|
|
516
|
+
fs.mkdirSync(this.credentialsDir, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
const credentials = {
|
|
519
|
+
access_token: token,
|
|
520
|
+
email,
|
|
521
|
+
expires_at: expiresAt
|
|
522
|
+
};
|
|
523
|
+
try {
|
|
524
|
+
fs.writeFileSync(this.credentialsPath, JSON.stringify(credentials, null, 2), "utf-8");
|
|
525
|
+
this.logger.info(`Saved access token to ${this.credentialsPath}`);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
this.logger.error("Failed to save credentials file", error);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// src/config.ts
|
|
533
|
+
var os = __toESM(require("os"));
|
|
534
|
+
var path2 = __toESM(require("path"));
|
|
535
|
+
|
|
536
|
+
// src/reply-target.ts
|
|
537
|
+
function readNonEmptyString(value) {
|
|
538
|
+
if (typeof value !== "string") {
|
|
539
|
+
return void 0;
|
|
540
|
+
}
|
|
541
|
+
const trimmed = value.trim();
|
|
542
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
176
543
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
544
|
+
function isNormalizedConversationTarget(value) {
|
|
545
|
+
return /^(user|chat|channel|room):/u.test(value);
|
|
546
|
+
}
|
|
547
|
+
function isSessionPeerShape(value) {
|
|
548
|
+
const normalized = value?.trim().toLowerCase();
|
|
549
|
+
return normalized === "direct" || normalized === "dm" || normalized === "group" || normalized === "channel" || normalized === "room";
|
|
550
|
+
}
|
|
551
|
+
function supportsKindPrefixedTargets(channel) {
|
|
552
|
+
return channel === "feishu" || channel === "discord";
|
|
553
|
+
}
|
|
554
|
+
function parseSessionRoute(sessionKey) {
|
|
555
|
+
const trimmed = readNonEmptyString(sessionKey);
|
|
556
|
+
if (!trimmed) {
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
const parts = trimmed.split(":").filter((part) => part.length > 0);
|
|
560
|
+
if (parts[0]?.toLowerCase() !== "agent") {
|
|
561
|
+
return {};
|
|
562
|
+
}
|
|
563
|
+
const channel = readNonEmptyString(parts[2])?.toLowerCase();
|
|
564
|
+
const peerShapeRaw = parts.length >= 6 && isSessionPeerShape(parts[4]) ? parts[4].toLowerCase() : parts.length >= 5 && isSessionPeerShape(parts[3]) ? parts[3].toLowerCase() : void 0;
|
|
565
|
+
const peerShape = peerShapeRaw;
|
|
566
|
+
return { channel, peerShape };
|
|
567
|
+
}
|
|
568
|
+
function deriveReplyTargetKindFromSessionKey(sessionKey) {
|
|
569
|
+
const route = parseSessionRoute(sessionKey);
|
|
570
|
+
if (!supportsKindPrefixedTargets(route.channel)) {
|
|
571
|
+
return void 0;
|
|
572
|
+
}
|
|
573
|
+
switch (route.channel) {
|
|
574
|
+
case "feishu":
|
|
575
|
+
switch (route.peerShape) {
|
|
576
|
+
case "direct":
|
|
577
|
+
case "dm":
|
|
578
|
+
return "user";
|
|
579
|
+
case "group":
|
|
580
|
+
return "chat";
|
|
581
|
+
default:
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
584
|
+
case "discord":
|
|
585
|
+
switch (route.peerShape) {
|
|
586
|
+
case "direct":
|
|
587
|
+
case "dm":
|
|
588
|
+
return "user";
|
|
589
|
+
case "channel":
|
|
590
|
+
return "channel";
|
|
591
|
+
default:
|
|
592
|
+
return void 0;
|
|
593
|
+
}
|
|
594
|
+
default:
|
|
595
|
+
return void 0;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function deriveReplyTargetKindFromValue(value, channel) {
|
|
599
|
+
if (channel === "feishu") {
|
|
600
|
+
if (/^ou_/iu.test(value)) {
|
|
601
|
+
return "user";
|
|
182
602
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
603
|
+
if (/^oc_/iu.test(value)) {
|
|
604
|
+
return "chat";
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
function normalizeReplyTarget(value, options) {
|
|
610
|
+
const trimmed = readNonEmptyString(value);
|
|
611
|
+
if (!trimmed) {
|
|
612
|
+
return void 0;
|
|
613
|
+
}
|
|
614
|
+
if (isNormalizedConversationTarget(trimmed)) {
|
|
615
|
+
return trimmed;
|
|
616
|
+
}
|
|
617
|
+
const channel = readNonEmptyString(options?.channel)?.toLowerCase();
|
|
618
|
+
if (channel && trimmed.startsWith(`${channel}:`)) {
|
|
619
|
+
return normalizeReplyTarget(trimmed.slice(channel.length + 1), {
|
|
620
|
+
...options,
|
|
621
|
+
channel
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const fallbackKind = deriveReplyTargetKindFromValue(trimmed, channel) ?? (supportsKindPrefixedTargets(channel) ? options?.fallbackKind : void 0) ?? deriveReplyTargetKindFromSessionKey(options?.sessionKey);
|
|
625
|
+
return fallbackKind ? `${fallbackKind}:${trimmed}` : trimmed;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/config.ts
|
|
629
|
+
var PLUGIN_VERSION = "0.0.9-beta.0";
|
|
630
|
+
var DEFAULT_EIGENFLUX_BIN = "eigenflux";
|
|
631
|
+
var DEFAULT_SESSION_KEY = "main";
|
|
632
|
+
var DEFAULT_AGENT_ID = "main";
|
|
633
|
+
var DEFAULT_OPENCLAW_CLI_BIN = "openclaw";
|
|
634
|
+
var HOST_KIND = "openclaw";
|
|
635
|
+
function isRecord(value) {
|
|
636
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
637
|
+
}
|
|
638
|
+
function readNonEmptyString2(value) {
|
|
639
|
+
if (typeof value !== "string") {
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
const trimmed = value.trim();
|
|
643
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
644
|
+
}
|
|
645
|
+
function isSessionPeerShape2(value) {
|
|
646
|
+
const normalized = value?.trim().toLowerCase();
|
|
647
|
+
return normalized === "direct" || normalized === "dm" || normalized === "group" || normalized === "channel";
|
|
648
|
+
}
|
|
649
|
+
function deriveNotificationRoute(sessionKey) {
|
|
650
|
+
const trimmed = readNonEmptyString2(sessionKey);
|
|
651
|
+
if (!trimmed) {
|
|
652
|
+
return {};
|
|
653
|
+
}
|
|
654
|
+
const parts = trimmed.split(":").filter((part) => part.length > 0);
|
|
655
|
+
if (parts.length < 3 || parts[0]?.toLowerCase() !== "agent") {
|
|
656
|
+
return {};
|
|
657
|
+
}
|
|
658
|
+
const agentId = readNonEmptyString2(parts[1]);
|
|
659
|
+
if (parts.length >= 6 && isSessionPeerShape2(parts[4])) {
|
|
660
|
+
return {
|
|
661
|
+
agentId,
|
|
662
|
+
replyChannel: readNonEmptyString2(parts[2]),
|
|
663
|
+
replyAccountId: readNonEmptyString2(parts[3]),
|
|
664
|
+
replyTo: normalizeReplyTarget(parts.slice(5).join(":"), {
|
|
665
|
+
channel: readNonEmptyString2(parts[2]),
|
|
666
|
+
sessionKey: trimmed
|
|
667
|
+
})
|
|
195
668
|
};
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
return inflightDiscovery;
|
|
669
|
+
}
|
|
670
|
+
if (parts.length >= 5 && isSessionPeerShape2(parts[3])) {
|
|
671
|
+
return {
|
|
672
|
+
agentId,
|
|
673
|
+
replyChannel: readNonEmptyString2(parts[2]),
|
|
674
|
+
replyTo: normalizeReplyTarget(parts.slice(4).join(":"), {
|
|
675
|
+
channel: readNonEmptyString2(parts[2]),
|
|
676
|
+
sessionKey: trimmed
|
|
677
|
+
})
|
|
207
678
|
};
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
description: 'EigenFlux plugin commands: auth, profile, servers, feed, pm, here, version',
|
|
211
|
-
acceptsArgs: true,
|
|
212
|
-
handler: async (ctx) => {
|
|
213
|
-
const parsed = parseCommandArgs(ctx.args);
|
|
214
|
-
if (parsed.command === 'version') {
|
|
215
|
-
return {
|
|
216
|
-
text: await buildVersionText(pluginConfig.eigenfluxBin),
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
const { runtimes, notInstalledBin } = await ensureRuntimes();
|
|
220
|
-
if (notInstalledBin && runtimes.length === 0) {
|
|
221
|
-
return {
|
|
222
|
-
text: `EigenFlux CLI not installed (bin=${notInstalledBin}). Install with: ${INSTALL_COMMAND}`,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
if (parsed.command === 'servers') {
|
|
226
|
-
return {
|
|
227
|
-
text: buildServersText(runtimes),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
const selection = selectServerRuntime(runtimes, parsed.serverName);
|
|
231
|
-
if (!selection.runtime) {
|
|
232
|
-
return {
|
|
233
|
-
text: selection.error ?? buildHelpText(runtimes),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const runtime = selection.runtime;
|
|
237
|
-
await rememberCurrentCommandRouteIfPossible(ctx, runtime, pluginConfig.eigenfluxBin, logger);
|
|
238
|
-
switch (parsed.command) {
|
|
239
|
-
case 'auth':
|
|
240
|
-
return {
|
|
241
|
-
text: buildAuthStatusText(runtime),
|
|
242
|
-
};
|
|
243
|
-
case 'profile':
|
|
244
|
-
return {
|
|
245
|
-
text: await buildProfileText(runtime, pluginConfig.eigenfluxBin),
|
|
246
|
-
};
|
|
247
|
-
case 'feed':
|
|
248
|
-
return {
|
|
249
|
-
text: await buildFeedText(runtime),
|
|
250
|
-
};
|
|
251
|
-
case 'pm':
|
|
252
|
-
return {
|
|
253
|
-
text: buildPmStatusText(runtime),
|
|
254
|
-
};
|
|
255
|
-
case 'here':
|
|
256
|
-
return {
|
|
257
|
-
text: await buildHereText(ctx, runtime, pluginConfig.eigenfluxBin, logger),
|
|
258
|
-
};
|
|
259
|
-
default:
|
|
260
|
-
return {
|
|
261
|
-
text: buildHelpText(runtimes),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
});
|
|
679
|
+
}
|
|
680
|
+
return { agentId };
|
|
266
681
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
682
|
+
function createRouteOverrides(normalized) {
|
|
683
|
+
const sessionKey = readNonEmptyString2(normalized.sessionKey);
|
|
684
|
+
const agentId = readNonEmptyString2(normalized.agentId);
|
|
685
|
+
const replyChannel = readNonEmptyString2(normalized.replyChannel);
|
|
686
|
+
const replyTo = readNonEmptyString2(normalized.replyTo);
|
|
687
|
+
const replyAccountId = readNonEmptyString2(normalized.replyAccountId);
|
|
688
|
+
return {
|
|
689
|
+
sessionKey: sessionKey !== void 0 && sessionKey !== DEFAULT_SESSION_KEY,
|
|
690
|
+
agentId: agentId !== void 0 && agentId !== DEFAULT_AGENT_ID && !(sessionKey && deriveNotificationRoute(sessionKey).agentId === agentId),
|
|
691
|
+
replyChannel: replyChannel !== void 0,
|
|
692
|
+
replyTo: replyTo !== void 0,
|
|
693
|
+
replyAccountId: replyAccountId !== void 0
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function discoverServers(eigenfluxBin, logger) {
|
|
697
|
+
const result = await execEigenflux(
|
|
698
|
+
eigenfluxBin,
|
|
699
|
+
["server", "list", "--format", "json"],
|
|
700
|
+
{ logger }
|
|
701
|
+
);
|
|
702
|
+
if (result.kind === "success") {
|
|
703
|
+
if (Array.isArray(result.data)) {
|
|
704
|
+
return { kind: "ok", servers: result.data };
|
|
705
|
+
}
|
|
706
|
+
logger?.warn("eigenflux server list returned non-array data");
|
|
707
|
+
return { kind: "ok", servers: [] };
|
|
708
|
+
}
|
|
709
|
+
if (result.kind === "not_installed") {
|
|
710
|
+
return { kind: "not_installed", bin: result.bin };
|
|
711
|
+
}
|
|
712
|
+
if (result.kind === "auth_required") {
|
|
713
|
+
logger?.warn("eigenflux server list: auth required (unexpected)");
|
|
714
|
+
return { kind: "ok", servers: [] };
|
|
715
|
+
}
|
|
716
|
+
logger?.error(`eigenflux server list failed: ${result.error.message}`);
|
|
717
|
+
return { kind: "ok", servers: [] };
|
|
718
|
+
}
|
|
719
|
+
function resolveEigenfluxHome() {
|
|
720
|
+
const envHome = process.env.EIGENFLUX_HOME;
|
|
721
|
+
if (envHome) {
|
|
722
|
+
const expanded = expandHomeDir(envHome);
|
|
723
|
+
if (!expanded.endsWith(".eigenflux")) {
|
|
724
|
+
return path2.join(expanded, ".eigenflux");
|
|
725
|
+
}
|
|
726
|
+
return expanded;
|
|
727
|
+
}
|
|
728
|
+
return path2.join(os.homedir(), ".eigenflux");
|
|
729
|
+
}
|
|
730
|
+
function resolveRoutingConfig(raw, logger) {
|
|
731
|
+
const normalized = isRecord(raw) ? raw : {};
|
|
732
|
+
const sessionKey = readNonEmptyString2(normalized.sessionKey) ?? DEFAULT_SESSION_KEY;
|
|
733
|
+
const derivedRoute = deriveNotificationRoute(sessionKey);
|
|
734
|
+
const replyChannel = readNonEmptyString2(normalized.replyChannel) ?? derivedRoute.replyChannel;
|
|
735
|
+
const replyTo = normalizeReplyTarget(readNonEmptyString2(normalized.replyTo), {
|
|
736
|
+
channel: replyChannel,
|
|
737
|
+
sessionKey
|
|
738
|
+
}) ?? derivedRoute.replyTo;
|
|
739
|
+
return {
|
|
740
|
+
sessionKey,
|
|
741
|
+
agentId: readNonEmptyString2(normalized.agentId) ?? derivedRoute.agentId ?? DEFAULT_AGENT_ID,
|
|
742
|
+
replyChannel,
|
|
743
|
+
replyTo,
|
|
744
|
+
replyAccountId: readNonEmptyString2(normalized.replyAccountId) ?? derivedRoute.replyAccountId,
|
|
745
|
+
routeOverrides: createRouteOverrides(normalized)
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function resolvePluginConfig(pluginConfig, logger) {
|
|
749
|
+
const normalized = isRecord(pluginConfig) ? pluginConfig : {};
|
|
750
|
+
const rawRouting = isRecord(normalized.serverRouting) ? normalized.serverRouting : {};
|
|
751
|
+
const serverRouting = {};
|
|
752
|
+
for (const [serverName, rawConfig] of Object.entries(rawRouting)) {
|
|
753
|
+
serverRouting[serverName] = resolveRoutingConfig(
|
|
754
|
+
isRecord(rawConfig) ? rawConfig : void 0,
|
|
755
|
+
logger
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const rawSkills = Array.isArray(normalized.skills) ? normalized.skills.filter((s) => typeof s === "string" && s.trim().length > 0) : ["ef-broadcast", "ef-communication"];
|
|
759
|
+
return {
|
|
760
|
+
eigenfluxBin: readNonEmptyString2(normalized.eigenfluxBin) ?? DEFAULT_EIGENFLUX_BIN,
|
|
761
|
+
skills: rawSkills,
|
|
762
|
+
openclawCliBin: readNonEmptyString2(normalized.openclawCliBin) ?? DEFAULT_OPENCLAW_CLI_BIN,
|
|
763
|
+
serverRouting
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function expandHomeDir(input) {
|
|
767
|
+
if (input === "~") {
|
|
768
|
+
return os.homedir();
|
|
769
|
+
}
|
|
770
|
+
if (input.startsWith("~/")) {
|
|
771
|
+
return path2.join(os.homedir(), input.slice(2));
|
|
772
|
+
}
|
|
773
|
+
return input;
|
|
774
|
+
}
|
|
775
|
+
var PLUGIN_CONFIG = {
|
|
776
|
+
DEFAULT_EIGENFLUX_BIN,
|
|
777
|
+
DEFAULT_SESSION_KEY,
|
|
778
|
+
DEFAULT_AGENT_ID,
|
|
779
|
+
DEFAULT_OPENCLAW_CLI_BIN,
|
|
780
|
+
HOST_KIND,
|
|
781
|
+
PLUGIN_VERSION
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/notification-route-resolver.ts
|
|
785
|
+
var fs2 = __toESM(require("fs"));
|
|
786
|
+
var os2 = __toESM(require("os"));
|
|
787
|
+
var path3 = __toESM(require("path"));
|
|
788
|
+
|
|
789
|
+
// src/session-route-memory.ts
|
|
790
|
+
var DELIVER_SESSION_KEY_PREFIX = "deliver_session";
|
|
791
|
+
function readNonEmptyString3(value) {
|
|
792
|
+
if (typeof value !== "string") {
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
const trimmed = value.trim();
|
|
796
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
797
|
+
}
|
|
798
|
+
function normalizeChannel(value) {
|
|
799
|
+
return readNonEmptyString3(value)?.toLowerCase();
|
|
800
|
+
}
|
|
801
|
+
function storeKey(serverName) {
|
|
802
|
+
return `${DELIVER_SESSION_KEY_PREFIX}:${serverName}`;
|
|
803
|
+
}
|
|
804
|
+
async function readStoredNotificationRoute(store, serverName, logger) {
|
|
805
|
+
const server = readNonEmptyString3(serverName);
|
|
806
|
+
if (!store || !server) {
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
809
|
+
let parsed;
|
|
810
|
+
try {
|
|
811
|
+
parsed = await store.get(storeKey(server));
|
|
812
|
+
} catch (error) {
|
|
813
|
+
logger.debug(
|
|
814
|
+
`readStoredNotificationRoute: store.get failed for server=${server}: ${error instanceof Error ? error.message : String(error)}`
|
|
815
|
+
);
|
|
816
|
+
return void 0;
|
|
817
|
+
}
|
|
818
|
+
if (parsed === void 0 || parsed === null) {
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
822
|
+
return void 0;
|
|
823
|
+
}
|
|
824
|
+
const record = parsed;
|
|
825
|
+
const sessionKey = readNonEmptyString3(record.sessionKey);
|
|
826
|
+
const agentId = readNonEmptyString3(record.agentId);
|
|
827
|
+
if (!sessionKey || !agentId) {
|
|
828
|
+
logger.warn(
|
|
829
|
+
`Remembered route entry for server=${server} is incomplete (sessionKey/agentId missing)`
|
|
830
|
+
);
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
const route = {
|
|
834
|
+
sessionKey,
|
|
835
|
+
agentId,
|
|
836
|
+
replyChannel: normalizeChannel(record.replyChannel),
|
|
837
|
+
replyTo: normalizeReplyTarget(readNonEmptyString3(record.replyTo), {
|
|
838
|
+
channel: normalizeChannel(record.replyChannel),
|
|
839
|
+
sessionKey
|
|
840
|
+
}),
|
|
841
|
+
replyAccountId: readNonEmptyString3(record.replyAccountId),
|
|
842
|
+
updatedAt: typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt) ? record.updatedAt : 0
|
|
843
|
+
};
|
|
844
|
+
logger.info(
|
|
845
|
+
`Remembered route loaded: server=${server}, session_key=${route.sessionKey}, agent_id=${route.agentId}, channel=${route.replyChannel ?? "n/a"}, to=${route.replyTo ?? "n/a"}, account=${route.replyAccountId ?? "n/a"}`
|
|
846
|
+
);
|
|
847
|
+
return route;
|
|
848
|
+
}
|
|
849
|
+
async function writeStoredNotificationRoute(store, serverName, route, logger) {
|
|
850
|
+
const server = readNonEmptyString3(serverName);
|
|
851
|
+
if (!store || !server) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
const normalized = {
|
|
855
|
+
sessionKey: route.sessionKey,
|
|
856
|
+
agentId: route.agentId,
|
|
857
|
+
replyChannel: normalizeChannel(route.replyChannel),
|
|
858
|
+
replyTo: normalizeReplyTarget(readNonEmptyString3(route.replyTo), {
|
|
859
|
+
channel: normalizeChannel(route.replyChannel),
|
|
860
|
+
sessionKey: route.sessionKey
|
|
861
|
+
}),
|
|
862
|
+
replyAccountId: readNonEmptyString3(route.replyAccountId)
|
|
863
|
+
};
|
|
864
|
+
const existing = await readStoredNotificationRoute(store, server, logger);
|
|
865
|
+
if (existing && existing.sessionKey === normalized.sessionKey && existing.agentId === normalized.agentId && existing.replyChannel === normalized.replyChannel && existing.replyTo === normalized.replyTo && existing.replyAccountId === normalized.replyAccountId) {
|
|
866
|
+
logger.debug(
|
|
867
|
+
`Remembered route unchanged for server=${server} (session_key=${normalized.sessionKey}); skipping write`
|
|
868
|
+
);
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
const payload = {
|
|
872
|
+
...normalized,
|
|
873
|
+
updatedAt: Date.now()
|
|
874
|
+
};
|
|
875
|
+
try {
|
|
876
|
+
await store.set(storeKey(server), payload);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
logger.warn(
|
|
879
|
+
`Failed to persist remembered session route via store.set (server=${server}): ${error instanceof Error ? error.message : String(error)}`
|
|
880
|
+
);
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
logger.info(
|
|
884
|
+
`Remembered route saved: server=${server}, session_key=${payload.sessionKey}, agent_id=${payload.agentId}, channel=${payload.replyChannel ?? "n/a"}, to=${payload.replyTo ?? "n/a"}, account=${payload.replyAccountId ?? "n/a"}`
|
|
885
|
+
);
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/notification-route-resolver.ts
|
|
890
|
+
var INTERNAL_CHANNELS = /* @__PURE__ */ new Set(["webchat"]);
|
|
891
|
+
function getDefaultOpenClawStateDir() {
|
|
892
|
+
return path3.join(os2.homedir(), ".openclaw");
|
|
893
|
+
}
|
|
894
|
+
function readNonEmptyString4(value) {
|
|
895
|
+
if (typeof value !== "string") {
|
|
896
|
+
return void 0;
|
|
897
|
+
}
|
|
898
|
+
const trimmed = value.trim();
|
|
899
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
900
|
+
}
|
|
901
|
+
function normalizeChannel2(value) {
|
|
902
|
+
return readNonEmptyString4(value)?.toLowerCase();
|
|
903
|
+
}
|
|
904
|
+
function normalizeUpdatedAt(value) {
|
|
905
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
906
|
+
return value;
|
|
907
|
+
}
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
function createRouteOverrides2(overrides) {
|
|
911
|
+
return {
|
|
912
|
+
sessionKey: overrides?.sessionKey === true,
|
|
913
|
+
agentId: overrides?.agentId === true,
|
|
914
|
+
replyChannel: overrides?.replyChannel === true,
|
|
915
|
+
replyTo: overrides?.replyTo === true,
|
|
916
|
+
replyAccountId: overrides?.replyAccountId === true
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function isAnyRouteOverrideEnabled(overrides) {
|
|
920
|
+
return Object.values(overrides).some(Boolean);
|
|
921
|
+
}
|
|
922
|
+
function isInternalSessionKey(sessionKey) {
|
|
923
|
+
const trimmed = readNonEmptyString4(sessionKey);
|
|
924
|
+
if (!trimmed) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
const lower = trimmed.toLowerCase();
|
|
928
|
+
if (lower === "main" || lower === "heartbeat") {
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
const parts = lower.split(":").filter((part) => part.length > 0);
|
|
932
|
+
return parts[0] === "agent" && parts[2] === "heartbeat";
|
|
933
|
+
}
|
|
934
|
+
function isExternalChannel(channel) {
|
|
935
|
+
return Boolean(channel && !INTERNAL_CHANNELS.has(channel));
|
|
936
|
+
}
|
|
937
|
+
function isDirectSessionKey(sessionKey, entry) {
|
|
938
|
+
const parts = sessionKey.toLowerCase().split(":").filter(Boolean);
|
|
939
|
+
if (parts.includes("direct") || parts.includes("dm")) {
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
const chatType = readNonEmptyString4(entry.chatType)?.toLowerCase() ?? readNonEmptyString4(entry.origin?.chatType)?.toLowerCase();
|
|
943
|
+
if (chatType === "direct" || chatType === "dm") {
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
const toCandidates = [entry.deliveryContext?.to, entry.lastTo, entry.origin?.to];
|
|
947
|
+
return toCandidates.some((candidate) => {
|
|
948
|
+
const trimmed = readNonEmptyString4(candidate);
|
|
949
|
+
if (!trimmed) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
const colonAt = trimmed.indexOf(":");
|
|
953
|
+
if (colonAt <= 0) {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
return trimmed.slice(0, colonAt).toLowerCase() === "user";
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
var GROUP_PEER_SHAPES = /* @__PURE__ */ new Set(["group", "channel", "room"]);
|
|
960
|
+
var GROUP_TARGET_PREFIXES = /* @__PURE__ */ new Set(["chat", "channel", "room"]);
|
|
961
|
+
function readChatTypeSignal(value) {
|
|
962
|
+
const normalized = readNonEmptyString4(value)?.toLowerCase();
|
|
963
|
+
return normalized && GROUP_PEER_SHAPES.has(normalized) ? normalized : void 0;
|
|
964
|
+
}
|
|
965
|
+
function readTargetPrefixSignal(value) {
|
|
966
|
+
const trimmed = readNonEmptyString4(value);
|
|
967
|
+
if (!trimmed) {
|
|
968
|
+
return void 0;
|
|
969
|
+
}
|
|
970
|
+
const colonAt = trimmed.indexOf(":");
|
|
971
|
+
if (colonAt <= 0) {
|
|
972
|
+
return void 0;
|
|
973
|
+
}
|
|
974
|
+
const prefix = trimmed.slice(0, colonAt).toLowerCase();
|
|
975
|
+
return GROUP_TARGET_PREFIXES.has(prefix) ? prefix : void 0;
|
|
976
|
+
}
|
|
977
|
+
function isGroupEntry(sessionKey, entry) {
|
|
978
|
+
const parts = sessionKey.toLowerCase().split(":").filter(Boolean);
|
|
979
|
+
if (parts.some((part) => GROUP_PEER_SHAPES.has(part))) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
if (readChatTypeSignal(entry.chatType) || readChatTypeSignal(entry.origin?.chatType)) {
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
const toCandidates = [entry.deliveryContext?.to, entry.lastTo, entry.origin?.to];
|
|
986
|
+
if (toCandidates.some((candidate) => readTargetPrefixSignal(candidate))) {
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
function isSessionPeerShape3(value) {
|
|
992
|
+
const normalized = value?.trim().toLowerCase();
|
|
993
|
+
return normalized === "direct" || normalized === "dm" || normalized === "group" || normalized === "channel" || normalized === "room";
|
|
994
|
+
}
|
|
995
|
+
function routeTargetMatches(actual, expected) {
|
|
996
|
+
if (!expected) {
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
if (!actual) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
return actual === expected || actual.endsWith(`:${expected}`) || expected.endsWith(`:${actual}`);
|
|
1003
|
+
}
|
|
1004
|
+
function routeMatchesPreferred(route, preferred) {
|
|
1005
|
+
if (!preferred) {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
if (preferred.channel && route.replyChannel !== preferred.channel) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
if (!routeTargetMatches(route.replyTo, preferred.to)) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
if (preferred.accountId && route.replyAccountId !== preferred.accountId) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
function deriveAgentIdFromSessionKey(sessionKey) {
|
|
1020
|
+
const trimmed = readNonEmptyString4(sessionKey);
|
|
1021
|
+
if (!trimmed) {
|
|
1022
|
+
return void 0;
|
|
1023
|
+
}
|
|
1024
|
+
const parts = trimmed.split(":").filter((part) => part.length > 0);
|
|
1025
|
+
if (parts[0]?.toLowerCase() !== "agent") {
|
|
1026
|
+
return void 0;
|
|
1027
|
+
}
|
|
1028
|
+
return readNonEmptyString4(parts[1]);
|
|
1029
|
+
}
|
|
1030
|
+
function deriveReplyTargetKindFromSessionKey2(sessionKey) {
|
|
1031
|
+
const trimmed = readNonEmptyString4(sessionKey);
|
|
1032
|
+
if (!trimmed) {
|
|
1033
|
+
return void 0;
|
|
1034
|
+
}
|
|
1035
|
+
const parts = trimmed.split(":").filter((part) => part.length > 0);
|
|
1036
|
+
if (parts[0]?.toLowerCase() !== "agent") {
|
|
1037
|
+
return void 0;
|
|
1038
|
+
}
|
|
1039
|
+
const channel = readNonEmptyString4(parts[2])?.toLowerCase();
|
|
1040
|
+
const peerShape = parts.length >= 6 && isSessionPeerShape3(parts[4]) ? parts[4].toLowerCase() : parts.length >= 5 && isSessionPeerShape3(parts[3]) ? parts[3].toLowerCase() : void 0;
|
|
1041
|
+
switch (channel) {
|
|
1042
|
+
case "feishu":
|
|
1043
|
+
if (peerShape === "direct" || peerShape === "dm") {
|
|
1044
|
+
return "user";
|
|
1045
|
+
}
|
|
1046
|
+
if (peerShape === "group") {
|
|
1047
|
+
return "chat";
|
|
1048
|
+
}
|
|
1049
|
+
return void 0;
|
|
1050
|
+
case "discord":
|
|
1051
|
+
if (peerShape === "direct" || peerShape === "dm") {
|
|
1052
|
+
return "user";
|
|
1053
|
+
}
|
|
1054
|
+
if (peerShape === "channel") {
|
|
1055
|
+
return "channel";
|
|
1056
|
+
}
|
|
1057
|
+
return void 0;
|
|
1058
|
+
default:
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function normalizeSessionStoreTarget(value, channel, sessionKey) {
|
|
1063
|
+
const trimmed = readNonEmptyString4(value);
|
|
1064
|
+
if (!trimmed) {
|
|
1065
|
+
return void 0;
|
|
1066
|
+
}
|
|
1067
|
+
const derivedKind = deriveReplyTargetKindFromSessionKey2(sessionKey);
|
|
1068
|
+
if (derivedKind && !/^(user|chat|channel|room):/u.test(trimmed)) {
|
|
1069
|
+
return `${derivedKind}:${trimmed}`;
|
|
1070
|
+
}
|
|
1071
|
+
return normalizeReplyTarget(trimmed, {
|
|
1072
|
+
channel,
|
|
1073
|
+
sessionKey
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function deriveChannelFromSessionKey(sessionKey) {
|
|
1077
|
+
const parts = sessionKey.split(":").filter(Boolean);
|
|
1078
|
+
if (parts[0]?.toLowerCase() !== "agent") {
|
|
1079
|
+
return void 0;
|
|
1080
|
+
}
|
|
1081
|
+
return normalizeChannel2(parts[2]);
|
|
1082
|
+
}
|
|
1083
|
+
function deriveTargetFromSessionKey(sessionKey, channel) {
|
|
1084
|
+
const parts = sessionKey.split(":").filter(Boolean);
|
|
1085
|
+
if (parts[0]?.toLowerCase() !== "agent") {
|
|
1086
|
+
return void 0;
|
|
1087
|
+
}
|
|
1088
|
+
if (parts.length < 5 || !isSessionPeerShape3(parts[3])) {
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
const rawTarget = parts.length >= 6 ? parts[5] : parts[4];
|
|
1092
|
+
if (!readNonEmptyString4(rawTarget)) {
|
|
1093
|
+
return void 0;
|
|
1094
|
+
}
|
|
1095
|
+
return normalizeSessionStoreTarget(rawTarget, channel, sessionKey);
|
|
1096
|
+
}
|
|
1097
|
+
function extractRouteFromEntry(sessionKey, entry) {
|
|
1098
|
+
if (!entry) {
|
|
1099
|
+
return void 0;
|
|
1100
|
+
}
|
|
1101
|
+
const replyChannel = normalizeChannel2(entry.deliveryContext?.channel) ?? normalizeChannel2(entry.origin?.provider) ?? deriveChannelFromSessionKey(sessionKey);
|
|
1102
|
+
const replyTo = normalizeSessionStoreTarget(entry.deliveryContext?.to, replyChannel, sessionKey) ?? normalizeSessionStoreTarget(entry.lastTo, replyChannel, sessionKey) ?? normalizeSessionStoreTarget(entry.origin?.to, replyChannel, sessionKey) ?? deriveTargetFromSessionKey(sessionKey, replyChannel);
|
|
1103
|
+
const replyAccountId = readNonEmptyString4(entry.deliveryContext?.accountId) ?? readNonEmptyString4(entry.lastAccountId) ?? readNonEmptyString4(entry.origin?.accountId);
|
|
1104
|
+
if (!replyChannel || !replyTo) {
|
|
1105
|
+
return void 0;
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
sessionKey,
|
|
1109
|
+
agentId: deriveAgentIdFromSessionKey(sessionKey) ?? "main",
|
|
1110
|
+
replyChannel,
|
|
1111
|
+
replyTo,
|
|
1112
|
+
replyAccountId
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function tryDeriveAgentIdFromStorePath(sessionStorePath) {
|
|
1116
|
+
const normalized = path3.normalize(sessionStorePath);
|
|
1117
|
+
const parts = normalized.split(path3.sep).filter(Boolean);
|
|
1118
|
+
const agentsIndex = parts.lastIndexOf("agents");
|
|
1119
|
+
if (agentsIndex === -1) {
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
return readNonEmptyString4(parts[agentsIndex + 1]);
|
|
1123
|
+
}
|
|
1124
|
+
function listSessionStorePaths(explicitPath, baseAgentId) {
|
|
1125
|
+
const candidates = [];
|
|
1126
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1127
|
+
const addPath = (candidate) => {
|
|
1128
|
+
const trimmed = readNonEmptyString4(candidate);
|
|
1129
|
+
if (!trimmed) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const normalized = path3.normalize(trimmed);
|
|
1133
|
+
if (seen.has(normalized)) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
seen.add(normalized);
|
|
1137
|
+
candidates.push(normalized);
|
|
1138
|
+
};
|
|
1139
|
+
addPath(explicitPath);
|
|
1140
|
+
if (candidates.length > 0) {
|
|
1141
|
+
return candidates;
|
|
1142
|
+
}
|
|
1143
|
+
const defaultOpenClawStateDir = getDefaultOpenClawStateDir();
|
|
1144
|
+
addPath(path3.join(defaultOpenClawStateDir, "agents", baseAgentId, "sessions", "sessions.json"));
|
|
1145
|
+
const agentsRoot = path3.join(defaultOpenClawStateDir, "agents");
|
|
1146
|
+
try {
|
|
1147
|
+
if (fs2.existsSync(agentsRoot)) {
|
|
1148
|
+
for (const entry of fs2.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
1149
|
+
if (!entry.isDirectory()) {
|
|
1150
|
+
continue;
|
|
277
1151
|
}
|
|
278
|
-
|
|
1152
|
+
addPath(path3.join(agentsRoot, entry.name, "sessions", "sessions.json"));
|
|
1153
|
+
}
|
|
279
1154
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
serverName,
|
|
284
|
-
};
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
return candidates;
|
|
285
1158
|
}
|
|
286
|
-
function
|
|
287
|
-
|
|
1159
|
+
function readSessionStore(sessionStorePath, logger) {
|
|
1160
|
+
try {
|
|
1161
|
+
if (!fs2.existsSync(sessionStorePath)) {
|
|
1162
|
+
return void 0;
|
|
1163
|
+
}
|
|
1164
|
+
const raw = fs2.readFileSync(sessionStorePath, "utf-8");
|
|
1165
|
+
const parsed = JSON.parse(raw);
|
|
1166
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1167
|
+
return void 0;
|
|
1168
|
+
}
|
|
1169
|
+
return parsed;
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
logger.debug(
|
|
1172
|
+
`Failed to read session store ${sessionStorePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
1173
|
+
);
|
|
1174
|
+
return void 0;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function readSessionStores(sessionStorePath, baseAgentId, logger) {
|
|
1178
|
+
const snapshots = [];
|
|
1179
|
+
const candidates = listSessionStorePaths(sessionStorePath, baseAgentId);
|
|
1180
|
+
logger.info(
|
|
1181
|
+
`Session store scan candidates: base_agent_id=${baseAgentId}, explicit_path=${sessionStorePath ?? "n/a"}, candidates=${JSON.stringify(candidates)}`
|
|
1182
|
+
);
|
|
1183
|
+
for (const candidate of candidates) {
|
|
1184
|
+
const store = readSessionStore(candidate, logger);
|
|
1185
|
+
if (store) {
|
|
1186
|
+
const keys = Object.keys(store);
|
|
1187
|
+
logger.info(
|
|
1188
|
+
`Session store loaded: path=${candidate}, entries=${keys.length}, session_keys=${JSON.stringify(keys)}`
|
|
1189
|
+
);
|
|
1190
|
+
snapshots.push({ path: candidate, store });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return snapshots;
|
|
1194
|
+
}
|
|
1195
|
+
function mergeRoute(base, resolved, overrides, allowSessionOverride) {
|
|
1196
|
+
const nextSessionKey = allowSessionOverride && !overrides.sessionKey ? resolved.sessionKey : base.sessionKey;
|
|
1197
|
+
return {
|
|
1198
|
+
sessionKey: nextSessionKey,
|
|
1199
|
+
agentId: overrides.agentId === true ? base.agentId : resolved.agentId ?? deriveAgentIdFromSessionKey(nextSessionKey) ?? base.agentId,
|
|
1200
|
+
replyChannel: overrides.replyChannel === true ? base.replyChannel : resolved.replyChannel ?? base.replyChannel,
|
|
1201
|
+
replyTo: overrides.replyTo === true ? base.replyTo : resolved.replyTo ?? base.replyTo,
|
|
1202
|
+
replyAccountId: overrides.replyAccountId === true ? base.replyAccountId : resolved.replyAccountId ?? base.replyAccountId
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function selectExactRoute(snapshots, sessionKey) {
|
|
1206
|
+
let best;
|
|
1207
|
+
for (const snapshot of snapshots) {
|
|
1208
|
+
const entry = snapshot.store[sessionKey];
|
|
1209
|
+
const route = extractRouteFromEntry(sessionKey, entry);
|
|
1210
|
+
if (!route || !isExternalChannel(route.replyChannel)) {
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const updatedAt = normalizeUpdatedAt(entry?.updatedAt);
|
|
1214
|
+
if (!best || updatedAt > best.updatedAt) {
|
|
1215
|
+
best = { route, updatedAt };
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return best;
|
|
1219
|
+
}
|
|
1220
|
+
function selectBestRoute(snapshots, preferred, preferredAgentId, logger) {
|
|
1221
|
+
const candidates = [];
|
|
1222
|
+
const autoScan = preferred === void 0;
|
|
1223
|
+
for (const snapshot of snapshots) {
|
|
1224
|
+
const pathAgentId = tryDeriveAgentIdFromStorePath(snapshot.path);
|
|
1225
|
+
for (const [sessionKey, entry] of Object.entries(snapshot.store)) {
|
|
1226
|
+
if (sessionKey.includes(":subagent:")) {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (isInternalSessionKey(sessionKey)) {
|
|
1230
|
+
logger?.debug(`Skipping ${sessionKey}: internal session`);
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (autoScan && isGroupEntry(sessionKey, entry)) {
|
|
1234
|
+
logger?.debug(`Skipping ${sessionKey}: group entry in auto-scan`);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
const route = extractRouteFromEntry(sessionKey, entry);
|
|
1238
|
+
if (!route || !routeMatchesPreferred(route, preferred)) {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (preferredAgentId && route.agentId !== preferredAgentId && pathAgentId !== preferredAgentId) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
candidates.push({
|
|
1245
|
+
route,
|
|
1246
|
+
updatedAt: normalizeUpdatedAt(entry.updatedAt),
|
|
1247
|
+
isExternal: isExternalChannel(route.replyChannel),
|
|
1248
|
+
isDirect: isDirectSessionKey(sessionKey, entry)
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (candidates.length === 0) {
|
|
1253
|
+
return void 0;
|
|
1254
|
+
}
|
|
1255
|
+
const externalPool = candidates.filter((c) => c.isExternal);
|
|
1256
|
+
const channelPool = externalPool.length > 0 ? externalPool : candidates;
|
|
1257
|
+
const directPool = channelPool.filter((c) => c.isDirect);
|
|
1258
|
+
const finalPool = directPool.length > 0 ? directPool : channelPool;
|
|
1259
|
+
const chosen = finalPool.reduce((best, c) => c.updatedAt > best.updatedAt ? c : best);
|
|
1260
|
+
return { route: chosen.route, updatedAt: chosen.updatedAt };
|
|
1261
|
+
}
|
|
1262
|
+
function findSessionRouteForBinding(options, logger) {
|
|
1263
|
+
const channel = normalizeChannel2(options.channel);
|
|
1264
|
+
const to = normalizeReplyTarget(options.to, { channel });
|
|
1265
|
+
const accountId = readNonEmptyString4(options.accountId);
|
|
1266
|
+
const agentId = readNonEmptyString4(options.agentId) ?? "main";
|
|
1267
|
+
if (!channel || !to) {
|
|
1268
|
+
return void 0;
|
|
1269
|
+
}
|
|
1270
|
+
const snapshots = readSessionStores(options.sessionStorePath, agentId, logger);
|
|
1271
|
+
const best = selectBestRoute(snapshots, { channel, to, accountId }, agentId, logger);
|
|
1272
|
+
if (best) {
|
|
1273
|
+
return best.route;
|
|
1274
|
+
}
|
|
1275
|
+
const peerShape = inferPeerShape(channel, to);
|
|
1276
|
+
const targetLocal = stripTargetPrefix(to);
|
|
1277
|
+
if (!peerShape || !targetLocal) {
|
|
1278
|
+
return void 0;
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
sessionKey: `agent:${agentId}:${channel}:${peerShape}:${targetLocal}`,
|
|
1282
|
+
agentId,
|
|
1283
|
+
replyChannel: channel,
|
|
1284
|
+
replyTo: to,
|
|
1285
|
+
replyAccountId: accountId
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function inferPeerShape(channel, to) {
|
|
1289
|
+
const kind = to.split(":", 1)[0]?.toLowerCase();
|
|
1290
|
+
switch (kind) {
|
|
1291
|
+
case "user":
|
|
1292
|
+
return "direct";
|
|
1293
|
+
case "chat":
|
|
1294
|
+
return channel === "feishu" ? "group" : "direct";
|
|
1295
|
+
case "channel":
|
|
1296
|
+
return "channel";
|
|
1297
|
+
case "room":
|
|
1298
|
+
return "group";
|
|
1299
|
+
default:
|
|
1300
|
+
return void 0;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function stripTargetPrefix(to) {
|
|
1304
|
+
const idx = to.indexOf(":");
|
|
1305
|
+
if (idx === -1) {
|
|
1306
|
+
return readNonEmptyString4(to);
|
|
1307
|
+
}
|
|
1308
|
+
return readNonEmptyString4(to.slice(idx + 1));
|
|
1309
|
+
}
|
|
1310
|
+
async function resolveNotificationRoute(config, logger, options = {}) {
|
|
1311
|
+
const overrides = createRouteOverrides2(config.routeOverrides);
|
|
1312
|
+
const configRoute = {
|
|
1313
|
+
sessionKey: readNonEmptyString4(config.sessionKey) ?? "main",
|
|
1314
|
+
agentId: readNonEmptyString4(config.agentId) ?? deriveAgentIdFromSessionKey(config.sessionKey) ?? "main",
|
|
1315
|
+
replyChannel: normalizeChannel2(config.replyChannel),
|
|
1316
|
+
replyTo: normalizeReplyTarget(config.replyTo, {
|
|
1317
|
+
channel: normalizeChannel2(config.replyChannel),
|
|
1318
|
+
sessionKey: config.sessionKey
|
|
1319
|
+
}),
|
|
1320
|
+
replyAccountId: readNonEmptyString4(config.replyAccountId)
|
|
1321
|
+
};
|
|
1322
|
+
logger.info(
|
|
1323
|
+
`Route resolve start: session_key=${configRoute.sessionKey}, agent_id=${configRoute.agentId}, channel=${configRoute.replyChannel ?? "n/a"}, to=${configRoute.replyTo ?? "n/a"}, account=${configRoute.replyAccountId ?? "n/a"}, overrides=${JSON.stringify(overrides)}, ignore_remembered=${options.ignoreRemembered === true}`
|
|
1324
|
+
);
|
|
1325
|
+
const hasExplicitConfig = isAnyRouteOverrideEnabled(overrides) || !isInternalSessionKey(configRoute.sessionKey) || Boolean(configRoute.replyChannel && configRoute.replyTo);
|
|
1326
|
+
if (hasExplicitConfig) {
|
|
1327
|
+
const snapshots2 = readSessionStores(config.sessionStorePath, configRoute.agentId, logger);
|
|
1328
|
+
let enriched = selectExactRoute(snapshots2, configRoute.sessionKey)?.route;
|
|
1329
|
+
if (!enriched && configRoute.replyChannel && configRoute.replyTo) {
|
|
1330
|
+
enriched = selectBestRoute(
|
|
1331
|
+
snapshots2,
|
|
1332
|
+
{
|
|
1333
|
+
channel: configRoute.replyChannel,
|
|
1334
|
+
to: configRoute.replyTo,
|
|
1335
|
+
accountId: configRoute.replyAccountId
|
|
1336
|
+
},
|
|
1337
|
+
void 0,
|
|
1338
|
+
logger
|
|
1339
|
+
)?.route;
|
|
1340
|
+
}
|
|
1341
|
+
const resolved = enriched ? mergeRoute(configRoute, enriched, overrides, isInternalSessionKey(configRoute.sessionKey)) : configRoute;
|
|
1342
|
+
logger.info(
|
|
1343
|
+
`Route resolve final (config): session_key=${resolved.sessionKey}, agent_id=${resolved.agentId}, channel=${resolved.replyChannel ?? "n/a"}, to=${resolved.replyTo ?? "n/a"}, account=${resolved.replyAccountId ?? "n/a"}`
|
|
1344
|
+
);
|
|
1345
|
+
return { route: resolved, source: "config" };
|
|
1346
|
+
}
|
|
1347
|
+
const snapshots = readSessionStores(config.sessionStorePath, configRoute.agentId, logger);
|
|
1348
|
+
if (options.ignoreRemembered !== true) {
|
|
1349
|
+
const remembered = await readStoredNotificationRoute(
|
|
1350
|
+
config.store,
|
|
1351
|
+
config.serverName,
|
|
1352
|
+
logger
|
|
1353
|
+
);
|
|
1354
|
+
if (remembered) {
|
|
1355
|
+
logger.info(
|
|
1356
|
+
`Route resolve remembered: session_key=${remembered.sessionKey}, agent_id=${remembered.agentId}, channel=${remembered.replyChannel ?? "n/a"}, to=${remembered.replyTo ?? "n/a"}, account=${remembered.replyAccountId ?? "n/a"}`
|
|
1357
|
+
);
|
|
1358
|
+
const preferred = remembered.replyChannel && remembered.replyTo ? {
|
|
1359
|
+
channel: remembered.replyChannel,
|
|
1360
|
+
to: remembered.replyTo,
|
|
1361
|
+
accountId: remembered.replyAccountId
|
|
1362
|
+
} : void 0;
|
|
1363
|
+
const peerMatch = selectBestRoute(snapshots, preferred, void 0, logger);
|
|
1364
|
+
if (peerMatch) {
|
|
1365
|
+
return { route: peerMatch.route, source: "remembered" };
|
|
1366
|
+
}
|
|
1367
|
+
if (!isInternalSessionKey(remembered.sessionKey)) {
|
|
288
1368
|
return {
|
|
289
|
-
|
|
1369
|
+
route: {
|
|
1370
|
+
sessionKey: remembered.sessionKey,
|
|
1371
|
+
agentId: remembered.agentId,
|
|
1372
|
+
replyChannel: remembered.replyChannel,
|
|
1373
|
+
replyTo: remembered.replyTo,
|
|
1374
|
+
replyAccountId: remembered.replyAccountId
|
|
1375
|
+
},
|
|
1376
|
+
source: "remembered"
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
logger.warn(
|
|
1380
|
+
`Remembered route has internal session_key=${remembered.sessionKey} and no peer match; falling through to session-store scan.`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const best = selectBestRoute(snapshots, void 0, void 0, logger);
|
|
1385
|
+
if (best) {
|
|
1386
|
+
logger.info(
|
|
1387
|
+
`Route resolve from session store: session_key=${best.route.sessionKey}, agent_id=${best.route.agentId}, channel=${best.route.replyChannel ?? "n/a"}, to=${best.route.replyTo ?? "n/a"}, account=${best.route.replyAccountId ?? "n/a"}, updated_at=${best.updatedAt}`
|
|
1388
|
+
);
|
|
1389
|
+
return { route: best.route, source: "session-store" };
|
|
1390
|
+
}
|
|
1391
|
+
logger.warn(
|
|
1392
|
+
`Route resolve fell back to config default: session_key=${configRoute.sessionKey}, agent_id=${configRoute.agentId}`
|
|
1393
|
+
);
|
|
1394
|
+
return { route: configRoute, source: "default" };
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/agent-prompt-templates.ts
|
|
1398
|
+
function buildContextLines(context) {
|
|
1399
|
+
return [
|
|
1400
|
+
`homedir=${context.eigenfluxHome}`,
|
|
1401
|
+
`server=${context.serverName}`
|
|
1402
|
+
];
|
|
1403
|
+
}
|
|
1404
|
+
function buildAuthRequiredPromptTemplate({
|
|
1405
|
+
context,
|
|
1406
|
+
stderr
|
|
1407
|
+
}) {
|
|
1408
|
+
const lines = [
|
|
1409
|
+
"[EIGENFLUX_AUTH_REQUIRED]",
|
|
1410
|
+
...buildContextLines(context),
|
|
1411
|
+
"EigenFlux authentication is required.",
|
|
1412
|
+
`Run \`eigenflux auth login --email <email> -s ${context.serverName}\` to authenticate.`,
|
|
1413
|
+
`For first time login, use the ef-profile skill to complete the onboarding flow.`
|
|
1414
|
+
];
|
|
1415
|
+
if (stderr) {
|
|
1416
|
+
lines.push(`detail=${stderr}`);
|
|
1417
|
+
}
|
|
1418
|
+
return lines.join("\n");
|
|
1419
|
+
}
|
|
1420
|
+
function buildFeedPayloadPromptTemplate(payload, context) {
|
|
1421
|
+
return [
|
|
1422
|
+
"[EIGENFLUX_FEED_PAYLOAD]",
|
|
1423
|
+
...buildContextLines(context),
|
|
1424
|
+
`EigenFlux feed payload received. Use the ef-broadcast skill to process feed payload.`,
|
|
1425
|
+
"Payload:",
|
|
1426
|
+
"```json",
|
|
1427
|
+
JSON.stringify(payload, null, 2),
|
|
1428
|
+
"```"
|
|
1429
|
+
].join("\n");
|
|
1430
|
+
}
|
|
1431
|
+
function buildNotInstalledPromptTemplate({
|
|
1432
|
+
bin,
|
|
1433
|
+
installCommand
|
|
1434
|
+
}) {
|
|
1435
|
+
return [
|
|
1436
|
+
"[EIGENFLUX_NOT_INSTALLED]",
|
|
1437
|
+
`The EigenFlux CLI is not installed on this machine (tried bin=${bin}).`,
|
|
1438
|
+
"Please tell the user to run the following command to install it:",
|
|
1439
|
+
`\`${installCommand}\``
|
|
1440
|
+
].join("\n");
|
|
1441
|
+
}
|
|
1442
|
+
function buildPmStreamEventPromptTemplate(event, context) {
|
|
1443
|
+
return [
|
|
1444
|
+
"[EIGENFLUX_MSG_PAYLOAD]",
|
|
1445
|
+
...buildContextLines(context),
|
|
1446
|
+
`EigenFlux private messages received. Use the ef-communication skill to process private messages.`,
|
|
1447
|
+
"Payload:",
|
|
1448
|
+
"```json",
|
|
1449
|
+
JSON.stringify(event, null, 2),
|
|
1450
|
+
"```"
|
|
1451
|
+
].join("\n");
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/notifier.ts
|
|
1455
|
+
var import_node_crypto = require("crypto");
|
|
1456
|
+
var COMMAND_TIMEOUT_MS = 15e3;
|
|
1457
|
+
var SUBAGENT_WAIT_TIMEOUT_MS = 18e4;
|
|
1458
|
+
var HEARTBEAT_REASON = "plugin:eigenflux";
|
|
1459
|
+
var EigenFluxNotifier = class {
|
|
1460
|
+
constructor(api, logger, config) {
|
|
1461
|
+
this.api = api;
|
|
1462
|
+
this.logger = logger;
|
|
1463
|
+
this.config = config;
|
|
1464
|
+
}
|
|
1465
|
+
get runtime() {
|
|
1466
|
+
return this.api.runtime ?? {};
|
|
1467
|
+
}
|
|
1468
|
+
async deliver(message) {
|
|
1469
|
+
const initial = await this.resolveRoute();
|
|
1470
|
+
this.logger.info(
|
|
1471
|
+
`Delivery route resolved: source=${initial.source}, ${formatRouteForLog(initial.route)}, message_preview=${previewMessage(message)}`
|
|
1472
|
+
);
|
|
1473
|
+
const firstAttempt = await this.attemptDelivery(message, initial.route);
|
|
1474
|
+
if (firstAttempt.result.ok) {
|
|
1475
|
+
await this.rememberRouteIfChanged(firstAttempt.finalRoute, initial.source);
|
|
1476
|
+
this.logDispatch(firstAttempt.result);
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
if (initial.source === "remembered") {
|
|
1480
|
+
this.logger.warn(
|
|
1481
|
+
`All transports failed with remembered route; re-resolving without remembered config.`
|
|
1482
|
+
);
|
|
1483
|
+
const fallback = await this.resolveRoute({ ignoreRemembered: true });
|
|
1484
|
+
if (fallback.route.sessionKey !== initial.route.sessionKey || fallback.route.replyTo !== initial.route.replyTo || fallback.route.replyChannel !== initial.route.replyChannel) {
|
|
1485
|
+
this.logger.info(
|
|
1486
|
+
`Retrying delivery with fresh route: source=${fallback.source}, ${formatRouteForLog(fallback.route)}`
|
|
1487
|
+
);
|
|
1488
|
+
const retry = await this.attemptDelivery(message, fallback.route);
|
|
1489
|
+
if (retry.result.ok) {
|
|
1490
|
+
await this.rememberRouteIfChanged(retry.finalRoute, fallback.source);
|
|
1491
|
+
this.logDispatch(retry.result);
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
this.logger.error(
|
|
1495
|
+
`Failed to deliver notification after fresh re-resolve: ${retry.errors.join(" | ")}`
|
|
1496
|
+
);
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
this.logger.warn("Fresh re-resolve produced the same route; skipping retry.");
|
|
1500
|
+
}
|
|
1501
|
+
this.logger.error(`Failed to deliver notification: ${firstAttempt.errors.join(" | ")}`);
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
async attemptDelivery(message, route) {
|
|
1505
|
+
const attempts = [
|
|
1506
|
+
() => this.tryNotifyViaRuntimeSubagent(message, route),
|
|
1507
|
+
() => this.tryNotifyViaRuntimeCommandAgent(message, route),
|
|
1508
|
+
() => this.tryNotifyViaRuntimeHeartbeat(message, route),
|
|
1509
|
+
() => this.tryNotifyViaRuntimeCommandHeartbeat(message)
|
|
1510
|
+
];
|
|
1511
|
+
const errors = [];
|
|
1512
|
+
for (const attempt of attempts) {
|
|
1513
|
+
const result = await attempt();
|
|
1514
|
+
if (result.ok) {
|
|
1515
|
+
const finalRoute = {
|
|
1516
|
+
sessionKey: result.sessionKey ?? route.sessionKey,
|
|
1517
|
+
agentId: route.agentId,
|
|
1518
|
+
replyChannel: route.replyChannel,
|
|
1519
|
+
replyTo: route.replyTo,
|
|
1520
|
+
replyAccountId: route.replyAccountId
|
|
290
1521
|
};
|
|
1522
|
+
return { result, finalRoute, errors };
|
|
1523
|
+
}
|
|
1524
|
+
this.logger.warn(
|
|
1525
|
+
`Notification attempt failed: mode=${result.mode}, ${formatRouteForLog(route)}, error=${result.error}`
|
|
1526
|
+
);
|
|
1527
|
+
errors.push(`${result.mode}: ${result.error}`);
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1530
|
+
result: { ok: false, mode: "all", error: errors.join(" | ") },
|
|
1531
|
+
finalRoute: route,
|
|
1532
|
+
errors
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
async tryNotifyViaRuntimeSubagent(message, route) {
|
|
1536
|
+
const runtimeSubagent = this.runtime.subagent;
|
|
1537
|
+
if (!runtimeSubagent || typeof runtimeSubagent.run !== "function") {
|
|
1538
|
+
return {
|
|
1539
|
+
ok: false,
|
|
1540
|
+
mode: "runtime.subagent",
|
|
1541
|
+
error: "runtime.subagent.run is unavailable"
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
this.logger.info(
|
|
1546
|
+
`Attempting runtime.subagent delivery: ${formatRouteForLog(route)}, deliver=true`
|
|
1547
|
+
);
|
|
1548
|
+
const { runId } = await runtimeSubagent.run({
|
|
1549
|
+
sessionKey: route.sessionKey,
|
|
1550
|
+
message,
|
|
1551
|
+
deliver: true,
|
|
1552
|
+
idempotencyKey: (0, import_node_crypto.randomUUID)()
|
|
1553
|
+
});
|
|
1554
|
+
if (typeof runtimeSubagent.waitForRun === "function") {
|
|
1555
|
+
const waited = await runtimeSubagent.waitForRun({
|
|
1556
|
+
runId,
|
|
1557
|
+
timeoutMs: SUBAGENT_WAIT_TIMEOUT_MS
|
|
1558
|
+
});
|
|
1559
|
+
if (waited.status === "error") {
|
|
1560
|
+
return {
|
|
1561
|
+
ok: false,
|
|
1562
|
+
mode: "runtime.subagent",
|
|
1563
|
+
error: `subagent run error${waited.error ? `: ${waited.error}` : ""}`
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
ok: true,
|
|
1569
|
+
mode: "runtime.subagent",
|
|
1570
|
+
sessionKey: route.sessionKey,
|
|
1571
|
+
runId
|
|
1572
|
+
};
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
return {
|
|
1575
|
+
ok: false,
|
|
1576
|
+
mode: "runtime.subagent",
|
|
1577
|
+
error: formatError(error)
|
|
1578
|
+
};
|
|
291
1579
|
}
|
|
292
|
-
|
|
1580
|
+
}
|
|
1581
|
+
async tryNotifyViaRuntimeCommandAgent(message, route) {
|
|
1582
|
+
return this.runRuntimeCommand(
|
|
1583
|
+
"runtime.command.agent",
|
|
1584
|
+
this.buildAgentCliArgs(message, route),
|
|
1585
|
+
route
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
async tryNotifyViaRuntimeHeartbeat(message, route) {
|
|
1589
|
+
const runtimeSystem = this.runtime.system;
|
|
1590
|
+
if (!runtimeSystem || typeof runtimeSystem.enqueueSystemEvent !== "function" || typeof runtimeSystem.requestHeartbeatNow !== "function") {
|
|
1591
|
+
return {
|
|
1592
|
+
ok: false,
|
|
1593
|
+
mode: "runtime.system.heartbeat",
|
|
1594
|
+
error: "runtime.system heartbeat APIs are unavailable"
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
const deliveryContext = this.resolveHeartbeatDeliveryContext(route);
|
|
1599
|
+
this.logger.info(
|
|
1600
|
+
`Attempting runtime.system.heartbeat delivery: ${formatRouteForLog(route)}, delivery_context=${formatDeliveryContextForLog(deliveryContext)}`
|
|
1601
|
+
);
|
|
1602
|
+
const enqueued = runtimeSystem.enqueueSystemEvent(message, {
|
|
1603
|
+
sessionKey: route.sessionKey,
|
|
1604
|
+
...deliveryContext ? { deliveryContext } : {}
|
|
1605
|
+
});
|
|
1606
|
+
runtimeSystem.requestHeartbeatNow({
|
|
1607
|
+
reason: HEARTBEAT_REASON,
|
|
1608
|
+
coalesceMs: 0,
|
|
1609
|
+
agentId: route.agentId,
|
|
1610
|
+
sessionKey: route.sessionKey
|
|
1611
|
+
});
|
|
1612
|
+
return {
|
|
1613
|
+
ok: true,
|
|
1614
|
+
mode: "runtime.system.heartbeat",
|
|
1615
|
+
sessionKey: route.sessionKey,
|
|
1616
|
+
detail: enqueued ? "enqueued" : "duplicate-enqueued"
|
|
1617
|
+
};
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
return {
|
|
1620
|
+
ok: false,
|
|
1621
|
+
mode: "runtime.system.heartbeat",
|
|
1622
|
+
error: formatError(error)
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
async tryNotifyViaRuntimeCommandHeartbeat(message) {
|
|
1627
|
+
const { route } = await this.resolveRoute();
|
|
1628
|
+
return this.runRuntimeCommand(
|
|
1629
|
+
"runtime.command.heartbeat",
|
|
1630
|
+
this.buildHeartbeatCliArgs(message),
|
|
1631
|
+
route
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
async runRuntimeCommand(mode, argv, route) {
|
|
1635
|
+
const runtimeCommand = this.runtime.system?.runCommandWithTimeout;
|
|
1636
|
+
if (typeof runtimeCommand !== "function") {
|
|
1637
|
+
return {
|
|
1638
|
+
ok: false,
|
|
1639
|
+
mode,
|
|
1640
|
+
error: "runtime.system.runCommandWithTimeout is unavailable"
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
try {
|
|
1644
|
+
this.logger.info(
|
|
1645
|
+
`Attempting ${mode} delivery: ${formatRouteForLog(route)}, argv=${formatCommandArgsForLog(argv)}`
|
|
1646
|
+
);
|
|
1647
|
+
const result = await runtimeCommand(argv, { timeoutMs: COMMAND_TIMEOUT_MS });
|
|
1648
|
+
if (result.code === 0) {
|
|
293
1649
|
return {
|
|
294
|
-
|
|
1650
|
+
ok: true,
|
|
1651
|
+
mode,
|
|
1652
|
+
sessionKey: route.sessionKey
|
|
295
1653
|
};
|
|
1654
|
+
}
|
|
1655
|
+
return {
|
|
1656
|
+
ok: false,
|
|
1657
|
+
mode,
|
|
1658
|
+
error: `${formatCommandFailure(result)} (argv=${formatCommandArgsForLog(argv)})`
|
|
1659
|
+
};
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
return {
|
|
1662
|
+
ok: false,
|
|
1663
|
+
mode,
|
|
1664
|
+
error: formatError(error)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
buildAgentCliArgs(message, route) {
|
|
1669
|
+
const args = [
|
|
1670
|
+
this.config.openclawCliBin,
|
|
1671
|
+
"agent",
|
|
1672
|
+
"--message",
|
|
1673
|
+
message,
|
|
1674
|
+
"--agent",
|
|
1675
|
+
route.agentId,
|
|
1676
|
+
"--deliver"
|
|
1677
|
+
];
|
|
1678
|
+
if (route.replyChannel) {
|
|
1679
|
+
args.push("--reply-channel", route.replyChannel);
|
|
296
1680
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1681
|
+
if (route.replyTo) {
|
|
1682
|
+
args.push("--reply-to", route.replyTo);
|
|
1683
|
+
}
|
|
1684
|
+
if (route.replyAccountId) {
|
|
1685
|
+
args.push("--reply-account", route.replyAccountId);
|
|
1686
|
+
}
|
|
1687
|
+
return args;
|
|
1688
|
+
}
|
|
1689
|
+
buildHeartbeatCliArgs(message) {
|
|
1690
|
+
return [
|
|
1691
|
+
this.config.openclawCliBin,
|
|
1692
|
+
"system",
|
|
1693
|
+
"event",
|
|
1694
|
+
"--text",
|
|
1695
|
+
message,
|
|
1696
|
+
"--mode",
|
|
1697
|
+
"now"
|
|
1698
|
+
];
|
|
1699
|
+
}
|
|
1700
|
+
resolveHeartbeatDeliveryContext(route) {
|
|
1701
|
+
if (!route.replyChannel && !route.replyTo && !route.replyAccountId) {
|
|
1702
|
+
return void 0;
|
|
301
1703
|
}
|
|
302
1704
|
return {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
].join('\n'),
|
|
1705
|
+
...route.replyChannel ? { channel: route.replyChannel } : {},
|
|
1706
|
+
...route.replyTo ? { to: route.replyTo } : {},
|
|
1707
|
+
...route.replyAccountId ? { accountId: route.replyAccountId } : {}
|
|
307
1708
|
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
1709
|
+
}
|
|
1710
|
+
async resolveRoute(options = {}) {
|
|
1711
|
+
return resolveNotificationRoute(
|
|
1712
|
+
this.config,
|
|
1713
|
+
this.logger,
|
|
1714
|
+
options
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Persist the successful route to the eigenflux CLI config unless it came from
|
|
1719
|
+
* the remembered config already (no-op when unchanged).
|
|
1720
|
+
*/
|
|
1721
|
+
async rememberRouteIfChanged(route, source) {
|
|
1722
|
+
if (!route.sessionKey || !route.agentId) {
|
|
1723
|
+
return;
|
|
312
1724
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1725
|
+
if (isInternalSessionKey(route.sessionKey)) {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (!route.replyChannel || !route.replyTo) {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (source === "remembered") {
|
|
1732
|
+
this.logger.debug(
|
|
1733
|
+
`Skipping remembered-route write; route came from config (session_key=${route.sessionKey})`
|
|
1734
|
+
);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
await writeStoredNotificationRoute(
|
|
1738
|
+
this.config.store,
|
|
1739
|
+
this.config.serverName,
|
|
1740
|
+
route,
|
|
1741
|
+
this.logger
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
logDispatch(result) {
|
|
1745
|
+
const details = [
|
|
1746
|
+
`mode=${result.mode}`,
|
|
1747
|
+
result.sessionKey ? `session_key=${result.sessionKey}` : null,
|
|
1748
|
+
result.runId ? `run_id=${result.runId}` : null,
|
|
1749
|
+
result.detail ? `detail=${result.detail}` : null
|
|
1750
|
+
].filter(Boolean).join(", ");
|
|
1751
|
+
this.logger.info(`Notification dispatched: ${details}`);
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
function formatCommandFailure(result) {
|
|
1755
|
+
return result.stderr?.trim() || result.stdout?.trim() || `command exited with ${result.code ?? "unknown"}`;
|
|
326
1756
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
defaultRuntime ? `Default server: ${defaultRuntime.server.name}` : undefined,
|
|
333
|
-
runtimes.length > 0
|
|
334
|
-
? `Available servers: ${runtimes.map((runtime) => runtime.server.name).join(', ')}`
|
|
335
|
-
: undefined,
|
|
336
|
-
'',
|
|
337
|
-
'/eigenflux auth — Show credential status',
|
|
338
|
-
'/eigenflux profile — Fetch agent profile',
|
|
339
|
-
'/eigenflux servers — List discovered servers',
|
|
340
|
-
'/eigenflux feed — Run one feed refresh',
|
|
341
|
-
'/eigenflux pm — Show PM stream status',
|
|
342
|
-
'/eigenflux here — Remember current conversation as delivery route',
|
|
343
|
-
'/eigenflux version — Show eigenflux CLI version info',
|
|
344
|
-
]
|
|
345
|
-
.filter(Boolean)
|
|
346
|
-
.join('\n');
|
|
1757
|
+
function formatError(error) {
|
|
1758
|
+
if (error instanceof Error) {
|
|
1759
|
+
return `${error.name}: ${error.message}`;
|
|
1760
|
+
}
|
|
1761
|
+
return String(error);
|
|
347
1762
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
1763
|
+
function formatRouteForLog(route) {
|
|
1764
|
+
return [
|
|
1765
|
+
`session_key=${route.sessionKey}`,
|
|
1766
|
+
`agent_id=${route.agentId}`,
|
|
1767
|
+
`channel=${route.replyChannel ?? "n/a"}`,
|
|
1768
|
+
`to=${route.replyTo ?? "n/a"}`,
|
|
1769
|
+
`account=${route.replyAccountId ?? "n/a"}`
|
|
1770
|
+
].join(", ");
|
|
354
1771
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
1772
|
+
function formatDeliveryContextForLog(deliveryContext) {
|
|
1773
|
+
if (!deliveryContext) {
|
|
1774
|
+
return "none";
|
|
1775
|
+
}
|
|
1776
|
+
return [
|
|
1777
|
+
`channel=${deliveryContext.channel ?? "n/a"}`,
|
|
1778
|
+
`to=${deliveryContext.to ?? "n/a"}`,
|
|
1779
|
+
`account=${deliveryContext.accountId ?? "n/a"}`
|
|
1780
|
+
].join(", ");
|
|
1781
|
+
}
|
|
1782
|
+
function previewMessage(message, maxLength = 120) {
|
|
1783
|
+
const singleLine = message.replace(/\s+/gu, " ").trim();
|
|
1784
|
+
if (singleLine.length <= maxLength) {
|
|
1785
|
+
return JSON.stringify(singleLine);
|
|
1786
|
+
}
|
|
1787
|
+
return JSON.stringify(`${singleLine.slice(0, maxLength - 3)}...`);
|
|
357
1788
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1789
|
+
function formatCommandArgsForLog(argv) {
|
|
1790
|
+
const sanitized = [...argv];
|
|
1791
|
+
for (let index = 0; index < sanitized.length; index += 1) {
|
|
1792
|
+
if (sanitized[index] === "--message" || sanitized[index] === "--text") {
|
|
1793
|
+
if (typeof sanitized[index + 1] === "string") {
|
|
1794
|
+
sanitized[index + 1] = `<len:${sanitized[index + 1].length}>`;
|
|
1795
|
+
}
|
|
362
1796
|
}
|
|
363
|
-
|
|
364
|
-
|
|
1797
|
+
}
|
|
1798
|
+
return JSON.stringify(sanitized);
|
|
365
1799
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
1800
|
+
|
|
1801
|
+
// src/index.ts
|
|
1802
|
+
var COMMAND_NAMES = ["auth", "profile", "servers", "feed", "pm", "here", "version"];
|
|
1803
|
+
var COMMAND_NAME_SET = new Set(COMMAND_NAMES);
|
|
1804
|
+
var DEFAULT_ROUTING = {
|
|
1805
|
+
sessionKey: PLUGIN_CONFIG.DEFAULT_SESSION_KEY,
|
|
1806
|
+
agentId: PLUGIN_CONFIG.DEFAULT_AGENT_ID,
|
|
1807
|
+
routeOverrides: {
|
|
1808
|
+
sessionKey: false,
|
|
1809
|
+
agentId: false,
|
|
1810
|
+
replyChannel: false,
|
|
1811
|
+
replyTo: false,
|
|
1812
|
+
replyAccountId: false
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
function registerPlugin(api) {
|
|
1816
|
+
const logger = new Logger(resolvePluginLogger(api));
|
|
1817
|
+
const pluginConfig = resolvePluginConfig(api.pluginConfig, logger);
|
|
1818
|
+
const eigenfluxHome = resolveEigenfluxHome();
|
|
1819
|
+
const store = createInMemoryPluginStore();
|
|
1820
|
+
let runtimes = [];
|
|
1821
|
+
let notInstalledPromptDelivered = false;
|
|
1822
|
+
api.registerService({
|
|
1823
|
+
id: "eigenflux:discovery",
|
|
1824
|
+
start: async () => {
|
|
1825
|
+
logger.info("Starting EigenFlux discovery service...");
|
|
1826
|
+
const discovery = await discoverServers(pluginConfig.eigenfluxBin, logger);
|
|
1827
|
+
if (discovery.kind === "not_installed") {
|
|
1828
|
+
logger.warn(
|
|
1829
|
+
`EigenFlux CLI not installed (bin=${discovery.bin}); delivering install prompt to user`
|
|
1830
|
+
);
|
|
1831
|
+
if (!notInstalledPromptDelivered) {
|
|
1832
|
+
notInstalledPromptDelivered = true;
|
|
1833
|
+
await deliverNotInstalledPrompt(api, logger, pluginConfig, eigenfluxHome, discovery.bin, store);
|
|
385
1834
|
}
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const servers = discovery.servers;
|
|
1838
|
+
if (servers.length === 0) {
|
|
1839
|
+
logger.warn("No EigenFlux servers discovered; services will not start");
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
logger.info(`Discovered ${servers.length} server(s): ${servers.map((s) => s.name).join(", ")}`);
|
|
1843
|
+
runtimes = servers.map(
|
|
1844
|
+
(server) => createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, store)
|
|
1845
|
+
);
|
|
1846
|
+
for (const runtime of runtimes) {
|
|
1847
|
+
logger.info(`Starting services for server=${runtime.server.name}`);
|
|
1848
|
+
await runtime.feedPoller.start();
|
|
1849
|
+
await runtime.streamClient.start();
|
|
1850
|
+
}
|
|
1851
|
+
},
|
|
1852
|
+
stop: async () => {
|
|
1853
|
+
logger.info("Stopping EigenFlux discovery service...");
|
|
1854
|
+
for (const runtime of runtimes) {
|
|
1855
|
+
logger.info(`Stopping services for server=${runtime.server.name}`);
|
|
1856
|
+
runtime.feedPoller.stop();
|
|
1857
|
+
await runtime.streamClient.stop();
|
|
1858
|
+
}
|
|
1859
|
+
runtimes = [];
|
|
1860
|
+
notInstalledPromptDelivered = false;
|
|
386
1861
|
}
|
|
387
|
-
|
|
388
|
-
|
|
1862
|
+
});
|
|
1863
|
+
registerCommand(
|
|
1864
|
+
api,
|
|
1865
|
+
logger,
|
|
1866
|
+
pluginConfig,
|
|
1867
|
+
eigenfluxHome,
|
|
1868
|
+
store,
|
|
1869
|
+
() => runtimes,
|
|
1870
|
+
(next) => {
|
|
1871
|
+
runtimes = next;
|
|
389
1872
|
}
|
|
390
|
-
|
|
391
|
-
agentId: runtime.routing.agentId,
|
|
392
|
-
channel,
|
|
393
|
-
to,
|
|
394
|
-
accountId,
|
|
395
|
-
}, logger);
|
|
1873
|
+
);
|
|
396
1874
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (!saved) {
|
|
407
|
-
return `Failed to persist the current EigenFlux route for server=${runtime.server.name}; check plugin logs for details.`;
|
|
1875
|
+
function resolvePluginLogger(api) {
|
|
1876
|
+
const runtimeLogging = api.runtime?.logging;
|
|
1877
|
+
if (runtimeLogging && typeof runtimeLogging.getChildLogger === "function") {
|
|
1878
|
+
try {
|
|
1879
|
+
const child = runtimeLogging.getChildLogger({ plugin: "eigenflux" });
|
|
1880
|
+
if (child) {
|
|
1881
|
+
return child;
|
|
1882
|
+
}
|
|
1883
|
+
} catch {
|
|
408
1884
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1885
|
+
}
|
|
1886
|
+
return api.logger;
|
|
1887
|
+
}
|
|
1888
|
+
var index_default = (0, import_plugin_entry.definePluginEntry)({
|
|
1889
|
+
id: "openclaw-eigenflux",
|
|
1890
|
+
name: "EigenFlux",
|
|
1891
|
+
description: "OpenClaw extension for EigenFlux with CLI-based feed polling and PM streaming",
|
|
1892
|
+
register(api) {
|
|
1893
|
+
if (api.registrationMode && api.registrationMode !== "full") return;
|
|
1894
|
+
registerPlugin(api);
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
var INSTALL_COMMAND = "curl -fsSL https://eigenflux.ai/install.sh | bash";
|
|
1898
|
+
async function deliverNotInstalledPrompt(api, logger, pluginConfig, _eigenfluxHome, bin, store) {
|
|
1899
|
+
const notifier = new EigenFluxNotifier(api, logger, {
|
|
1900
|
+
sessionKey: DEFAULT_ROUTING.sessionKey,
|
|
1901
|
+
agentId: DEFAULT_ROUTING.agentId,
|
|
1902
|
+
replyChannel: DEFAULT_ROUTING.replyChannel,
|
|
1903
|
+
replyTo: DEFAULT_ROUTING.replyTo,
|
|
1904
|
+
replyAccountId: DEFAULT_ROUTING.replyAccountId,
|
|
1905
|
+
openclawCliBin: pluginConfig.openclawCliBin,
|
|
1906
|
+
routeOverrides: DEFAULT_ROUTING.routeOverrides
|
|
1907
|
+
});
|
|
1908
|
+
await notifier.deliver(
|
|
1909
|
+
buildNotInstalledPromptTemplate({ bin, installCommand: INSTALL_COMMAND })
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, store) {
|
|
1913
|
+
const routing = pluginConfig.serverRouting[server.name] ?? DEFAULT_ROUTING;
|
|
1914
|
+
const credentialsLoader = new CredentialsLoader(logger, eigenfluxHome, server.name);
|
|
1915
|
+
const notifier = new EigenFluxNotifier(api, logger, {
|
|
1916
|
+
store,
|
|
1917
|
+
eigenfluxBin: pluginConfig.eigenfluxBin,
|
|
1918
|
+
serverName: server.name,
|
|
1919
|
+
sessionKey: routing.sessionKey,
|
|
1920
|
+
agentId: routing.agentId,
|
|
1921
|
+
replyChannel: routing.replyChannel,
|
|
1922
|
+
replyTo: routing.replyTo,
|
|
1923
|
+
replyAccountId: routing.replyAccountId,
|
|
1924
|
+
openclawCliBin: pluginConfig.openclawCliBin,
|
|
1925
|
+
routeOverrides: routing.routeOverrides
|
|
1926
|
+
});
|
|
1927
|
+
const getPromptContext = () => ({
|
|
1928
|
+
serverName: server.name,
|
|
1929
|
+
eigenfluxHome
|
|
1930
|
+
});
|
|
1931
|
+
let lastAuthPromptKey = null;
|
|
1932
|
+
const resetAuthPromptGate = () => {
|
|
1933
|
+
lastAuthPromptKey = null;
|
|
1934
|
+
};
|
|
1935
|
+
const notifyAuthRequired = async (_authEvent) => {
|
|
1936
|
+
const promptKey = `auth_required:${server.name}`;
|
|
1937
|
+
if (lastAuthPromptKey === promptKey) {
|
|
1938
|
+
logger.debug(`Skipping duplicate auth prompt for server=${server.name}`);
|
|
1939
|
+
return;
|
|
424
1940
|
}
|
|
425
|
-
|
|
426
|
-
|
|
1941
|
+
lastAuthPromptKey = promptKey;
|
|
1942
|
+
await notifier.deliver(
|
|
1943
|
+
buildAuthRequiredPromptTemplate({ context: getPromptContext() })
|
|
1944
|
+
);
|
|
1945
|
+
};
|
|
1946
|
+
const feedPoller = new EigenFluxPollingClient({
|
|
1947
|
+
serverName: server.name,
|
|
1948
|
+
eigenfluxBin: pluginConfig.eigenfluxBin,
|
|
1949
|
+
resolvePollIntervalSec: () => readPollIntervalSec(pluginConfig.eigenfluxBin, server.name, logger),
|
|
1950
|
+
logger,
|
|
1951
|
+
onFeedPolled: async (payload) => {
|
|
1952
|
+
resetAuthPromptGate();
|
|
1953
|
+
await notifier.deliver(buildFeedPayloadPromptTemplate(payload, getPromptContext()));
|
|
1954
|
+
},
|
|
1955
|
+
onAuthRequired: notifyAuthRequired
|
|
1956
|
+
});
|
|
1957
|
+
const streamClient = new EigenFluxStreamClient({
|
|
1958
|
+
serverName: server.name,
|
|
1959
|
+
eigenfluxBin: pluginConfig.eigenfluxBin,
|
|
1960
|
+
logger,
|
|
1961
|
+
onPmEvent: async (event) => {
|
|
1962
|
+
resetAuthPromptGate();
|
|
1963
|
+
await notifier.deliver(buildPmStreamEventPromptTemplate(event, getPromptContext()));
|
|
1964
|
+
},
|
|
1965
|
+
onAuthRequired: async () => {
|
|
1966
|
+
await notifyAuthRequired({ reason: "auth_required" });
|
|
427
1967
|
}
|
|
1968
|
+
});
|
|
1969
|
+
return {
|
|
1970
|
+
server,
|
|
1971
|
+
routing,
|
|
1972
|
+
credentialsLoader,
|
|
1973
|
+
notifier,
|
|
1974
|
+
feedPoller,
|
|
1975
|
+
streamClient,
|
|
1976
|
+
getPromptContext
|
|
1977
|
+
};
|
|
428
1978
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1979
|
+
function registerCommand(api, logger, pluginConfig, eigenfluxHome, store, getRuntimes, setRuntimes) {
|
|
1980
|
+
if (!api.registerCommand) {
|
|
1981
|
+
logger.warn("registerCommand API unavailable; skipping /eigenflux command registration");
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
let inflightDiscovery = null;
|
|
1985
|
+
const runDiscovery = async () => {
|
|
1986
|
+
const discovery = await discoverServers(pluginConfig.eigenfluxBin, logger);
|
|
1987
|
+
if (discovery.kind === "not_installed") {
|
|
1988
|
+
return { runtimes: getRuntimes(), notInstalledBin: discovery.bin };
|
|
437
1989
|
}
|
|
438
|
-
if (
|
|
439
|
-
|
|
1990
|
+
if (discovery.servers.length === 0) {
|
|
1991
|
+
return { runtimes: getRuntimes() };
|
|
440
1992
|
}
|
|
441
|
-
|
|
442
|
-
|
|
1993
|
+
const created = discovery.servers.map(
|
|
1994
|
+
(server) => createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, store)
|
|
1995
|
+
);
|
|
1996
|
+
setRuntimes(created);
|
|
1997
|
+
return { runtimes: created };
|
|
1998
|
+
};
|
|
1999
|
+
const ensureRuntimes = async () => {
|
|
2000
|
+
const existing = getRuntimes();
|
|
2001
|
+
if (existing.length > 0) {
|
|
2002
|
+
return { runtimes: existing };
|
|
443
2003
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (result.kind === 'auth_required') {
|
|
449
|
-
return (0, agent_prompt_templates_1.buildAuthRequiredPromptTemplate)({ context: runtime.getPromptContext() });
|
|
2004
|
+
if (!inflightDiscovery) {
|
|
2005
|
+
inflightDiscovery = runDiscovery().finally(() => {
|
|
2006
|
+
inflightDiscovery = null;
|
|
2007
|
+
});
|
|
450
2008
|
}
|
|
451
|
-
|
|
452
|
-
|
|
2009
|
+
return inflightDiscovery;
|
|
2010
|
+
};
|
|
2011
|
+
api.registerCommand({
|
|
2012
|
+
name: "eigenflux",
|
|
2013
|
+
description: "EigenFlux plugin commands: auth, profile, servers, feed, pm, here, version",
|
|
2014
|
+
acceptsArgs: true,
|
|
2015
|
+
handler: async (ctx) => {
|
|
2016
|
+
const parsed = parseCommandArgs(ctx.args);
|
|
2017
|
+
if (parsed.command === "version") {
|
|
2018
|
+
return {
|
|
2019
|
+
text: await buildVersionText(pluginConfig.eigenfluxBin)
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
const { runtimes, notInstalledBin } = await ensureRuntimes();
|
|
2023
|
+
if (notInstalledBin && runtimes.length === 0) {
|
|
2024
|
+
return {
|
|
2025
|
+
text: `EigenFlux CLI not installed (bin=${notInstalledBin}). Install with: ${INSTALL_COMMAND}`
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
if (parsed.command === "servers") {
|
|
2029
|
+
return {
|
|
2030
|
+
text: buildServersText(runtimes)
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
const selection = selectServerRuntime(runtimes, parsed.serverName);
|
|
2034
|
+
if (!selection.runtime) {
|
|
2035
|
+
return {
|
|
2036
|
+
text: selection.error ?? buildHelpText(runtimes)
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
const runtime = selection.runtime;
|
|
2040
|
+
await rememberCurrentCommandRouteIfPossible(ctx, runtime, store, logger);
|
|
2041
|
+
switch (parsed.command) {
|
|
2042
|
+
case "auth":
|
|
2043
|
+
return {
|
|
2044
|
+
text: buildAuthStatusText(runtime)
|
|
2045
|
+
};
|
|
2046
|
+
case "profile":
|
|
2047
|
+
return {
|
|
2048
|
+
text: await buildProfileText(runtime, pluginConfig.eigenfluxBin)
|
|
2049
|
+
};
|
|
2050
|
+
case "feed":
|
|
2051
|
+
return {
|
|
2052
|
+
text: await buildFeedText(runtime)
|
|
2053
|
+
};
|
|
2054
|
+
case "pm":
|
|
2055
|
+
return {
|
|
2056
|
+
text: buildPmStatusText(runtime)
|
|
2057
|
+
};
|
|
2058
|
+
case "here":
|
|
2059
|
+
return {
|
|
2060
|
+
text: await buildHereText(ctx, runtime, store, logger)
|
|
2061
|
+
};
|
|
2062
|
+
default:
|
|
2063
|
+
return {
|
|
2064
|
+
text: buildHelpText(runtimes)
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
453
2067
|
}
|
|
454
|
-
|
|
455
|
-
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
function parseCommandArgs(args) {
|
|
2071
|
+
const tokens = args?.trim().length ? args.trim().split(/\s+/u) : [];
|
|
2072
|
+
let serverName;
|
|
2073
|
+
const filtered = [];
|
|
2074
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
2075
|
+
const token = tokens[index];
|
|
2076
|
+
if ((token === "--server" || token === "-s") && tokens[index + 1]) {
|
|
2077
|
+
serverName = tokens[index + 1];
|
|
2078
|
+
index += 1;
|
|
2079
|
+
continue;
|
|
456
2080
|
}
|
|
2081
|
+
filtered.push(token);
|
|
2082
|
+
}
|
|
2083
|
+
const command = filtered[0]?.toLowerCase() ?? "";
|
|
2084
|
+
return {
|
|
2085
|
+
command,
|
|
2086
|
+
serverName
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
function selectServerRuntime(runtimes, requestedServerName) {
|
|
2090
|
+
if (runtimes.length === 0) {
|
|
2091
|
+
return {
|
|
2092
|
+
error: "No EigenFlux servers discovered. Ensure eigenflux CLI is configured with at least one server."
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
if (!requestedServerName) {
|
|
2096
|
+
return {
|
|
2097
|
+
runtime: runtimes[0]
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
const normalizedRequestedName = requestedServerName.trim().toLowerCase();
|
|
2101
|
+
const runtime = runtimes.find(
|
|
2102
|
+
(item) => item.server.name.trim().toLowerCase() === normalizedRequestedName
|
|
2103
|
+
);
|
|
2104
|
+
if (runtime) {
|
|
2105
|
+
return { runtime };
|
|
2106
|
+
}
|
|
2107
|
+
return {
|
|
2108
|
+
error: [
|
|
2109
|
+
`Unknown EigenFlux server: ${requestedServerName}`,
|
|
2110
|
+
`Available servers: ${runtimes.map((item) => item.server.name).join(", ")}`
|
|
2111
|
+
].join("\n")
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function buildServersText(runtimes) {
|
|
2115
|
+
if (runtimes.length === 0) {
|
|
2116
|
+
return "No EigenFlux servers discovered.";
|
|
2117
|
+
}
|
|
2118
|
+
return [
|
|
2119
|
+
"EigenFlux servers (discovered via CLI):",
|
|
2120
|
+
...runtimes.map((runtime) => {
|
|
2121
|
+
const flags = [
|
|
2122
|
+
runtime.server.current ? "default" : null,
|
|
2123
|
+
runtime.streamClient.isRunning() ? "streaming" : null
|
|
2124
|
+
].filter(Boolean).join(", ");
|
|
2125
|
+
const suffix = flags ? ` (${flags})` : "";
|
|
2126
|
+
return `- ${runtime.server.name}: endpoint=${runtime.server.endpoint}${suffix}`;
|
|
2127
|
+
})
|
|
2128
|
+
].join("\n");
|
|
2129
|
+
}
|
|
2130
|
+
function buildHelpText(runtimes) {
|
|
2131
|
+
const defaultRuntime = runtimes[0];
|
|
2132
|
+
const availableCommands = Array.from(COMMAND_NAME_SET).join("|");
|
|
2133
|
+
return [
|
|
2134
|
+
`Usage: /eigenflux [--server <name>] <${availableCommands}>`,
|
|
2135
|
+
defaultRuntime ? `Default server: ${defaultRuntime.server.name}` : void 0,
|
|
2136
|
+
runtimes.length > 0 ? `Available servers: ${runtimes.map((runtime) => runtime.server.name).join(", ")}` : void 0,
|
|
2137
|
+
"",
|
|
2138
|
+
"/eigenflux auth \u2014 Show credential status",
|
|
2139
|
+
"/eigenflux profile \u2014 Fetch agent profile",
|
|
2140
|
+
"/eigenflux servers \u2014 List discovered servers",
|
|
2141
|
+
"/eigenflux feed \u2014 Run one feed refresh",
|
|
2142
|
+
"/eigenflux pm \u2014 Show PM stream status",
|
|
2143
|
+
"/eigenflux here \u2014 Remember current conversation as delivery route",
|
|
2144
|
+
"/eigenflux version \u2014 Show eigenflux CLI version info"
|
|
2145
|
+
].filter(Boolean).join("\n");
|
|
2146
|
+
}
|
|
2147
|
+
function readNonEmptyString5(value) {
|
|
2148
|
+
if (typeof value !== "string") {
|
|
2149
|
+
return void 0;
|
|
2150
|
+
}
|
|
2151
|
+
const trimmed = value.trim();
|
|
2152
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2153
|
+
}
|
|
2154
|
+
function normalizeChannel3(value) {
|
|
2155
|
+
return readNonEmptyString5(value)?.toLowerCase();
|
|
2156
|
+
}
|
|
2157
|
+
async function resolveCurrentCommandRoute(ctx, runtime, logger) {
|
|
2158
|
+
let channel = normalizeChannel3(ctx.channel);
|
|
2159
|
+
let to = normalizeReplyTarget(ctx.to, { channel }) ?? normalizeReplyTarget(ctx.from, { channel, fallbackKind: "user" });
|
|
2160
|
+
let accountId = readNonEmptyString5(ctx.accountId);
|
|
2161
|
+
if (typeof ctx.getCurrentConversationBinding === "function") {
|
|
2162
|
+
try {
|
|
2163
|
+
const binding = await ctx.getCurrentConversationBinding();
|
|
2164
|
+
if (binding) {
|
|
2165
|
+
channel = normalizeChannel3(binding.channel) ?? channel;
|
|
2166
|
+
to = normalizeReplyTarget(binding.conversationId, { channel }) ?? normalizeReplyTarget(binding.parentConversationId, { channel }) ?? to;
|
|
2167
|
+
accountId = readNonEmptyString5(binding.accountId) ?? accountId;
|
|
2168
|
+
}
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
logger.debug(
|
|
2171
|
+
`Failed to read current conversation binding: ${error instanceof Error ? error.message : String(error)}`
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (!channel || !to) {
|
|
2176
|
+
return void 0;
|
|
2177
|
+
}
|
|
2178
|
+
return findSessionRouteForBinding(
|
|
2179
|
+
{
|
|
2180
|
+
agentId: runtime.routing.agentId,
|
|
2181
|
+
channel,
|
|
2182
|
+
to,
|
|
2183
|
+
accountId
|
|
2184
|
+
},
|
|
2185
|
+
logger
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
async function buildHereText(ctx, runtime, store, logger) {
|
|
2189
|
+
const route = await resolveCurrentCommandRoute(ctx, runtime, logger);
|
|
2190
|
+
if (!route || !route.replyChannel || !route.replyTo) {
|
|
457
2191
|
return [
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
2192
|
+
`Unable to resolve the current external session for server=${runtime.server.name}.`,
|
|
2193
|
+
"Run `/eigenflux here` inside the target conversation after OpenClaw has already created a session for it."
|
|
2194
|
+
].join("\n");
|
|
2195
|
+
}
|
|
2196
|
+
const saved = await writeStoredNotificationRoute(store, runtime.server.name, route, logger);
|
|
2197
|
+
if (!saved) {
|
|
2198
|
+
return `Failed to persist the current EigenFlux route for server=${runtime.server.name}; check plugin logs for details.`;
|
|
2199
|
+
}
|
|
2200
|
+
return [
|
|
2201
|
+
`EigenFlux server ${runtime.server.name} will deliver to this conversation by default:`,
|
|
2202
|
+
`sessionKey: ${route.sessionKey}`,
|
|
2203
|
+
`agentId: ${route.agentId}`,
|
|
2204
|
+
`channel: ${route.replyChannel ?? "unknown"}`,
|
|
2205
|
+
`target: ${route.replyTo ?? "unknown"}`,
|
|
2206
|
+
route.replyAccountId ? `account: ${route.replyAccountId}` : void 0
|
|
2207
|
+
].filter(Boolean).join("\n");
|
|
2208
|
+
}
|
|
2209
|
+
async function rememberCurrentCommandRouteIfPossible(ctx, runtime, store, logger) {
|
|
2210
|
+
const route = await resolveCurrentCommandRoute(ctx, runtime, logger);
|
|
2211
|
+
if (!route || !route.replyChannel || !route.replyTo) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (await writeStoredNotificationRoute(store, runtime.server.name, route, logger)) {
|
|
2215
|
+
logger.debug(
|
|
2216
|
+
`Remembered current command route for server=${runtime.server.name}: session_key=${route.sessionKey}, channel=${route.replyChannel ?? "unknown"}, to=${route.replyTo ?? "unknown"}`
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function buildAuthStatusText(runtime) {
|
|
2221
|
+
const authState = runtime.credentialsLoader.loadAuthState();
|
|
2222
|
+
const lines = [`EigenFlux auth status (server=${runtime.server.name}):`];
|
|
2223
|
+
lines.push(`- credentials_path: ${authState.credentialsPath}`);
|
|
2224
|
+
lines.push(`- status: ${authState.status}`);
|
|
2225
|
+
if (authState.expiresAt) {
|
|
2226
|
+
lines.push(`- expires_at: ${authState.expiresAt}`);
|
|
2227
|
+
}
|
|
2228
|
+
if (authState.status === "available") {
|
|
2229
|
+
lines.push(`- token: ${maskToken(authState.accessToken)}`);
|
|
2230
|
+
} else {
|
|
2231
|
+
lines.push("- token: unavailable");
|
|
2232
|
+
}
|
|
2233
|
+
return lines.join("\n");
|
|
2234
|
+
}
|
|
2235
|
+
async function buildProfileText(runtime, eigenfluxBin) {
|
|
2236
|
+
const result = await execEigenflux(
|
|
2237
|
+
eigenfluxBin,
|
|
2238
|
+
["profile", "show", "-s", runtime.server.name, "-f", "json"]
|
|
2239
|
+
);
|
|
2240
|
+
if (result.kind === "auth_required") {
|
|
2241
|
+
return buildAuthRequiredPromptTemplate({ context: runtime.getPromptContext() });
|
|
2242
|
+
}
|
|
2243
|
+
if (result.kind === "not_installed") {
|
|
2244
|
+
return `EigenFlux CLI not installed (bin=${result.bin}). Install with: ${INSTALL_COMMAND}`;
|
|
2245
|
+
}
|
|
2246
|
+
if (result.kind === "error") {
|
|
2247
|
+
return `Failed to fetch profile for server ${runtime.server.name}: ${result.error.message}`;
|
|
2248
|
+
}
|
|
2249
|
+
return [
|
|
2250
|
+
`EigenFlux profile (server=${runtime.server.name}):`,
|
|
2251
|
+
"```json",
|
|
2252
|
+
safeJsonStringify(result.data),
|
|
2253
|
+
"```"
|
|
2254
|
+
].join("\n");
|
|
463
2255
|
}
|
|
464
2256
|
async function buildFeedText(runtime) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
2257
|
+
const result = await runtime.feedPoller.pollOnce({
|
|
2258
|
+
notifyFeed: false,
|
|
2259
|
+
notifyAuthRequired: false
|
|
2260
|
+
});
|
|
2261
|
+
switch (result.kind) {
|
|
2262
|
+
case "success":
|
|
2263
|
+
return [
|
|
2264
|
+
`EigenFlux feed result (server=${runtime.server.name}):`,
|
|
2265
|
+
"```json",
|
|
2266
|
+
safeJsonStringify(result.payload),
|
|
2267
|
+
"```"
|
|
2268
|
+
].join("\n");
|
|
2269
|
+
case "auth_required":
|
|
2270
|
+
return buildAuthRequiredPromptTemplate({ context: runtime.getPromptContext() });
|
|
2271
|
+
case "error":
|
|
2272
|
+
return `EigenFlux feed failed for server ${runtime.server.name}: ${result.error.message}`;
|
|
2273
|
+
default:
|
|
2274
|
+
return `EigenFlux feed finished with an unknown result for server ${runtime.server.name}.`;
|
|
2275
|
+
}
|
|
484
2276
|
}
|
|
485
2277
|
async function buildVersionText(eigenfluxBin) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
2278
|
+
const result = await execEigenflux(eigenfluxBin, ["version"]);
|
|
2279
|
+
if (result.kind === "not_installed") {
|
|
2280
|
+
return `EigenFlux CLI not installed (bin=${result.bin}). Install with: ${INSTALL_COMMAND}`;
|
|
2281
|
+
}
|
|
2282
|
+
if (result.kind === "auth_required") {
|
|
2283
|
+
return `EigenFlux CLI reported auth_required while fetching version (stderr: ${result.stderr || "n/a"}).`;
|
|
2284
|
+
}
|
|
2285
|
+
if (result.kind === "error") {
|
|
2286
|
+
return `Failed to fetch eigenflux version: ${result.error.message}`;
|
|
2287
|
+
}
|
|
2288
|
+
const body = typeof result.data === "string" ? result.data : safeJsonStringify(result.data);
|
|
2289
|
+
return ["EigenFlux CLI version:", "```json", body, "```"].join("\n");
|
|
498
2290
|
}
|
|
499
2291
|
function buildPmStatusText(runtime) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
2292
|
+
const running = runtime.streamClient.isRunning();
|
|
2293
|
+
const cursor = runtime.streamClient.getLastCursor();
|
|
2294
|
+
const lines = [`EigenFlux PM stream status (server=${runtime.server.name}):`];
|
|
2295
|
+
lines.push(`- streaming: ${running ? "active" : "inactive"}`);
|
|
2296
|
+
if (cursor) {
|
|
2297
|
+
lines.push(`- last_cursor: ${cursor}`);
|
|
2298
|
+
}
|
|
2299
|
+
if (!running) {
|
|
2300
|
+
lines.push("PM stream is not running. Check auth status or restart the service.");
|
|
2301
|
+
}
|
|
2302
|
+
return lines.join("\n");
|
|
2303
|
+
}
|
|
2304
|
+
function createInMemoryPluginStore() {
|
|
2305
|
+
const data = /* @__PURE__ */ new Map();
|
|
2306
|
+
return {
|
|
2307
|
+
async get(key) {
|
|
2308
|
+
return data.get(key);
|
|
2309
|
+
},
|
|
2310
|
+
async set(key, value) {
|
|
2311
|
+
data.set(key, value);
|
|
509
2312
|
}
|
|
510
|
-
|
|
2313
|
+
};
|
|
511
2314
|
}
|
|
512
|
-
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
513
2315
|
function maskToken(token) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
2316
|
+
const trimmed = token.trim();
|
|
2317
|
+
if (trimmed.length <= 10) {
|
|
2318
|
+
return `${trimmed.slice(0, 2)}***`;
|
|
2319
|
+
}
|
|
2320
|
+
return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
|
|
519
2321
|
}
|
|
520
2322
|
function safeJsonStringify(value) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
2323
|
+
try {
|
|
2324
|
+
return JSON.stringify(value, null, 2);
|
|
2325
|
+
} catch {
|
|
2326
|
+
return String(value);
|
|
2327
|
+
}
|
|
527
2328
|
}
|
|
528
|
-
//# sourceMappingURL=index.js.map
|