@phronesis-io/openclaw-eigenflux 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +28 -0
  2. package/dist/index.d.ts +9 -23
  3. package/dist/index.js +2265 -457
  4. package/openclaw.plugin.json +5 -1
  5. package/package.json +21 -8
  6. package/dist/agent-prompt-templates.d.ts +0 -19
  7. package/dist/agent-prompt-templates.d.ts.map +0 -1
  8. package/dist/agent-prompt-templates.js +0 -56
  9. package/dist/agent-prompt-templates.js.map +0 -1
  10. package/dist/cli-executor.d.ts +0 -32
  11. package/dist/cli-executor.d.ts.map +0 -1
  12. package/dist/cli-executor.js +0 -75
  13. package/dist/cli-executor.js.map +0 -1
  14. package/dist/config.d.ts +0 -83
  15. package/dist/config.d.ts.map +0 -1
  16. package/dist/config.js +0 -226
  17. package/dist/config.js.map +0 -1
  18. package/dist/credentials-loader.d.ts +0 -29
  19. package/dist/credentials-loader.d.ts.map +0 -1
  20. package/dist/credentials-loader.js +0 -117
  21. package/dist/credentials-loader.js.map +0 -1
  22. package/dist/index.d.ts.map +0 -1
  23. package/dist/index.js.map +0 -1
  24. package/dist/logger.d.ts +0 -12
  25. package/dist/logger.d.ts.map +0 -1
  26. package/dist/logger.js +0 -25
  27. package/dist/logger.js.map +0 -1
  28. package/dist/notification-route-resolver.d.ts +0 -66
  29. package/dist/notification-route-resolver.d.ts.map +0 -1
  30. package/dist/notification-route-resolver.js +0 -603
  31. package/dist/notification-route-resolver.js.map +0 -1
  32. package/dist/notifier.d.ts +0 -39
  33. package/dist/notifier.d.ts.map +0 -1
  34. package/dist/notifier.js +0 -335
  35. package/dist/notifier.js.map +0 -1
  36. package/dist/polling-client.d.ts +0 -86
  37. package/dist/polling-client.d.ts.map +0 -1
  38. package/dist/polling-client.js +0 -158
  39. package/dist/polling-client.js.map +0 -1
  40. package/dist/reply-target.d.ts +0 -8
  41. package/dist/reply-target.d.ts.map +0 -1
  42. package/dist/reply-target.js +0 -104
  43. package/dist/reply-target.js.map +0 -1
  44. package/dist/session-route-memory.d.ts +0 -22
  45. package/dist/session-route-memory.d.ts.map +0 -1
  46. package/dist/session-route-memory.js +0 -117
  47. package/dist/session-route-memory.js.map +0 -1
  48. package/dist/stream-client.d.ts +0 -48
  49. package/dist/stream-client.d.ts.map +0 -1
  50. package/dist/stream-client.js +0 -168
  51. package/dist/stream-client.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,520 +1,2328 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const polling_client_1 = require("./polling-client");
