@phronesis-io/openclaw-eigenflux 0.0.14 → 0.0.16

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/dist/index.js CHANGED
@@ -444,10 +444,104 @@ var EigenFluxStreamClient = class {
444
444
  }
445
445
  };
446
446
 
447
+ // src/openclaw-context.ts
448
+ var import_node_fs = require("fs");
449
+ var import_node_path = require("path");
450
+ var import_node_os = require("os");
451
+ var MEMORY_DIR_REL = ["workspace", "memory"];
452
+ var EMPTY_CONTEXT = { memoryDirs: [], sessionSnippets: [] };
453
+ function resolveOpenClawStateDir(logger) {
454
+ try {
455
+ const mod = require("openclaw/plugin-sdk/memory-core-host-runtime-core");
456
+ const dir = mod.resolveStateDir?.();
457
+ if (dir && typeof dir === "string") return dir;
458
+ } catch (err) {
459
+ logger.debug(`resolveOpenClawStateDir: SDK resolveStateDir unavailable: ${err instanceof Error ? err.message : String(err)}`);
460
+ }
461
+ const cwd = process.cwd();
462
+ if (cwd && (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "agents"))) return cwd;
463
+ const home = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw");
464
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(home, "agents"))) return home;
465
+ return void 0;
466
+ }
467
+ var MAX_SESSION_TURNS = 12;
468
+ var MAX_SESSION_SNIPPET_CHARS = 280;
469
+ function collectOpenClawContext(stateDir, logger, options) {
470
+ const agentName = options?.agentName ?? "main";
471
+ const memoryDir = (0, import_node_path.join)(stateDir, ...MEMORY_DIR_REL);
472
+ return {
473
+ memoryDirs: (0, import_node_fs.existsSync)(memoryDir) ? [memoryDir] : [],
474
+ sessionSnippets: readSessionSnippets(stateDir, agentName, logger)
475
+ };
476
+ }
477
+ function readSessionSnippets(stateDir, agentName, logger) {
478
+ try {
479
+ const dir = (0, import_node_path.join)(stateDir, "agents", agentName, "sessions");
480
+ const latest = latestSessionFile(dir);
481
+ if (!latest) return [];
482
+ const lines = (0, import_node_fs.readFileSync)(latest, "utf-8").split("\n");
483
+ const snippets = [];
484
+ for (let i = lines.length - 1; i >= 0 && snippets.length < MAX_SESSION_TURNS; i -= 1) {
485
+ const line = lines[i].trim();
486
+ if (!line) continue;
487
+ const text = extractTurnText(line);
488
+ if (text) snippets.push(text);
489
+ }
490
+ return snippets.reverse();
491
+ } catch (err) {
492
+ logger.debug(`readSessionSnippets: ${err instanceof Error ? err.message : String(err)}`);
493
+ return [];
494
+ }
495
+ }
496
+ function latestSessionFile(dir) {
497
+ let newest;
498
+ let entries;
499
+ try {
500
+ entries = (0, import_node_fs.readdirSync)(dir);
501
+ } catch {
502
+ return void 0;
503
+ }
504
+ for (const name of entries) {
505
+ if (!name.endsWith(".jsonl") || name.endsWith(".trajectory.jsonl")) continue;
506
+ const path4 = (0, import_node_path.join)(dir, name);
507
+ try {
508
+ const mtime = (0, import_node_fs.statSync)(path4).mtimeMs;
509
+ if (!newest || mtime > newest.mtime) newest = { path: path4, mtime };
510
+ } catch {
511
+ }
512
+ }
513
+ return newest?.path;
514
+ }
515
+ function extractTurnText(line) {
516
+ let obj;
517
+ try {
518
+ obj = JSON.parse(line);
519
+ } catch {
520
+ return void 0;
521
+ }
522
+ if (!obj || typeof obj !== "object") return void 0;
523
+ const message = obj.message;
524
+ if (!message || typeof message !== "object") return void 0;
525
+ const role = message.role;
526
+ if (role !== "user" && role !== "assistant") return void 0;
527
+ const content = message.content;
528
+ let text = "";
529
+ if (typeof content === "string") {
530
+ text = content;
531
+ } else if (Array.isArray(content)) {
532
+ text = content.filter((c) => !!c && typeof c === "object" && c.type === "text" && typeof c.text === "string").map((c) => c.text).join(" ");
533
+ }
534
+ text = text.trim();
535
+ if (!text) return void 0;
536
+ if (/EIGENFLUX_FEED_PAYLOAD|profile is due for|EigenFlux feed payload/i.test(text)) return void 0;
537
+ if (/^(NO_REPLY|HEARTBEAT_OK|Triggered a silent profile refresh)/.test(text)) return void 0;
538
+ const oneLine = text.replace(/\s+/g, " ");
539
+ return oneLine.length > MAX_SESSION_SNIPPET_CHARS ? `${oneLine.slice(0, MAX_SESSION_SNIPPET_CHARS)}\u2026` : oneLine;
540
+ }
541
+
447
542
  // src/profile-refresher.ts
