@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts DELETED
@@ -1,758 +0,0 @@
1
- import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
-
3
- import {
4
- EigenFluxPollingClient,
5
- AuthRequiredEvent,
6
- FeedResponse,
7
- } from './polling-client';
8
- import { EigenFluxPmPollingClient, PmFetchResponse } from './pm-polling-client';
9
- import { Logger } from './logger';
10
- import { AuthState, CredentialsLoader } from './credentials-loader';
11
- import {
12
- PLUGIN_CONFIG,
13
- PLUGIN_CONFIG_SCHEMA,
14
- resolvePluginConfig,
15
- resolveServerSkillPath,
16
- type ResolvedEigenFluxPluginConfig,
17
- type ResolvedEigenFluxServerConfig,
18
- } from './config';
19
- import { resolveNotificationRoute } from './notification-route-resolver';
20
- import {
21
- buildAuthRequiredPromptTemplate,
22
- buildFeedPayloadPromptTemplate,
23
- buildPmPayloadPromptTemplate,
24
- type EigenFluxPromptServerContext,
25
- } from './agent-prompt-templates';
26
- import { EigenFluxNotifier } from './notifier';
27
- import { writeStoredNotificationRoute } from './session-route-memory';
28
-
29
- type JsonRecord = Record<string, unknown>;
30
-
31
- type JsonApiSuccess<T extends JsonRecord> = {
32
- code: number;
33
- msg: string;
34
- data: T;
35
- };
36
-
37
- type ProfileResponseData = {
38
- agent: JsonRecord;
39
- profile: JsonRecord;
40
- influence: JsonRecord;
41
- };
42
-
43
- type AuthPromptContext = {
44
- authEvent: AuthRequiredEvent;
45
- authState?: AuthState;
46
- };
47
-
48
- type CommandRouteContext = {
49
- channel?: string;
50
- to?: string;
51
- from?: string;
52
- accountId?: string;
53
- getCurrentConversationBinding?: () => Promise<{
54
- channel: string;
55
- accountId: string;
56
- conversationId: string;
57
- parentConversationId?: string;
58
- } | null>;
59
- };
60
-
61
- type ServerRuntime = {
62
- server: ResolvedEigenFluxServerConfig;
63
- credentialsLoader: CredentialsLoader;
64
- notifier: EigenFluxNotifier;
65
- pollingClient: EigenFluxPollingClient;
66
- pmPollingClient: EigenFluxPmPollingClient;
67
- getPromptContext: () => EigenFluxPromptServerContext;
68
- };
69
-
70
- type ParsedCommandArgs = {
71
- command: string;
72
- serverName?: string;
73
- };
74
-
75
- type ServerRuntimeSelection = {
76
- runtime?: ServerRuntime;
77
- error?: string;
78
- };
79
-
80
- const COMMAND_NAMES = ['auth', 'profile', 'servers', 'feed', 'pm', 'here'] as const;
81
- const COMMAND_NAME_SET = new Set<string>(COMMAND_NAMES);
82
-
83
- function readServerSessionStorePath(
84
- server: ResolvedEigenFluxServerConfig
85
- ): string | undefined {
86
- return (server as ResolvedEigenFluxServerConfig & { sessionStorePath?: string })
87
- .sessionStorePath;
88
- }
89
-
90
- function register(api: OpenClawPluginApi): void {
91
- const logger = new Logger(api.logger);
92
- logger.info('EigenFlux activating...');
93
-
94
- const pluginConfig = resolvePluginConfig(api.pluginConfig, api.config as any);
95
- const runtimes = pluginConfig.servers.map((server) =>
96
- createServerRuntime(api, logger, pluginConfig, server)
97
- );
98
- const enabledRuntimes = runtimes.filter((runtime) => runtime.server.enabled);
99
-
100
- if (!pluginConfig.gatewayToken) {
101
- logger.warn(
102
- 'OpenClaw gateway token not found in config.gateway.auth.token or plugin config; Gateway RPC fallback may fail when gateway auth mode is token'
103
- );
104
- }
105
-
106
- if (enabledRuntimes.length === 0) {
107
- logger.warn('No enabled EigenFlux servers configured; background polling services will not start');
108
- }
109
-
110
- registerServices(api, logger, enabledRuntimes);
111
- registerCommand(api, logger, runtimes);
112
-
113
- logger.info(
114
- `EigenFlux activated with ${enabledRuntimes.length}/${runtimes.length} enabled server(s)`
115
- );
116
- }
117
-
118
- const plugin = {
119
- id: 'eigenflux',
120
- name: 'EigenFlux',
121
- description: 'OpenClaw extension for EigenFlux periodic polling with multi-server delivery',
122
- configSchema: PLUGIN_CONFIG_SCHEMA,
123
- register,
124
- };
125
-
126
- export default plugin;
127
-
128
- function createServerRuntime(
129
- api: OpenClawPluginApi,
130
- logger: Logger,
131
- pluginConfig: ResolvedEigenFluxPluginConfig,
132
- server: ResolvedEigenFluxServerConfig
133
- ): ServerRuntime {
134
- const credentialsLoader = new CredentialsLoader(logger, server.workdir);
135
- const notifier = new EigenFluxNotifier(api, logger, {
136
- gatewayUrl: pluginConfig.gatewayUrl,
137
- gatewayToken: pluginConfig.gatewayToken,
138
- workdir: server.workdir,
139
- sessionKey: server.sessionKey,
140
- agentId: server.agentId,
141
- replyChannel: server.replyChannel,
142
- replyTo: server.replyTo,
143
- replyAccountId: server.replyAccountId,
144
- openclawCliBin: pluginConfig.openclawCliBin,
145
- sessionStorePath: readServerSessionStorePath(server),
146
- routeOverrides: server.routeOverrides,
147
- });
148
-
149
- const getPromptContext = (): EigenFluxPromptServerContext => ({
150
- serverName: server.name,
151
- workdir: server.workdir,
152
- skillPath: resolveServerSkillPath(server),
153
- });
154
-
155
- let lastAuthPromptKey: string | null = null;
156
-
157
- const resetAuthPromptGate = (): void => {
158
- lastAuthPromptKey = null;
159
- };
160
-
161
- const notifyAuthRequired = async (authEvent: AuthRequiredEvent): Promise<void> => {
162
- const promptKey = `${authEvent.reason}:${authEvent.credentialsPath}:${authEvent.source || 'unknown'}`;
163
- if (lastAuthPromptKey === promptKey) {
164
- logger.debug(`Skipping duplicate auth prompt for server=${server.name}, key=${promptKey}`);
165
- return;
166
- }
167
-
168
- lastAuthPromptKey = promptKey;
169
- const authState = credentialsLoader.loadAuthState();
170
- await notifier.deliver(
171
- buildAuthRequiredMessage(getPromptContext(), {
172
- authEvent,
173
- authState,
174
- })
175
- );
176
- };
177
-
178
- const pollingClient = new EigenFluxPollingClient({
179
- apiUrl: server.endpoint,
180
- getAuthState: () => credentialsLoader.loadAuthState(),
181
- pollIntervalSec: server.pollIntervalSec,
182
- logger,
183
- onFeedPolled: async (payload: FeedResponse) => {
184
- resetAuthPromptGate();
185
- await notifier.deliver(buildFeedPayloadMessage(getPromptContext(), payload));
186
- },
187
- onAuthRequired: notifyAuthRequired,
188
- });
189
-
190
- const pmPollingClient = new EigenFluxPmPollingClient({
191
- apiUrl: server.endpoint,
192
- getAuthState: () => credentialsLoader.loadAuthState(),
193
- pollIntervalSec: server.pmPollIntervalSec,
194
- logger,
195
- onPmFetched: async (payload: PmFetchResponse) => {
196
- resetAuthPromptGate();
197
- await notifier.deliver(buildPmPayloadMessage(getPromptContext(), payload));
198
- },
199
- onAuthRequired: notifyAuthRequired,
200
- });
201
-
202
- return {
203
- server,
204
- credentialsLoader,
205
- notifier,
206
- pollingClient,
207
- pmPollingClient,
208
- getPromptContext,
209
- };
210
- }
211
-
212
- function registerServices(
213
- api: OpenClawPluginApi,
214
- logger: Logger,
215
- runtimes: ServerRuntime[]
216
- ): void {
217
- for (const runtime of runtimes) {
218
- api.registerService({
219
- id: `eigenflux:${toServiceIdSegment(runtime.server.name)}`,
220
- start: async () => {
221
- logger.info(`Starting EigenFlux polling services for server=${runtime.server.name}`);
222
- await runtime.pollingClient.start();
223
- await runtime.pmPollingClient.start();
224
- },
225
- stop: async () => {
226
- logger.info(`Stopping EigenFlux polling services for server=${runtime.server.name}`);
227
- runtime.pollingClient.stop();
228
- runtime.pmPollingClient.stop();
229
- },
230
- });
231
- }
232
- }
233
-
234
- function registerCommand(
235
- api: OpenClawPluginApi,
236
- logger: Logger,
237
- runtimes: ServerRuntime[]
238
- ): void {
239
- if (!api.registerCommand) {
240
- logger.warn('registerCommand API unavailable; skipping /eigenflux command registration');
241
- return;
242
- }
243
-
244
- api.registerCommand({
245
- name: 'eigenflux',
246
- description: 'EigenFlux plugin commands: auth, profile, servers, feed, pm, here',
247
- acceptsArgs: true,
248
- handler: async (ctx) => {
249
- const parsed = parseCommandArgs(ctx.args);
250
-
251
- if (parsed.command === 'servers') {
252
- return {
253
- text: buildServersText(runtimes),
254
- };
255
- }
256
-
257
- const selection = selectServerRuntime(runtimes, parsed.serverName);
258
- if (!selection.runtime) {
259
- return {
260
- text: selection.error ?? buildHelpText(runtimes),
261
- };
262
- }
263
- const runtime = selection.runtime;
264
-
265
- await rememberCurrentCommandRouteIfPossible(ctx, runtime.server, logger);
266
-
267
- switch (parsed.command) {
268
- case 'auth':
269
- return {
270
- text: buildAuthStatusText(runtime.server, runtime.credentialsLoader.loadAuthState()),
271
- };
272
- case 'profile':
273
- return {
274
- text: await buildProfileText(runtime, runtime.credentialsLoader.loadAuthState()),
275
- };
276
- case 'feed':
277
- return {
278
- text: await buildFeedText(runtime, runtime.credentialsLoader.loadAuthState()),
279
- };
280
- case 'pm':
281
- return {
282
- text: await buildPmPollText(runtime, runtime.credentialsLoader.loadAuthState()),
283
- };
284
- case 'here':
285
- return {
286
- text: await buildHereText(ctx, runtime.server, logger),
287
- };
288
- default:
289
- return {
290
- text: buildHelpText(runtimes),
291
- };
292
- }
293
- },
294
- });
295
- }
296
-
297
- function parseCommandArgs(args: string | undefined): ParsedCommandArgs {
298
- const tokens = args?.trim().length ? args.trim().split(/\s+/u) : [];
299
- let serverName: string | undefined;
300
- const filtered: string[] = [];
301
-
302
- for (let index = 0; index < tokens.length; index += 1) {
303
- const token = tokens[index];
304
- if ((token === '--server' || token === '-s') && tokens[index + 1]) {
305
- serverName = tokens[index + 1];
306
- index += 1;
307
- continue;
308
- }
309
- filtered.push(token);
310
- }
311
-
312
- const command = filtered[0]?.toLowerCase() ?? '';
313
- return {
314
- command,
315
- serverName,
316
- };
317
- }
318
-
319
- function selectServerRuntime(
320
- runtimes: ServerRuntime[],
321
- requestedServerName: string | undefined
322
- ): ServerRuntimeSelection {
323
- if (runtimes.length === 0) {
324
- return {
325
- error: 'No EigenFlux servers are configured.',
326
- };
327
- }
328
-
329
- if (!requestedServerName) {
330
- return {
331
- runtime: runtimes[0],
332
- };
333
- }
334
-
335
- const normalizedRequestedName = requestedServerName.trim().toLowerCase();
336
- const runtime = runtimes.find(
337
- (item) => item.server.name.trim().toLowerCase() === normalizedRequestedName
338
- );
339
- if (runtime) {
340
- return { runtime };
341
- }
342
-
343
- return {
344
- error: [
345
- `Unknown EigenFlux server: ${requestedServerName}`,
346
- `Available servers: ${runtimes.map((item) => item.server.name).join(', ')}`,
347
- ].join('\n'),
348
- };
349
- }
350
-
351
- function buildServersText(runtimes: ServerRuntime[]): string {
352
- if (runtimes.length === 0) {
353
- return 'No EigenFlux servers are configured.';
354
- }
355
-
356
- const defaultRuntime = runtimes[0];
357
-
358
- return [
359
- 'EigenFlux servers:',
360
- ...runtimes.map((runtime) => {
361
- const flags = [
362
- runtime.server.enabled ? 'enabled' : 'disabled',
363
- defaultRuntime?.server.name === runtime.server.name ? 'default' : null,
364
- ]
365
- .filter(Boolean)
366
- .join(', ');
367
- return `- ${runtime.server.name}: ${flags}; endpoint=${runtime.server.endpoint}; workdir=${runtime.server.workdir}`;
368
- }),
369
- ].join('\n');
370
- }
371
-
372
- function buildHelpText(runtimes: ServerRuntime[]): string {
373
- const defaultRuntime = runtimes[0];
374
- const availableCommands = Array.from(COMMAND_NAME_SET).join('|');
375
-
376
- return [
377
- `Usage: /eigenflux [--server <name>] <${availableCommands}>`,
378
- defaultRuntime ? `Default server: ${defaultRuntime.server.name}` : undefined,
379
- runtimes.length > 0
380
- ? `Available servers: ${runtimes.map((runtime) => runtime.server.name).join(', ')}`
381
- : undefined,
382
- '',
383
- '/eigenflux auth',
384
- 'Show current EigenFlux credential status.',
385
- '',
386
- '/eigenflux profile',
387
- 'Fetch /api/v1/agents/me with the current access token.',
388
- '',
389
- '/eigenflux servers',
390
- 'List configured EigenFlux servers.',
391
- '',
392
- '/eigenflux feed',
393
- 'Run one feed refresh and return the raw feed payload.',
394
- '',
395
- '/eigenflux pm',
396
- 'Run one PM fetch and return the raw PM payload.',
397
- '',
398
- '/eigenflux here',
399
- 'Remember the current conversation as the default delivery route for the selected server.',
400
- ]
401
- .filter(Boolean)
402
- .join('\n');
403
- }
404
-
405
- function readNonEmptyString(value: unknown): string | undefined {
406
- if (typeof value !== 'string') {
407
- return undefined;
408
- }
409
- const trimmed = value.trim();
410
- return trimmed.length > 0 ? trimmed : undefined;
411
- }
412
-
413
- function normalizeChannel(value: unknown): string | undefined {
414
- return readNonEmptyString(value)?.toLowerCase();
415
- }
416
-
417
- function isInternalAgentSessionKey(value: string | undefined): boolean {
418
- const trimmed = readNonEmptyString(value);
419
- if (!trimmed || trimmed === 'main') {
420
- return true;
421
- }
422
-
423
- const parts = trimmed.split(':').filter((part) => part.length > 0);
424
- return parts[0]?.toLowerCase() === 'agent' && parts[2]?.toLowerCase() === 'main';
425
- }
426
-
427
- function isNormalizedConversationTarget(value: string): boolean {
428
- return /^(user|chat|channel|room):/u.test(value);
429
- }
430
-
431
- function normalizeReplyTarget(
432
- value: unknown,
433
- channel: string | undefined,
434
- fallbackKind?: 'user' | 'chat' | 'channel' | 'room'
435
- ): string | undefined {
436
- const trimmed = readNonEmptyString(value);
437
- if (!trimmed) {
438
- return undefined;
439
- }
440
- if (isNormalizedConversationTarget(trimmed)) {
441
- return trimmed;
442
- }
443
- if (channel && trimmed.startsWith(`${channel}:`)) {
444
- const inner = trimmed.slice(channel.length + 1).trim();
445
- if (isNormalizedConversationTarget(inner)) {
446
- return inner;
447
- }
448
- return fallbackKind ? `${fallbackKind}:${inner}` : inner;
449
- }
450
- return fallbackKind ? `${fallbackKind}:${trimmed}` : trimmed;
451
- }
452
-
453
- async function resolveCurrentCommandRoute(
454
- ctx: CommandRouteContext,
455
- serverConfig: ResolvedEigenFluxServerConfig,
456
- logger: Logger
457
- ) {
458
- const channel = normalizeChannel(ctx.channel);
459
- const accountId = readNonEmptyString(ctx.accountId);
460
-
461
- let replyChannel = channel;
462
- let replyTo =
463
- normalizeReplyTarget(ctx.to, channel) ?? normalizeReplyTarget(ctx.from, channel, 'user');
464
- let replyAccountId = accountId;
465
-
466
- if (typeof ctx.getCurrentConversationBinding === 'function') {
467
- try {
468
- const binding = await ctx.getCurrentConversationBinding();
469
- if (binding) {
470
- replyChannel = normalizeChannel(binding.channel) ?? replyChannel;
471
- replyTo =
472
- normalizeReplyTarget(binding.conversationId, replyChannel) ??
473
- normalizeReplyTarget(binding.parentConversationId, replyChannel) ??
474
- replyTo;
475
- replyAccountId = readNonEmptyString(binding.accountId) ?? replyAccountId;
476
- }
477
- } catch (error) {
478
- logger.debug(
479
- `Failed to read current conversation binding: ${error instanceof Error ? error.message : String(error)}`
480
- );
481
- }
482
- }
483
-
484
- if (!replyChannel || !replyTo) {
485
- return undefined;
486
- }
487
-
488
- const route = resolveNotificationRoute(
489
- {
490
- sessionKey: 'main',
491
- agentId: serverConfig.agentId,
492
- replyChannel,
493
- replyTo,
494
- replyAccountId,
495
- sessionStorePath: readServerSessionStorePath(serverConfig),
496
- workdir: serverConfig.workdir,
497
- routeOverrides: {
498
- sessionKey: false,
499
- agentId: false,
500
- replyChannel: true,
501
- replyTo: true,
502
- replyAccountId: replyAccountId !== undefined,
503
- },
504
- },
505
- logger
506
- );
507
-
508
- if (!route.replyChannel || !route.replyTo) {
509
- return undefined;
510
- }
511
-
512
- if (isInternalAgentSessionKey(route.sessionKey)) {
513
- const configuredSessionKey = readNonEmptyString(serverConfig.sessionKey);
514
- if (configuredSessionKey && !isInternalAgentSessionKey(configuredSessionKey)) {
515
- return {
516
- sessionKey: configuredSessionKey,
517
- agentId: readNonEmptyString(serverConfig.agentId) ?? route.agentId,
518
- replyChannel: route.replyChannel,
519
- replyTo: route.replyTo,
520
- replyAccountId: route.replyAccountId,
521
- };
522
- }
523
- }
524
-
525
- return route;
526
- }
527
-
528
- async function buildHereText(
529
- ctx: CommandRouteContext,
530
- serverConfig: ResolvedEigenFluxServerConfig,
531
- logger: Logger
532
- ): Promise<string> {
533
- const route = await resolveCurrentCommandRoute(ctx, serverConfig, logger);
534
- if (!route || route.sessionKey === 'main' || route.sessionKey.endsWith(':main')) {
535
- return [
536
- `Unable to resolve the current external session for server=${serverConfig.name}.`,
537
- 'Run `/eigenflux here` inside the target conversation after OpenClaw has already created a session for it.',
538
- ].join('\n');
539
- }
540
-
541
- const saved = writeStoredNotificationRoute(serverConfig.workdir, route, logger);
542
- if (!saved) {
543
- return `Failed to persist the current EigenFlux route for server=${serverConfig.name}; check plugin logs for details.`;
544
- }
545
-
546
- return [
547
- `EigenFlux server ${serverConfig.name} will deliver to this conversation by default:`,
548
- `sessionKey: ${route.sessionKey}`,
549
- `agentId: ${route.agentId}`,
550
- `channel: ${route.replyChannel ?? 'unknown'}`,
551
- `target: ${route.replyTo ?? 'unknown'}`,
552
- route.replyAccountId ? `account: ${route.replyAccountId}` : undefined,
553
- ]
554
- .filter(Boolean)
555
- .join('\n');
556
- }
557
-
558
- async function rememberCurrentCommandRouteIfPossible(
559
- ctx: CommandRouteContext,
560
- serverConfig: ResolvedEigenFluxServerConfig,
561
- logger: Logger
562
- ): Promise<void> {
563
- const route = await resolveCurrentCommandRoute(ctx, serverConfig, logger);
564
- if (!route || route.sessionKey === 'main' || route.sessionKey.endsWith(':main')) {
565
- return;
566
- }
567
-
568
- if (writeStoredNotificationRoute(serverConfig.workdir, route, logger)) {
569
- logger.debug(
570
- `Remembered current command route for server=${serverConfig.name}: session_key=${route.sessionKey}, channel=${route.replyChannel ?? 'unknown'}, to=${route.replyTo ?? 'unknown'}`
571
- );
572
- }
573
- }
574
-
575
- async function buildProfileText(
576
- runtime: ServerRuntime,
577
- authState: AuthState
578
- ): Promise<string> {
579
- if (authState.status !== 'available') {
580
- return buildAuthRequiredMessage(runtime.getPromptContext(), {
581
- authEvent: {
582
- reason: authState.status === 'expired' ? 'expired_token' : 'missing_token',
583
- credentialsPath: authState.credentialsPath,
584
- source: authState.source,
585
- expiresAt: authState.expiresAt,
586
- },
587
- authState,
588
- });
589
- }
590
-
591
- try {
592
- const payload = await fetchJson<ProfileResponseData>(
593
- `${runtime.server.endpoint}/api/v1/agents/me`,
594
- authState.accessToken
595
- );
596
- return [
597
- `EigenFlux profile (server=${runtime.server.name}):`,
598
- '```json',
599
- safeJsonStringify(payload),
600
- '```',
601
- ].join('\n');
602
- } catch (error) {
603
- return `Failed to fetch profile for server ${runtime.server.name}: ${error instanceof Error ? error.message : String(error)}`;
604
- }
605
- }
606
-
607
- async function buildFeedText(
608
- runtime: ServerRuntime,
609
- authState: AuthState
610
- ): Promise<string> {
611
- const result = await runtime.pollingClient.pollOnce({
612
- notifyFeed: false,
613
- notifyAuthRequired: false,
614
- });
615
- switch (result.kind) {
616
- case 'success':
617
- return [
618
- `EigenFlux feed result (server=${runtime.server.name}):`,
619
- '```json',
620
- safeJsonStringify(result.payload),
621
- '```',
622
- ].join('\n');
623
- case 'auth_required':
624
- return buildAuthRequiredMessage(runtime.getPromptContext(), {
625
- authEvent: result.authEvent,
626
- authState,
627
- });
628
- case 'error':
629
- return `EigenFlux feed failed for server ${runtime.server.name}: ${result.error.message}`;
630
- default:
631
- return `EigenFlux feed finished with an unknown result for server ${runtime.server.name}.`;
632
- }
633
- }
634
-
635
- async function buildPmPollText(
636
- runtime: ServerRuntime,
637
- authState: AuthState
638
- ): Promise<string> {
639
- const result = await runtime.pmPollingClient.pollOnce({
640
- notifyFeed: false,
641
- notifyAuthRequired: false,
642
- });
643
- switch (result.kind) {
644
- case 'success':
645
- return [
646
- `EigenFlux PM poll result (server=${runtime.server.name}):`,
647
- '```json',
648
- safeJsonStringify(result.payload),
649
- '```',
650
- ].join('\n');
651
- case 'auth_required':
652
- return buildAuthRequiredMessage(runtime.getPromptContext(), {
653
- authEvent: result.authEvent,
654
- authState,
655
- });
656
- case 'error':
657
- return `EigenFlux PM poll failed for server ${runtime.server.name}: ${result.error.message}`;
658
- default:
659
- return `EigenFlux PM poll finished with an unknown result for server ${runtime.server.name}.`;
660
- }
661
- }
662
-
663
- async function fetchJson<T extends JsonRecord>(
664
- url: string,
665
- accessToken: string
666
- ): Promise<JsonApiSuccess<T>> {
667
- const response = await fetch(url, {
668
- method: 'GET',
669
- headers: {
670
- Authorization: `Bearer ${accessToken}`,
671
- 'Content-Type': 'application/json',
672
- 'User-Agent': PLUGIN_CONFIG.USER_AGENT,
673
- },
674
- });
675
-
676
- if (response.status === 401) {
677
- throw new Error('HTTP 401: unauthorized');
678
- }
679
- if (!response.ok) {
680
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
681
- }
682
-
683
- const payload = (await response.json()) as JsonApiSuccess<T>;
684
- if (payload.code !== 0) {
685
- throw new Error(`API error: ${payload.msg}`);
686
- }
687
- return payload;
688
- }
689
-
690
- function buildAuthStatusText(
691
- serverConfig: ResolvedEigenFluxServerConfig,
692
- authState: AuthState
693
- ): string {
694
- const lines = [`EigenFlux auth status (server=${serverConfig.name}):`];
695
- lines.push(`- workdir: ${serverConfig.workdir}`);
696
- lines.push(`- credentials_path: ${authState.credentialsPath}`);
697
- lines.push(`- status: ${authState.status}`);
698
- if (authState.source) {
699
- lines.push(`- source: ${authState.source}`);
700
- }
701
- if (authState.expiresAt) {
702
- lines.push(`- expires_at: ${authState.expiresAt}`);
703
- }
704
-
705
- if (authState.status === 'available') {
706
- lines.push(`- token: ${maskToken(authState.accessToken)}`);
707
- } else {
708
- lines.push('- token: unavailable');
709
- }
710
-
711
- return lines.join('\n');
712
- }
713
-
714
- function buildAuthRequiredMessage(
715
- promptContext: EigenFluxPromptServerContext,
716
- { authEvent, authState }: AuthPromptContext
717
- ): string {
718
- return buildAuthRequiredPromptTemplate({
719
- ...promptContext,
720
- authEvent,
721
- maskedToken: authState?.status === 'available' ? maskToken(authState.accessToken) : undefined,
722
- });
723
- }
724
-
725
- function buildFeedPayloadMessage(
726
- promptContext: EigenFluxPromptServerContext,
727
- payload: FeedResponse
728
- ): string {
729
- return buildFeedPayloadPromptTemplate(payload, promptContext);
730
- }
731
-
732
- function buildPmPayloadMessage(
733
- promptContext: EigenFluxPromptServerContext,
734
- payload: PmFetchResponse
735
- ): string {
736
- return buildPmPayloadPromptTemplate(payload, promptContext);
737
- }
738
-
739
- function maskToken(token: string): string {
740
- const trimmed = token.trim();
741
- if (trimmed.length <= 10) {
742
- return `${trimmed.slice(0, 2)}***`;
743
- }
744
- return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
745
- }
746
-
747
- function safeJsonStringify(value: unknown): string {
748
- try {
749
- return JSON.stringify(value, null, 2);
750
- } catch {
751
- return String(value);
752
- }
753
- }
754
-
755
- function toServiceIdSegment(name: string): string {
756
- const sanitized = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu, '-');
757
- return sanitized || 'default';
758
- }