4
- const stream_client_1 = require("./stream-client");
5
- const cli_executor_1 = require("./cli-executor");
6
- const logger_1 = require("./logger");
7
- const credentials_loader_1 = require("./credentials-loader");
8
- const config_1 = require("./config");
9
- const notification_route_resolver_1 = require("./notification-route-resolver");
10
- const agent_prompt_templates_1 = require("./agent-prompt-templates");
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
- function register(api) {
28
- const logger = new logger_1.Logger(resolvePluginLogger(api));
29
- const pluginConfig = (0, config_1.resolvePluginConfig)(api.pluginConfig, logger);
30
- const eigenfluxHome = (0, config_1.resolveEigenfluxHome)();
31
- let runtimes = [];
32
- let notInstalledPromptDelivered = false;
33
- // Register a single meta-service that discovers servers on start
34
- api.registerService({
35
- id: 'eigenflux:discovery',
36
- start: async () => {
37
- logger.info('Starting EigenFlux discovery service...');
38
- const discovery = await (0, config_1.discoverServers)(pluginConfig.eigenfluxBin, logger);
39
- if (discovery.kind === 'not_installed') {
40
- logger.warn(`EigenFlux CLI not installed (bin=${discovery.bin}); delivering install prompt to user`);
41
- if (!notInstalledPromptDelivered) {
42
- notInstalledPromptDelivered = true;
43
- await deliverNotInstalledPrompt(api, logger, pluginConfig, eigenfluxHome, discovery.bin);
44
- }
45
- return;
46
- }
47
- const servers = discovery.servers;
48
- if (servers.length === 0) {
49
- logger.warn('No EigenFlux servers discovered; services will not start');
50
- return;
51
- }
52
- logger.info(`Discovered ${servers.length} server(s): ${servers.map((s) => s.name).join(', ')}`);
53
- runtimes = servers.map((server) => createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome));
54
- for (const runtime of runtimes) {
55
- logger.info(`Starting services for server=${runtime.server.name}`);
56
- await runtime.feedPoller.start();
57
- await runtime.streamClient.start();
58
- }
59
- },
60
- stop: async () => {
61
- logger.info('Stopping EigenFlux discovery service...');
62
- for (const runtime of runtimes) {
63
- logger.info(`Stopping services for server=${runtime.server.name}`);
64
- runtime.feedPoller.stop();
65
- await runtime.streamClient.stop();
66
- }
67
- runtimes = [];
68
- notInstalledPromptDelivered = false;
69
- },
70
- });
71
- registerCommand(api, logger, pluginConfig, eigenfluxHome, () => runtimes, (next) => {
72
- runtimes = next;
73
- });
74
- }
75
- function resolvePluginLogger(api) {
76
- const runtimeLogging = api.runtime?.logging;
77
- if (runtimeLogging && typeof runtimeLogging.getChildLogger === 'function') {
78
- try {
79
- const child = runtimeLogging.getChildLogger({ plugin: 'eigenflux' });
80
- if (child) {
81
- return child;
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
- catch {
85
- // fall through to api.logger
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
- return api.logger;
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
- const plugin = {
91
- id: 'openclaw-eigenflux',
92
- name: 'EigenFlux',
93
- description: 'OpenClaw extension for EigenFlux with CLI-based feed polling and PM streaming',
94
- configSchema: config_1.PLUGIN_CONFIG_SCHEMA,
95
- register,
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
- exports.default = plugin;
98
- const INSTALL_COMMAND = 'curl -fsSL https://eigenflux.ai/install.sh | bash';
99
- async function deliverNotInstalledPrompt(api, logger, pluginConfig, _eigenfluxHome, bin) {
100
- // Intentionally no workdir: the bootstrap notifier must not read or persist
101
- // any remembered session route under <eigenfluxHome>/bootstrap.
102
- const notifier = new notifier_1.EigenFluxNotifier(api, logger, {
103
- sessionKey: DEFAULT_ROUTING.sessionKey,
104
- agentId: DEFAULT_ROUTING.agentId,
105
- replyChannel: DEFAULT_ROUTING.replyChannel,
106
- replyTo: DEFAULT_ROUTING.replyTo,
107
- replyAccountId: DEFAULT_ROUTING.replyAccountId,
108
- openclawCliBin: pluginConfig.openclawCliBin,
109
- routeOverrides: DEFAULT_ROUTING.routeOverrides,
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
- await notifier.deliver((0, agent_prompt_templates_1.buildNotInstalledPromptTemplate)({ bin, installCommand: INSTALL_COMMAND }));
112
- }
113
- function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome) {
114
- const routing = pluginConfig.serverRouting[server.name] ?? DEFAULT_ROUTING;
115
- const credentialsLoader = new credentials_loader_1.CredentialsLoader(logger, eigenfluxHome, server.name);
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
- const getPromptContext = () => ({
128
- serverName: server.name,
129
- eigenfluxHome,
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
- let lastAuthPromptKey = null;
132
- const resetAuthPromptGate = () => {
133
- lastAuthPromptKey = null;
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
- const streamClient = new stream_client_1.EigenFluxStreamClient({
156
- serverName: server.name,
157
- eigenfluxBin: pluginConfig.eigenfluxBin,
158
- logger,
159
- onPmEvent: async (event) => {
160
- resetAuthPromptGate();
161
- await notifier.deliver((0, agent_prompt_templates_1.buildPmStreamEventPromptTemplate)(event, getPromptContext()));
162
- },
163
- onAuthRequired: async () => {
164
- await notifyAuthRequired({ reason: 'auth_required' });
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
- server,
169
- routing,
170
- credentialsLoader,
171
- notifier,
172
- feedPoller,
173
- streamClient,
174
- getPromptContext,
510
+ status: "missing",
511
+ credentialsPath: this.credentialsPath
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
175
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
- // ─── Command Handler ────────────────────────────────────────────────────────
178
- function registerCommand(api, logger, pluginConfig, eigenfluxHome, getRuntimes, setRuntimes) {
179
- if (!api.registerCommand) {
180
- logger.warn('registerCommand API unavailable; skipping /eigenflux command registration');
181
- return;
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
- let inflightDiscovery = null;
184
- const runDiscovery = async () => {
185
- const discovery = await (0, config_1.discoverServers)(pluginConfig.eigenfluxBin, logger);
186
- if (discovery.kind === 'not_installed') {
187
- return { runtimes: getRuntimes(), notInstalledBin: discovery.bin };
188
- }
189
- if (discovery.servers.length === 0) {
190
- return { runtimes: getRuntimes() };
191
- }
192
- const created = discovery.servers.map((server) => createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome));
193
- setRuntimes(created);
194
- return { runtimes: created };
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";
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
- const ensureRuntimes = async () => {
197
- const existing = getRuntimes();
198
- if (existing.length > 0) {
199
- return { runtimes: existing };
200
- }
201
- if (!inflightDiscovery) {
202
- inflightDiscovery = runDiscovery().finally(() => {
203
- inflightDiscovery = null;
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
- api.registerCommand({
209
- name: 'eigenflux',
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 parseCommandArgs(args) {
268
- const tokens = args?.trim().length ? args.trim().split(/\s+/u) : [];
269
- let serverName;
270
- const filtered = [];
271
- for (let index = 0; index < tokens.length; index += 1) {
272
- const token = tokens[index];
273
- if ((token === '--server' || token === '-s') && tokens[index + 1]) {
274
- serverName = tokens[index + 1];
275
- index += 1;
276
- continue;
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
- filtered.push(token);
1152
+ addPath(path3.join(agentsRoot, entry.name, "sessions", "sessions.json"));
1153
+ }
279
1154
  }
280
- const command = filtered[0]?.toLowerCase() ?? '';
281
- return {
282
- command,
283
- serverName,
284
- };
1155
+ } catch {
1156
+ }
1157
+ return candidates;
285
1158
  }
286
- function selectServerRuntime(runtimes, requestedServerName) {
287
- if (runtimes.length === 0) {
288
- return {
289
- error: 'No EigenFlux servers discovered. Ensure eigenflux CLI is configured with at least one server.',
290
- };
1159
+ function readSessionStore(sessionStorePath, logger) {
1160
+ try {
1161
+ if (!fs2.existsSync(sessionStorePath)) {
1162
+ return void 0;
291
1163
  }
292
- if (!requestedServerName) {
293
- return {
294
- runtime: runtimes[0],
295
- };
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;
296
1168
  }
297
- const normalizedRequestedName = requestedServerName.trim().toLowerCase();
298
- const runtime = runtimes.find((item) => item.server.name.trim().toLowerCase() === normalizedRequestedName);
299
- if (runtime) {
300
- return { runtime };
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 });
301
1191
  }
302
- return {
303
- error: [
304
- `Unknown EigenFlux server: ${requestedServerName}`,
305
- `Available servers: ${runtimes.map((item) => item.server.name).join(', ')}`,
306
- ].join('\n'),
307
- };
1192
+ }
1193
+ return snapshots;
308
1194
  }
309
- function buildServersText(runtimes) {
310
- if (runtimes.length === 0) {
311
- return 'No EigenFlux servers discovered.';
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;
312
1212
  }
313
- return [
314
- 'EigenFlux servers (discovered via CLI):',
315
- ...runtimes.map((runtime) => {
316
- const flags = [
317
- runtime.server.current ? 'default' : null,
318
- runtime.streamClient.isRunning() ? 'streaming' : null,
319
- ]
320
- .filter(Boolean)
321
- .join(', ');
322
- const suffix = flags ? ` (${flags})` : '';
323
- return `- ${runtime.server.name}: endpoint=${runtime.server.endpoint}${suffix}`;
324
- }),
325
- ].join('\n');
1213
+ const updatedAt = normalizeUpdatedAt(entry?.updatedAt);
1214
+ if (!best || updatedAt > best.updatedAt) {
1215
+ best = { route, updatedAt };
1216
+ }
1217
+ }
1218
+ return best;
326
1219
  }
327
- function buildHelpText(runtimes) {
328
- const defaultRuntime = runtimes[0];
329
- const availableCommands = Array.from(COMMAND_NAME_SET).join('|');
330
- return [
331
- `Usage: /eigenflux [--server <name>] <${availableCommands}>`,
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');
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 };
347
1261
  }
348
- function readNonEmptyString(value) {
349
- if (typeof value !== 'string') {
350
- return undefined;
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)) {
1368
+ return {
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
+ );
351
1382
  }
352
- const trimmed = value.trim();
353
- return trimmed.length > 0 ? trimmed : undefined;
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" };
354
1395
  }
355
- function normalizeChannel(value) {
356
- return readNonEmptyString(value)?.toLowerCase();
1396
+
1397
+ // src/agent-prompt-templates.ts
1398
+ function buildContextLines(context) {
1399
+ return [
1400
+ `homedir=${context.eigenfluxHome}`,
1401
+ `server=${context.serverName}`
1402
+ ];
357
1403
  }
358
- async function resolveCurrentCommandRoute(ctx, runtime, logger) {
359
- let channel = normalizeChannel(ctx.channel);
360
- let to = (0, reply_target_1.normalizeReplyTarget)(ctx.to, { channel }) ??
361
- (0, reply_target_1.normalizeReplyTarget)(ctx.from, { channel, fallbackKind: 'user' });
362
- let accountId = readNonEmptyString(ctx.accountId);
363
- if (typeof ctx.getCurrentConversationBinding === 'function') {
364
- try {
365
- const binding = await ctx.getCurrentConversationBinding();
366
- if (binding) {
367
- channel = normalizeChannel(binding.channel) ?? channel;
368
- to =
369
- (0, reply_target_1.normalizeReplyTarget)(binding.conversationId, { channel }) ??
370
- (0, reply_target_1.normalizeReplyTarget)(binding.parentConversationId, { channel }) ??
371
- to;
372
- accountId = readNonEmptyString(binding.accountId) ?? accountId;
373
- }
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;
374
1493
  }
375
- catch (error) {
376
- logger.debug(`Failed to read current conversation binding: ${error instanceof Error ? error.message : String(error)}`);
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
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
+ };
377
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
+ };
378
1579
  }
379
- if (!channel || !to) {
380
- return undefined;
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
+ };
381
1596
  }
382
- return (0, notification_route_resolver_1.findSessionRouteForBinding)({
383
- agentId: runtime.routing.agentId,
384
- channel,
385
- to,
386
- accountId,
387
- }, logger);
388
- }
389
- async function buildHereText(ctx, runtime, eigenfluxBin, logger) {
390
- const route = await resolveCurrentCommandRoute(ctx, runtime, logger);
391
- if (!route || !route.replyChannel || !route.replyTo) {
392
- return [
393
- `Unable to resolve the current external session for server=${runtime.server.name}.`,
394
- 'Run `/eigenflux here` inside the target conversation after OpenClaw has already created a session for it.',
395
- ].join('\n');
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
+ };
396
1624
  }
397
- const saved = await (0, session_route_memory_1.writeStoredNotificationRoute)(eigenfluxBin, runtime.server.name, route, logger);
398
- if (!saved) {
399
- return `Failed to persist the current EigenFlux route for server=${runtime.server.name}; check plugin logs for details.`;
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
+ };
400
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) {
1649
+ return {
1650
+ ok: true,
1651
+ mode,
1652
+ sessionKey: route.sessionKey
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);
1680
+ }
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) {
401
1690
  return [
402
- `EigenFlux server ${runtime.server.name} will deliver to this conversation by default:`,
403
- `sessionKey: ${route.sessionKey}`,
404
- `agentId: ${route.agentId}`,
405
- `channel: ${route.replyChannel ?? 'unknown'}`,
406
- `target: ${route.replyTo ?? 'unknown'}`,
407
- route.replyAccountId ? `account: ${route.replyAccountId}` : undefined,
408
- ]
409
- .filter(Boolean)
410
- .join('\n');
411
- }
412
- async function rememberCurrentCommandRouteIfPossible(ctx, runtime, eigenfluxBin, logger) {
413
- const route = await resolveCurrentCommandRoute(ctx, runtime, logger);
414
- if (!route || !route.replyChannel || !route.replyTo) {
415
- 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;
416
1703
  }
417
- if (await (0, session_route_memory_1.writeStoredNotificationRoute)(eigenfluxBin, runtime.server.name, route, logger)) {
418
- logger.debug(`Remembered current command route for server=${runtime.server.name}: session_key=${route.sessionKey}, channel=${route.replyChannel ?? 'unknown'}, to=${route.replyTo ?? 'unknown'}`);
1704
+ return {
1705
+ ...route.replyChannel ? { channel: route.replyChannel } : {},
1706
+ ...route.replyTo ? { to: route.replyTo } : {},
1707
+ ...route.replyAccountId ? { accountId: route.replyAccountId } : {}
1708
+ };
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;
419
1724
  }
420
- }
421
- // ─── Command Handlers ───────────────────────────────────────────────────────
422
- function buildAuthStatusText(runtime) {
423
- const authState = runtime.credentialsLoader.loadAuthState();
424
- const lines = [`EigenFlux auth status (server=${runtime.server.name}):`];
425
- lines.push(`- credentials_path: ${authState.credentialsPath}`);
426
- lines.push(`- status: ${authState.status}`);
427
- if (authState.expiresAt) {
428
- lines.push(`- expires_at: ${authState.expiresAt}`);
1725
+ if (isInternalSessionKey(route.sessionKey)) {
1726
+ return;
429
1727
  }
430
- if (authState.status === 'available') {
431
- lines.push(`- token: ${maskToken(authState.accessToken)}`);
1728
+ if (!route.replyChannel || !route.replyTo) {
1729
+ return;
432
1730
  }
433
- else {
434
- lines.push('- token: unavailable');
1731
+ if (source === "remembered") {
1732
+ this.logger.debug(
1733
+ `Skipping remembered-route write; route came from config (session_key=${route.sessionKey})`
1734
+ );
1735
+ return;
435
1736
  }
436
- return lines.join('\n');
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"}`;
437
1756
  }
438
- async function buildProfileText(runtime, eigenfluxBin) {
439
- const result = await (0, cli_executor_1.execEigenflux)(eigenfluxBin, ['profile', 'show', '-s', runtime.server.name, '-f', 'json']);
440
- if (result.kind === 'auth_required') {
441
- return (0, agent_prompt_templates_1.buildAuthRequiredPromptTemplate)({ context: runtime.getPromptContext() });
1757
+ function formatError(error) {
1758
+ if (error instanceof Error) {
1759
+ return `${error.name}: ${error.message}`;
1760
+ }
1761
+ return String(error);
1762
+ }
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(", ");
1771
+ }
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)}...`);
1788
+ }
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
+ }
442
1796
  }
443
- if (result.kind === 'not_installed') {
444
- return `EigenFlux CLI not installed (bin=${result.bin}). Install with: ${INSTALL_COMMAND}`;
1797
+ }
1798
+ return JSON.stringify(sanitized);
1799
+ }
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);
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;
445
1861
  }
446
- if (result.kind === 'error') {
447
- return `Failed to fetch profile for server ${runtime.server.name}: ${result.error.message}`;
1862
+ });
1863
+ registerCommand(
1864
+ api,
1865
+ logger,
1866
+ pluginConfig,
1867
+ eigenfluxHome,
1868
+ store,
1869
+ () => runtimes,
1870
+ (next) => {
1871
+ runtimes = next;
448
1872
  }
449
- return [
450
- `EigenFlux profile (server=${runtime.server.name}):`,
451
- '```json',
452
- safeJsonStringify(result.data),
453
- '```',
454
- ].join('\n');
1873
+ );
455
1874
  }
456
- async function buildFeedText(runtime) {
457
- const result = await runtime.feedPoller.pollOnce({
458
- notifyFeed: false,
459
- notifyAuthRequired: false,
460
- });
461
- switch (result.kind) {
462
- case 'success':
463
- return [
464
- `EigenFlux feed result (server=${runtime.server.name}):`,
465
- '```json',
466
- safeJsonStringify(result.payload),
467
- '```',
468
- ].join('\n');
469
- case 'auth_required':
470
- return (0, agent_prompt_templates_1.buildAuthRequiredPromptTemplate)({ context: runtime.getPromptContext() });
471
- case 'error':
472
- return `EigenFlux feed failed for server ${runtime.server.name}: ${result.error.message}`;
473
- default:
474
- return `EigenFlux feed finished with an unknown result for server ${runtime.server.name}.`;
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 {
475
1884
  }
1885
+ }
1886
+ return api.logger;
476
1887
  }
477
- async function buildVersionText(eigenfluxBin) {
478
- const result = await (0, cli_executor_1.execEigenflux)(eigenfluxBin, ['version']);
479
- if (result.kind === 'not_installed') {
480
- return `EigenFlux CLI not installed (bin=${result.bin}). Install with: ${INSTALL_COMMAND}`;
481
- }
482
- if (result.kind === 'auth_required') {
483
- return `EigenFlux CLI reported auth_required while fetching version (stderr: ${result.stderr || 'n/a'}).`;
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;
484
1940
  }
485
- if (result.kind === 'error') {
486
- return `Failed to fetch eigenflux version: ${result.error.message}`;
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" });
487
1967
  }
488
- const body = typeof result.data === 'string' ? result.data : safeJsonStringify(result.data);
489
- return ['EigenFlux CLI version:', '```json', body, '```'].join('\n');
1968
+ });
1969
+ return {
1970
+ server,
1971
+ routing,
1972
+ credentialsLoader,
1973
+ notifier,
1974
+ feedPoller,
1975
+ streamClient,
1976
+ getPromptContext
1977
+ };
490
1978
  }
491
- function buildPmStatusText(runtime) {
492
- const running = runtime.streamClient.isRunning();
493
- const cursor = runtime.streamClient.getLastCursor();
494
- const lines = [`EigenFlux PM stream status (server=${runtime.server.name}):`];
495
- lines.push(`- streaming: ${running ? 'active' : 'inactive'}`);
496
- if (cursor) {
497
- lines.push(`- last_cursor: ${cursor}`);
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 };
1989
+ }
1990
+ if (discovery.servers.length === 0) {
1991
+ return { runtimes: getRuntimes() };
1992
+ }
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 };
2003
+ }
2004
+ if (!inflightDiscovery) {
2005
+ inflightDiscovery = runDiscovery().finally(() => {
2006
+ inflightDiscovery = null;
2007
+ });
498
2008
  }
499
- if (!running) {
500
- lines.push('PM stream is not running. Check auth status or restart the service.');
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
+ }
501
2067
  }
502
- return lines.join('\n');
2068
+ });
503
2069
  }
504
- // ─── Utilities ──────────────────────────────────────────────────────────────
505
- function maskToken(token) {
506
- const trimmed = token.trim();
507
- if (trimmed.length <= 10) {
508
- return `${trimmed.slice(0, 2)}***`;
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;
509
2080
  }
510
- return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
2081
+ filtered.push(token);
2082
+ }
2083
+ const command = filtered[0]?.toLowerCase() ?? "";
2084
+ return {
2085
+ command,
2086
+ serverName
2087
+ };
511
2088
  }
512
- function safeJsonStringify(value) {
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") {
513
2162
  try {
514
- return JSON.stringify(value, null, 2);
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
+ );
515
2173
  }
516
- catch {
517
- return String(value);
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) {
2191
+ return [
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");
2255
+ }
2256
+ async function buildFeedText(runtime) {
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
+ }
2276
+ }
2277
+ async function buildVersionText(eigenfluxBin) {
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");
2290
+ }
2291
+ function buildPmStatusText(runtime) {
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);
518
2312
  }
2313
+ };
2314
+ }
2315
+ function maskToken(token) {
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)}`;
2321
+ }
2322
+ function safeJsonStringify(value) {
2323
+ try {
2324
+ return JSON.stringify(value, null, 2);
2325
+ } catch {
2326
+ return String(value);
2327
+ }
519
2328
  }
520
- //# sourceMappingURL=index.js.map