448
543
  var REFRESH_WINDOW_START = 1;
449
544
  var REFRESH_WINDOW_END = 5;
450
- var ITEMS_LIMIT = 30;
451
545
  var EigenFluxProfileRefresher = class {
452
546
  constructor(config) {
453
547
  this.timeoutId = null;
@@ -472,6 +566,18 @@ var EigenFluxProfileRefresher = class {
472
566
  }
473
567
  this.config.logger.info(`Stopped profile refresher for server=${this.config.serverName}`);
474
568
  }
569
+ /**
570
+ * Run a refresh immediately, out of band from the daily timer. Intended for
571
+ * manual verification (`/eigenflux refresh`) so the full silent loop can be
572
+ * exercised on demand instead of waiting for the 1–5 AM window. Runs
573
+ * independently of the timer state — the command path may hold a refresher
574
+ * instance that was never start()ed, and a manual one-shot should still work.
575
+ * The next scheduled refresh (if any) is left untouched.
576
+ */
577
+ async triggerNow() {
578
+ this.config.logger.info(`Manual profile refresh triggered for server=${this.config.serverName}`);
579
+ await this.refresh({ manual: true });
580
+ }
475
581
  scheduleNext() {
476
582
  if (!this.running) return;
477
583
  const delay = msUntilNextRefresh(/* @__PURE__ */ new Date());
@@ -491,57 +597,98 @@ var EigenFluxProfileRefresher = class {
491
597
  this.scheduleNext();
492
598
  }, delay);
493
599
  }
494
- async refresh() {
600
+ async refresh(opts = {}) {
495
601
  this.config.logger.info(`Running profile refresh for server=${this.config.serverName}`);
496
- const [profileResult, itemsResult] = await Promise.all([
497
- execEigenflux(
498
- this.config.eigenfluxBin,
499
- ["profile", "show", "-s", this.config.serverName, "-f", "json"],
500
- { logger: this.config.logger }
501
- ),
502
- execEigenflux(
503
- this.config.eigenfluxBin,
504
- ["profile", "items", "-s", this.config.serverName, "-f", "json", "--limit", String(ITEMS_LIMIT)],
505
- { logger: this.config.logger }
506
- )
507
- ]);
508
- if (!this.running) return;
509
- if (profileResult.kind === "auth_required" || itemsResult.kind === "auth_required") {
510
- this.config.logger.warn(`Profile refresh: auth required for server=${this.config.serverName}`);
511
- await this.config.onAuthRequired();
512
- return;
602
+ let context = EMPTY_CONTEXT;
603
+ if (this.config.collectContext) {
604
+ try {
605
+ context = await this.config.collectContext() ?? EMPTY_CONTEXT;
606
+ } catch (err) {
607
+ this.config.logger.warn(
608
+ `Context collection failed: ${err instanceof Error ? err.message : String(err)}`
609
+ );
610
+ }
513
611
  }
514
- if (profileResult.kind === "not_installed" || itemsResult.kind === "not_installed") {
515
- this.config.logger.error(`eigenflux CLI not found (bin=${this.config.eigenfluxBin})`);
612
+ const memoryDirs = context.memoryDirs ?? [];
613
+ const sessionSnippets = context.sessionSnippets ?? [];
614
+ if (memoryDirs.length === 0 && sessionSnippets.length === 0) {
615
+ this.config.logger.info("Profile refresh skipped: no memory/session context");
616
+ this.emitTelemetry({ outcome: "skipped_no_context", memory_dirs: 0, session_snippets: 0, prompt_bytes: 0, delivered: false });
516
617
  return;
517
618
  }
518
- if (profileResult.kind !== "success") {
519
- this.config.logger.error(`Profile fetch failed: ${profileResult.kind}`);
619
+ if (!opts.manual && !this.running) return;
620
+ const args = [
621
+ "profile",
622
+ "refresh-prompt",
623
+ "-s",
624
+ this.config.serverName,
625
+ ...memoryDirs.flatMap((d) => ["--memory-dir", d]),
626
+ ...sessionSnippets.flatMap((s) => ["--session-snippet", s])
627
+ ];
628
+ const result = await execEigenflux(this.config.eigenfluxBin, args, {
629
+ logger: this.config.logger,
630
+ parseJson: false
631
+ });
632
+ if (!opts.manual && !this.running) return;
633
+ if (result.kind === "auth_required") {
634
+ this.config.logger.warn(`Profile refresh: auth required for server=${this.config.serverName}`);
635
+ this.emitTelemetry({ outcome: "auth_required", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
636
+ await this.config.onAuthRequired();
520
637
  return;
521
638
  }
522
- if (itemsResult.kind !== "success") {
523
- this.config.logger.error(`Items fetch failed: ${itemsResult.kind}`);
639
+ if (result.kind === "not_installed") {
640
+ this.config.logger.error(`eigenflux CLI not found (bin=${this.config.eigenfluxBin})`);
641
+ this.emitTelemetry({ outcome: "not_installed", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
524
642
  return;
525
643
  }
526
- const profileData = profileResult.data;
527
- if (!profileData) {
528
- this.config.logger.error("Profile fetch returned empty data");
644
+ if (result.kind !== "success") {
645
+ this.config.logger.error(`refresh-prompt failed: ${result.error.message}`);
646
+ this.emitTelemetry({ outcome: "error", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
529
647
  return;
530
648
  }
531
- const items = itemsResult.data?.items ?? [];
532
- if (items.length === 0) {
533
- this.config.logger.info("Profile refresh skipped: no recent items");
649
+ const prompt = (result.data ?? "").trim();
650
+ if (!prompt) {
651
+ this.config.logger.info("Profile refresh skipped: CLI produced no prompt");
652
+ this.emitTelemetry({ outcome: "skipped_no_context", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
534
653
  return;
535
654
  }
536
- const prompt = buildRefreshPrompt(profileData, items);
655
+ this.config.logger.info(
656
+ `Profile refresh context: memory_dirs=${memoryDirs.length}, session_snippets=${sessionSnippets.length}`
657
+ );
537
658
  try {
538
- if (!this.running) return;
659
+ if (!opts.manual && !this.running) return;
539
660
  await this.config.onRefreshPrompt(prompt);
540
661
  this.config.logger.info(`Profile refresh prompt delivered for server=${this.config.serverName}`);
662
+ this.emitTelemetry({
663
+ outcome: "delivered",
664
+ memory_dirs: memoryDirs.length,
665
+ session_snippets: sessionSnippets.length,
666
+ prompt_bytes: Buffer.byteLength(prompt, "utf-8"),
667
+ delivered: true
668
+ });
541
669
  } catch (err) {
542
670
  this.config.logger.error(
543
671
  `Profile refresh delivery failed: ${err instanceof Error ? err.message : String(err)}`
544
672
  );
673
+ this.emitTelemetry({
674
+ outcome: "delivery_failed",
675
+ memory_dirs: memoryDirs.length,
676
+ session_snippets: sessionSnippets.length,
677
+ prompt_bytes: Buffer.byteLength(prompt, "utf-8"),
678
+ delivered: false
679
+ });
680
+ }
681
+ }
682
+ /**
683
+ * Emit one structured layer-1 telemetry line. Grep `profile_refresh_telemetry`
684
+ * in plugin logs to confirm the daily refresh fired and was handed off for
685
+ * silent delivery. Best-effort: never throws.
686
+ */
687
+ emitTelemetry(t) {
688
+ try {
689
+ const payload = { server: this.config.serverName, ...t };
690
+ this.config.logger.info(`profile_refresh_telemetry ${JSON.stringify(payload)}`);
691
+ } catch {
545
692
  }
546
693
  }
547
694
  };
@@ -556,41 +703,6 @@ function msUntilNextRefresh(now) {
556
703
  }
557
704
  return target.getTime() - now.getTime();
558
705
  }
559
- function buildRefreshPrompt(profile, items) {
560
- const name = profile.profile?.agent_name ?? "(unknown)";
561
- const bio = profile.profile?.bio || "(empty)";
562
- const totalItems = profile.influence?.total_items ?? 0;
563
- const totalConsumed = profile.influence?.total_consumed ?? 0;
564
- const totalScored = (profile.influence?.total_scored_1 ?? 0) + (profile.influence?.total_scored_2 ?? 0);
565
- const lines = [
566
- "Your EigenFlux profile is due for a refresh. Below is your current profile",
567
- "and recent broadcast activity.",
568
- "",
569
- "## Current Profile",
570
- `- Name: ${name}`,
571
- `- Bio: ${bio}`,
572
- `- Influence: ${totalItems} items published, ${totalConsumed} consumed, ${totalScored} scored`,
573
- "",
574
- "## Recent Broadcasts"
575
- ];
576
- for (const item of items) {
577
- const summary = item.summary || "(no summary)";
578
- let line = `- [${item.broadcast_type ?? "unknown"}] ${summary}`;
579
- if (item.keywords) line += ` (keywords: ${item.keywords})`;
580
- if (item.total_score && item.total_score > 0) line += ` (score: ${item.total_score})`;
581
- lines.push(line);
582
- }
583
- lines.push(
584
- "",
585
- "## Instructions",
586
- "1. Write a concise bio (2-4 sentences) reflecting current focus areas and expertise.",
587
- "2. Incorporate patterns from recent broadcasts \u2014 topics, domains, interests.",
588
- "3. Preserve still-relevant info from the current bio.",
589
- "4. If not enough new activity to meaningfully update, do nothing.",
590
- '5. To update, run: eigenflux profile update --bio "YOUR NEW BIO"'
591
- );
592
- return lines.join("\n");
593
- }
594
706
 
595
707
  // src/settings-reporter.ts
596
708
  function resolveAgentMode(env = process.env) {
@@ -772,26 +884,6 @@ var CredentialsLoader = class {
772
884
  credentialsPath: this.credentialsPath
773
885
  };
774
886
  }
775
- /**
776
- * Restore credentials from a backup object (e.g. OpenClaw config).
777
- * Only writes if no local credentials.json exists yet.
778
- * Returns true if credentials were restored.
779
- */
780
- restoreFromBackup(backup) {
781
- if (fs.existsSync(this.credentialsPath)) {
782
- this.logger.debug(`[credential-restore] skip: credentials.json already exists at ${this.credentialsPath}`);
783
- return false;
784
- }
785
- if (!backup?.access_token) {
786
- this.logger.debug(`[credential-restore] skip: backup has no access_token`);
787
- return false;
788
- }
789
- this.saveAccessToken(backup.access_token, backup.email, backup.expires_at);
790
- this.logger.info(
791
- `[credential-restore] restored from backup: email=${backup.email ?? "n/a"}, path=${this.credentialsPath}`
792
- );
793
- return true;
794
- }
795
887
  saveAccessToken(token, email, expiresAt) {
796
888
  this.logger.info(`Saving access token: path=${this.credentialsPath}, email=${email ?? "n/a"}`);
797
889
  try {
@@ -914,7 +1006,8 @@ function normalizeReplyTarget(value, options) {
914
1006
  }
915
1007
 
916
1008
  // src/config.ts
917
- var PLUGIN_VERSION = "0.0.14";
1009
+ var PLUGIN_VERSION = "0.0.16";
1010
+ var EXPECTED_CLI_VERSION = "0.0.13";
918
1011
  var DEFAULT_EIGENFLUX_BIN = "eigenflux";
919
1012
  var DEFAULT_SESSION_KEY = "main";
920
1013
  var DEFAULT_AGENT_ID = "main";
@@ -1004,6 +1097,30 @@ async function discoverServers(eigenfluxBin, logger) {
1004
1097
  logger?.error(`eigenflux server list failed: ${result.error.message}`);
1005
1098
  return { kind: "ok", servers: [] };
1006
1099
  }
1100
+ async function getInstalledCliVersion(eigenfluxBin, logger) {
1101
+ const result = await execEigenflux(
1102
+ eigenfluxBin,
1103
+ ["version"],
1104
+ { logger }
1105
+ );
1106
+ if (result.kind === "success" && typeof result.data?.cli_version === "string") {
1107
+ return result.data.cli_version;
1108
+ }
1109
+ return null;
1110
+ }
1111
+ function isCliOutdated(installed, target) {
1112
+ if (!installed) return false;
1113
+ const parse = (v) => v.split(".").slice(0, 3).map((part) => parseInt(part, 10));
1114
+ const a = parse(installed);
1115
+ const b = parse(target);
1116
+ for (let i = 0; i < 3; i++) {
1117
+ const x = a[i] ?? 0;
1118
+ const y = b[i] ?? 0;
1119
+ if (Number.isNaN(x) || Number.isNaN(y)) return false;
1120
+ if (x !== y) return x < y;
1121
+ }
1122
+ return false;
1123
+ }
1007
1124
  function resolveEigenfluxHome(baseDir) {
1008
1125
  const envHome = process.env.EIGENFLUX_HOME;
1009
1126
  if (envHome) {
@@ -1069,7 +1186,8 @@ var PLUGIN_CONFIG = {
1069
1186
  DEFAULT_AGENT_ID,
1070
1187
  DEFAULT_OPENCLAW_CLI_BIN,
1071
1188
  HOST_KIND,
1072
- PLUGIN_VERSION
1189
+ PLUGIN_VERSION,
1190
+ EXPECTED_CLI_VERSION
1073
1191
  };
1074
1192
 
1075
1193
  // src/notification-route-resolver.ts
@@ -1686,8 +1804,8 @@ async function resolveNotificationRoute(config, logger, options = {}) {
1686
1804
  }
1687
1805
 
1688
1806
  // src/agent-prompt-templates.ts
1689
- var import_node_fs = require("fs");
1690
- var import_node_path = require("path");
1807
+ var import_node_fs2 = require("fs");
1808
+ var import_node_path2 = require("path");
1691
1809
  var FEED_OUTPUT_CONTRACT_FALLBACK = [
1692
1810
  "OUTPUT CONTRACT \u2014 non-negotiable subset of references/feed.md (full procedure there):",
1693
1811
  "1. Triage silently: push items relevant to the user, discard the rest. Never",
@@ -1713,8 +1831,8 @@ var FEED_OUTPUT_CONTRACT_FALLBACK = [
1713
1831
  ].join("\n");
1714
1832
  function loadFeedOutputContract() {
1715
1833
  try {
1716
- return (0, import_node_fs.readFileSync)(
1717
- (0, import_node_path.join)(__dirname, "../skills/ef-broadcast/references/contract.md"),
1834
+ return (0, import_node_fs2.readFileSync)(
1835
+ (0, import_node_path2.join)(__dirname, "../skills/ef-broadcast/references/contract.md"),
1718
1836
  "utf-8"
1719
1837
  ).trim();
1720
1838
  } catch {
@@ -1772,6 +1890,18 @@ function buildNotInstalledPromptTemplate({
1772
1890
  `\`${installCommand}\``
1773
1891
  ].join("\n");
1774
1892
  }
1893
+ function buildOutdatedPromptTemplate({
1894
+ installed,
1895
+ expected,
1896
+ updateCommand
1897
+ }) {
1898
+ return [
1899
+ "[EIGENFLUX_CLI_OUTDATED]",
1900
+ `The EigenFlux CLI is outdated (installed=${installed}, this plugin expects >=${expected}).`,
1901
+ "Please update it to the latest version. You can run:",
1902
+ `\`${updateCommand}\``
1903
+ ].join("\n");
1904
+ }
1775
1905
  function buildPmStreamEventPromptTemplate(event, context) {
1776
1906
  const data = event.data ?? {};
1777
1907
  const parts = [];
@@ -1809,6 +1939,7 @@ var EigenFluxNotifier = class {
1809
1939
  }
1810
1940
  async deliver(message, options) {
1811
1941
  const targetKey = options?.targetSessionKey;
1942
+ const silent = options?.silent === true;
1812
1943
  if (targetKey) {
1813
1944
  await this.drainPendingCleanups();
1814
1945
  const baseRoute = await this.resolveRoute();
@@ -1823,7 +1954,7 @@ var EigenFluxNotifier = class {
1823
1954
  this.logger.info(
1824
1955
  `Delivery route resolved: source=targeted-oneshot, ${formatRouteForLog(route)}, message_preview=${previewMessage(message)}`
1825
1956
  );
1826
- const result = await this.attemptDelivery(message, route, { skipHeartbeat: true });
1957
+ const result = await this.attemptDelivery(message, route, { skipHeartbeat: true, silent });
1827
1958
  if (result.result.ok) {
1828
1959
  this.logDispatch(result.result);
1829
1960
  } else {
@@ -1836,7 +1967,7 @@ var EigenFluxNotifier = class {
1836
1967
  this.logger.info(
1837
1968
  `Delivery route resolved: source=${initial.source}, ${formatRouteForLog(initial.route)}, message_preview=${previewMessage(message)}`
1838
1969
  );
1839
- const firstAttempt = await this.attemptDelivery(message, initial.route);
1970
+ const firstAttempt = await this.attemptDelivery(message, initial.route, { silent });
1840
1971
  if (firstAttempt.result.ok) {
1841
1972
  await this.rememberRouteIfChanged(firstAttempt.finalRoute, initial.source);
1842
1973
  this.logDispatch(firstAttempt.result);
@@ -1851,7 +1982,7 @@ var EigenFluxNotifier = class {
1851
1982
  this.logger.info(
1852
1983
  `Retrying delivery with fresh route: source=${fallback.source}, ${formatRouteForLog(fallback.route)}`
1853
1984
  );
1854
- const retry = await this.attemptDelivery(message, fallback.route);
1985
+ const retry = await this.attemptDelivery(message, fallback.route, { silent });
1855
1986
  if (retry.result.ok) {
1856
1987
  await this.rememberRouteIfChanged(retry.finalRoute, fallback.source);
1857
1988
  this.logDispatch(retry.result);
@@ -1868,11 +1999,12 @@ var EigenFluxNotifier = class {
1868
1999
  return false;
1869
2000
  }
1870
2001
  async attemptDelivery(message, route, options = {}) {
2002
+ const silent = options.silent === true;
1871
2003
  const attempts = [
1872
- () => this.tryNotifyViaRuntimeSubagent(message, route),
1873
- () => this.tryNotifyViaRuntimeCommandAgent(message, route)
2004
+ () => this.tryNotifyViaRuntimeSubagent(message, route, silent),
2005
+ () => this.tryNotifyViaRuntimeCommandAgent(message, route, silent)
1874
2006
  ];
1875
- if (!options.skipHeartbeat) {
2007
+ if (!options.skipHeartbeat && !silent) {
1876
2008
  attempts.push(
1877
2009
  () => this.tryNotifyViaRuntimeHeartbeat(message, route),
1878
2010
  () => this.tryNotifyViaRuntimeCommandHeartbeat(message)
@@ -1902,7 +2034,7 @@ var EigenFluxNotifier = class {
1902
2034
  errors
1903
2035
  };
1904
2036
  }
1905
- async tryNotifyViaRuntimeSubagent(message, route) {
2037
+ async tryNotifyViaRuntimeSubagent(message, route, silent = false) {
1906
2038
  const runtimeSubagent = this.runtime.subagent;
1907
2039
  if (!runtimeSubagent || typeof runtimeSubagent.run !== "function") {
1908
2040
  return {
@@ -1912,13 +2044,14 @@ var EigenFluxNotifier = class {
1912
2044
  };
1913
2045
  }
1914
2046
  try {
2047
+ const deliver = !silent;
1915
2048
  this.logger.info(
1916
- `Attempting runtime.subagent delivery: ${formatRouteForLog(route)}, deliver=true`
2049
+ `Attempting runtime.subagent delivery: ${formatRouteForLog(route)}, deliver=${deliver}`
1917
2050
  );
1918
2051
  const { runId } = await runtimeSubagent.run({
1919
2052
  sessionKey: route.sessionKey,
1920
2053
  message,
1921
- deliver: true,
2054
+ deliver,
1922
2055
  idempotencyKey: (0, import_node_crypto.randomUUID)()
1923
2056
  });
1924
2057
  if (typeof runtimeSubagent.waitForRun === "function") {
@@ -1948,10 +2081,10 @@ var EigenFluxNotifier = class {
1948
2081
  };
1949
2082
  }
1950
2083
  }
1951
- async tryNotifyViaRuntimeCommandAgent(message, route) {
2084
+ async tryNotifyViaRuntimeCommandAgent(message, route, silent = false) {
1952
2085
  return this.runRuntimeCommand(
1953
2086
  "runtime.command.agent",
1954
- this.buildAgentCliArgs(message, route),
2087
+ this.buildAgentCliArgs(message, route, silent),
1955
2088
  route
1956
2089
  );
1957
2090
  }
@@ -2035,16 +2168,18 @@ var EigenFluxNotifier = class {
2035
2168
  };
2036
2169
  }
2037
2170
  }
2038
- buildAgentCliArgs(message, route) {
2171
+ buildAgentCliArgs(message, route, silent = false) {
2039
2172
  const args = [
2040
2173
  this.config.openclawCliBin,
2041
2174
  "agent",
2042
2175
  "--message",
2043
2176
  message,
2044
2177
  "--agent",
2045
- route.agentId,
2046
- "--deliver"
2178
+ route.agentId
2047
2179
  ];
2180
+ if (!silent) {
2181
+ args.push("--deliver");
2182
+ }
2048
2183
  if (route.replyChannel) {
2049
2184
  args.push("--reply-channel", route.replyChannel);
2050
2185
  }
@@ -2201,7 +2336,7 @@ function formatCommandArgsForLog(argv) {
2201
2336
  }
2202
2337
 
2203
2338
  // src/index.ts
2204
- var COMMAND_NAMES = ["auth", "profile", "servers", "feed", "pm", "here", "version"];
2339
+ var COMMAND_NAMES = ["auth", "profile", "refresh", "servers", "feed", "pm", "here", "version"];
2205
2340
  var COMMAND_NAME_SET = new Set(COMMAND_NAMES);
2206
2341
  var DEFAULT_ROUTING = {
2207
2342
  sessionKey: PLUGIN_CONFIG.DEFAULT_SESSION_KEY,
@@ -2227,6 +2362,7 @@ function registerPlugin(api) {
2227
2362
  const store = createInMemoryPluginStore();
2228
2363
  let runtimes = [];
2229
2364
  let notInstalledPromptDelivered = false;
2365
+ let outdatedPromptDelivered = false;
2230
2366
  api.registerService({
2231
2367
  id: "eigenflux:discovery",
2232
2368
  start: async () => {
@@ -2263,6 +2399,23 @@ function registerPlugin(api) {
2263
2399
  await runtime.streamClient.start();
2264
2400
  runtime.profileRefresher.start();
2265
2401
  }
2402
+ if (!outdatedPromptDelivered) {
2403
+ const installedVersion = await getInstalledCliVersion(pluginConfig.eigenfluxBin, logger);
2404
+ if (isCliOutdated(installedVersion, PLUGIN_CONFIG.EXPECTED_CLI_VERSION)) {
2405
+ outdatedPromptDelivered = true;
2406
+ logger.warn(
2407
+ `EigenFlux CLI outdated (installed=${installedVersion}, expected>=${PLUGIN_CONFIG.EXPECTED_CLI_VERSION}); delivering upgrade prompt`
2408
+ );
2409
+ await deliverOutdatedPrompt(
2410
+ api,
2411
+ logger,
2412
+ pluginConfig,
2413
+ installedVersion,
2414
+ PLUGIN_CONFIG.EXPECTED_CLI_VERSION,
2415
+ store
2416
+ );
2417
+ }
2418
+ }
2266
2419
  },
2267
2420
  stop: async () => {
2268
2421
  logger.info("Stopping EigenFlux discovery service...");
@@ -2276,6 +2429,7 @@ function registerPlugin(api) {
2276
2429
  }
2277
2430
  runtimes = [];
2278
2431
  notInstalledPromptDelivered = false;
2432
+ outdatedPromptDelivered = false;
2279
2433
  }
2280
2434
  });
2281
2435
  registerCommand(
@@ -2323,11 +2477,6 @@ var PLUGIN_CONFIG_SCHEMA = (0, import_plugin_entry.buildJsonPluginConfigSchema)(
2323
2477
  replyAccountId: { type: "string" }
2324
2478
  }
2325
2479
  }
2326
- },
2327
- _credentialBackup: {
2328
- type: "object",
2329
- description: "Internal: persisted credential backups for sandbox environments",
2330
- additionalProperties: { type: "object" }
2331
2480
  }
2332
2481
  }
2333
2482
  });
@@ -2342,48 +2491,6 @@ var index_default = (0, import_plugin_entry.definePluginEntry)({
2342
2491
  }
2343
2492
  });
2344
2493
  var INSTALL_COMMAND = "curl -fsSL https://eigenflux.ai/install.sh | bash";
2345
- var _lastBackedUpToken = {};
2346
- function backupCredentialsToConfig(api, logger, credentialsLoader, serverName) {
2347
- const authState = credentialsLoader.loadAuthState();
2348
- if (authState.status !== "available") {
2349
- logger.debug(`[credential-backup] skip: credentials not available for server=${serverName}`);
2350
- return;
2351
- }
2352
- if (_lastBackedUpToken[serverName] === authState.accessToken) {
2353
- return;
2354
- }
2355
- const backup = {
2356
- access_token: authState.accessToken,
2357
- email: authState.email,
2358
- expires_at: authState.expiresAt ?? Date.now() + 30 * 24 * 60 * 60 * 1e3,
2359
- server: serverName,
2360
- backed_up_at: Date.now()
2361
- };
2362
- logger.info(
2363
- `[credential-backup] backing up credentials to OpenClaw config: server=${serverName}, email=${authState.email ?? "n/a"}`
2364
- );
2365
- try {
2366
- api.runtime.config.mutateConfigFile({
2367
- afterWrite: { mode: "none", reason: "eigenflux credential backup" },
2368
- mutate(draft) {
2369
- var _a, _b, _c, _d;
2370
- draft.plugins ?? (draft.plugins = {});
2371
- (_a = draft.plugins).entries ?? (_a.entries = {});
2372
- (_b = draft.plugins.entries)["openclaw-eigenflux"] ?? (_b["openclaw-eigenflux"] = {});
2373
- (_c = draft.plugins.entries["openclaw-eigenflux"]).config ?? (_c.config = {});
2374
- (_d = draft.plugins.entries["openclaw-eigenflux"].config)._credentialBackup ?? (_d._credentialBackup = {});
2375
- draft.plugins.entries["openclaw-eigenflux"].config._credentialBackup[serverName] = backup;
2376
- }
2377
- }).then(() => {
2378
- _lastBackedUpToken[serverName] = authState.accessToken;
2379
- logger.info(`[credential-backup] saved to OpenClaw config for server=${serverName}`);
2380
- }).catch((err) => {
2381
- logger.warn(`[credential-backup] failed to save: ${err.message}`);
2382
- });
2383
- } catch (err) {
2384
- logger.warn(`[credential-backup] sync error: ${err instanceof Error ? err.message : String(err)}`);
2385
- }
2386
- }
2387
2494
  async function deliverNotInstalledPrompt(api, logger, pluginConfig, _eigenfluxHome, bin, store) {
2388
2495
  const notifier = new EigenFluxNotifier(api, logger, {
2389
2496
  sessionKey: DEFAULT_ROUTING.sessionKey,
@@ -2398,24 +2505,26 @@ async function deliverNotInstalledPrompt(api, logger, pluginConfig, _eigenfluxHo
2398
2505
  buildNotInstalledPromptTemplate({ bin, installCommand: INSTALL_COMMAND })
2399
2506
  );
2400
2507
  }
2508
+ async function deliverOutdatedPrompt(api, logger, pluginConfig, installed, expected, _store) {
2509
+ const notifier = new EigenFluxNotifier(api, logger, {
2510
+ sessionKey: DEFAULT_ROUTING.sessionKey,
2511
+ agentId: DEFAULT_ROUTING.agentId,
2512
+ replyChannel: DEFAULT_ROUTING.replyChannel,
2513
+ replyTo: DEFAULT_ROUTING.replyTo,
2514
+ replyAccountId: DEFAULT_ROUTING.replyAccountId,
2515
+ openclawCliBin: pluginConfig.openclawCliBin,
2516
+ routeOverrides: DEFAULT_ROUTING.routeOverrides
2517
+ });
2518
+ await notifier.deliver(
2519
+ buildOutdatedPromptTemplate({ installed, expected, updateCommand: INSTALL_COMMAND })
2520
+ );
2521
+ }
2401
2522
  function buildFeedSessionKey(serverName) {
2402
2523
  return `eigenflux:feed:${serverName}`;
2403
2524
  }
2404
2525
  function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, store) {
2405
2526
  const routing = pluginConfig.serverRouting[server.name] ?? DEFAULT_ROUTING;
2406
2527
  const credentialsLoader = new CredentialsLoader(logger, eigenfluxHome, server.name);
2407
- try {
2408
- const rawConfig = api.pluginConfig;
2409
- const backupMap = rawConfig?._credentialBackup;
2410
- const backup = backupMap?.[server.name];
2411
- if (backup?.access_token) {
2412
- credentialsLoader.restoreFromBackup(backup);
2413
- } else {
2414
- logger.debug(`[credential-restore] no backup found in OpenClaw config for server=${server.name}`);
2415
- }
2416
- } catch (err) {
2417
- logger.warn(`[credential-restore] failed: ${err instanceof Error ? err.message : String(err)}`);
2418
- }
2419
2528
  const notifier = new EigenFluxNotifier(api, logger, {
2420
2529
  store,
2421
2530
  eigenfluxBin: pluginConfig.eigenfluxBin,
@@ -2464,7 +2573,6 @@ function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, s
2464
2573
  logger,
2465
2574
  onFeedPolled: async (payload) => {
2466
2575
  resetAuthPromptGate();
2467
- backupCredentialsToConfig(api, logger, credentialsLoader, server.name);
2468
2576
  const items = payload.data?.items ?? [];
2469
2577
  const notifications = payload.data?.notifications ?? [];
2470
2578
  if (feedDeliveryInFlight && feedDeliveryStartedAt > 0) {
@@ -2526,9 +2634,27 @@ function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, s
2526
2634
  serverName: server.name,
2527
2635
  eigenfluxBin: pluginConfig.eigenfluxBin,
2528
2636
  logger,
2637
+ // OpenClaw adapter for the host-agnostic `eigenflux profile refresh-prompt`
2638
+ // core: supply the host-specific inputs (memory dir + extracted session
2639
+ // snippets); the CLI reads the memory markdown and assembles the prompt.
2640
+ // The state dir is resolved via the SDK, NOT api.rootDir (which is the
2641
+ // plugin's install directory). Best-effort; empty on error.
2642
+ //
2643
+ // TODO(multi-host): each host gets its own thin adapter that returns
2644
+ // { memoryDirs, sessionSnippets } and delivers the CLI's prompt silently:
2645
+ // - Claude Code: memory from CLAUDE.md / ~/.claude memory; session from
2646
+ // ~/.claude/projects/**/*.jsonl; delivery via the claude/channel
2647
+ // (note: channel pushes are user-visible — true silence needs more work).
2648
+ // - Hermes: memory/session locations + silent-delivery mechanism TBD —
2649
+ // investigate the host before writing the adapter.
2650
+ // - Codex: memory likely AGENTS.md; session store + delivery TBD.
2651
+ collectContext: () => {
2652
+ const stateDir = resolveOpenClawStateDir(logger);
2653
+ return stateDir ? collectOpenClawContext(stateDir, logger) : EMPTY_CONTEXT;
2654
+ },
2529
2655
  onRefreshPrompt: async (prompt) => {
2530
2656
  resetAuthPromptGate();
2531
- await notifier.deliver(prompt);
2657
+ await notifier.deliver(prompt, { silent: true });
2532
2658
  },
2533
2659
  onAuthRequired: async () => {
2534
2660
  await notifyAuthRequired({ reason: "auth_required" });
@@ -2588,7 +2714,7 @@ function registerCommand(api, logger, pluginConfig, eigenfluxHome, store, getRun
2588
2714
  };
2589
2715
  api.registerCommand({
2590
2716
  name: "eigenflux",
2591
- description: "EigenFlux plugin commands: auth, profile, servers, feed, pm, here, version",
2717
+ description: "EigenFlux plugin commands: auth, profile, refresh, servers, feed, pm, here, version",
2592
2718
  acceptsArgs: true,
2593
2719
  handler: async (ctx) => {
2594
2720
  const parsed = parseCommandArgs(ctx.args);
@@ -2625,6 +2751,22 @@ function registerCommand(api, logger, pluginConfig, eigenfluxHome, store, getRun
2625
2751
  return {
2626
2752
  text: await buildProfileText(runtime, pluginConfig.eigenfluxBin)
2627
2753
  };
2754
+ case "refresh": {
2755
+ const probeStateDir = resolveOpenClawStateDir(logger);
2756
+ const probe = probeStateDir ? collectOpenClawContext(probeStateDir, logger) : EMPTY_CONTEXT;
2757
+ void runtime.profileRefresher.triggerNow().catch((err) => {
2758
+ logger.error(
2759
+ `Manual profile refresh failed for server=${runtime.server.name}: ${err instanceof Error ? err.message : String(err)}`
2760
+ );
2761
+ });
2762
+ return {
2763
+ text: [
2764
+ `Triggered a silent profile refresh for server=${runtime.server.name} (running in background).`,
2765
+ `context probe: memory_dirs=${probe.memoryDirs.length}, session=${probe.sessionSnippets.length} snippet(s), stateDir=${probeStateDir ?? "undefined"}`,
2766
+ "No channel reply. Verify via a new agent_bio_history row if the bio changed."
2767
+ ].join("\n")
2768
+ };
2769
+ }
2628
2770
  case "feed":
2629
2771
  return {
2630
2772
  text: await buildFeedText(runtime)
@@ -2715,6 +2857,7 @@ function buildHelpText(runtimes) {
2715
2857
  "",
2716
2858
  "/eigenflux auth \u2014 Show credential status",
2717
2859
  "/eigenflux profile \u2014 Fetch agent profile",
2860
+ "/eigenflux refresh \u2014 Trigger a silent daily-style bio refresh now",
2718
2861
  "/eigenflux servers \u2014 List discovered servers",
2719
2862
  "/eigenflux feed \u2014 Run one feed refresh",
2720
2863
  "/eigenflux pm \u2014 Show PM stream status",