agent-relay 8.8.5 → 8.9.1
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/cli/agent-relay-mcp.d.ts +2 -53
- package/dist/cli/agent-relay-mcp.d.ts.map +1 -1
- package/dist/cli/agent-relay-mcp.js +11 -943
- package/dist/cli/agent-relay-mcp.js.map +1 -1
- package/dist/cli/commands/fleet.d.ts.map +1 -1
- package/dist/cli/commands/fleet.js +86 -13
- package/dist/cli/commands/fleet.js.map +1 -1
- package/dist/cli/lib/broker-dashboard.d.ts +12 -0
- package/dist/cli/lib/broker-dashboard.d.ts.map +1 -0
- package/dist/cli/lib/broker-dashboard.js +431 -0
- package/dist/cli/lib/broker-dashboard.js.map +1 -0
- package/dist/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/cli/lib/broker-lifecycle.js +1 -428
- package/dist/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/cli/mcp/action-schema.d.ts +20 -0
- package/dist/cli/mcp/action-schema.d.ts.map +1 -0
- package/dist/cli/mcp/action-schema.js +132 -0
- package/dist/cli/mcp/action-schema.js.map +1 -0
- package/dist/cli/mcp/action-tools.d.ts +10 -0
- package/dist/cli/mcp/action-tools.d.ts.map +1 -0
- package/dist/cli/mcp/action-tools.js +114 -0
- package/dist/cli/mcp/action-tools.js.map +1 -0
- package/dist/cli/mcp/inbox.d.ts +9 -0
- package/dist/cli/mcp/inbox.d.ts.map +1 -0
- package/dist/cli/mcp/inbox.js +49 -0
- package/dist/cli/mcp/inbox.js.map +1 -0
- package/dist/cli/mcp/messaging-tools.d.ts +9 -0
- package/dist/cli/mcp/messaging-tools.d.ts.map +1 -0
- package/dist/cli/mcp/messaging-tools.js +315 -0
- package/dist/cli/mcp/messaging-tools.js.map +1 -0
- package/dist/cli/mcp/resources.d.ts +39 -0
- package/dist/cli/mcp/resources.d.ts.map +1 -0
- package/dist/cli/mcp/resources.js +158 -0
- package/dist/cli/mcp/resources.js.map +1 -0
- package/dist/cli/mcp/telemetry.d.ts +9 -0
- package/dist/cli/mcp/telemetry.d.ts.map +1 -0
- package/dist/cli/mcp/telemetry.js +176 -0
- package/dist/cli/mcp/telemetry.js.map +1 -0
- package/dist/cli/mcp/tool-results.d.ts +29 -0
- package/dist/cli/mcp/tool-results.d.ts.map +1 -0
- package/dist/cli/mcp/tool-results.js +39 -0
- package/dist/cli/mcp/tool-results.js.map +1 -0
- package/dist/cli/mcp/tool-shapes.d.ts +13 -0
- package/dist/cli/mcp/tool-shapes.d.ts.map +1 -0
- package/dist/cli/mcp/tool-shapes.js +16 -0
- package/dist/cli/mcp/tool-shapes.js.map +1 -0
- package/dist/cli/mcp/types.d.ts +38 -0
- package/dist/cli/mcp/types.d.ts.map +1 -0
- package/dist/cli/mcp/types.js +2 -0
- package/dist/cli/mcp/types.js.map +1 -0
- package/dist/cli/mcp/workspace.d.ts +10 -0
- package/dist/cli/mcp/workspace.d.ts.map +1 -0
- package/dist/cli/mcp/workspace.js +36 -0
- package/dist/cli/mcp/workspace.js.map +1 -0
- package/dist/index.cjs +47 -2
- package/package.json +8 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { HarnessDriverClient } from '@agent-relay/harness-driver';
|
|
5
4
|
import { track } from '../telemetry/index.js';
|
|
5
|
+
import { getDefaultDashboardRelayUrl, isDebugLikeLoggingEnabled, normalizeDashboardPath, resolveDashboardPortWithFallback, resolveDashboardRelayUrl, startDashboardWithFallback, waitForDashboard, } from './broker-dashboard.js';
|
|
6
6
|
import { buildBundledAgentRelayMcpCommand } from './agent-relay-mcp-command.js';
|
|
7
7
|
import { errorClassName } from './telemetry-helpers.js';
|
|
8
8
|
import { createImplicitLocalFleetNode, fleetStatusPath, startFleetSidecar, } from './fleet-sidecar.js';
|
|
@@ -176,19 +176,6 @@ function startImplicitLocalFleetSidecar(paths, relay, options, deps, teamsConfig
|
|
|
176
176
|
warn: (message) => deps.warn(message),
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
|
-
async function resolveDashboardPortWithFallback(dashboardPort, dashboardPortCandidates, deps) {
|
|
180
|
-
for (let attempt = 0; attempt < dashboardPortCandidates; attempt += 1) {
|
|
181
|
-
const candidatePort = dashboardPort + attempt;
|
|
182
|
-
const inUse = await deps.isPortInUse(candidatePort);
|
|
183
|
-
if (!inUse) {
|
|
184
|
-
if (attempt > 0) {
|
|
185
|
-
deps.warn(`Dashboard port ${dashboardPort} is already in use; trying ${candidatePort}`);
|
|
186
|
-
}
|
|
187
|
-
return candidatePort;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
throw new Error(`Failed to find an available dashboard port near ${dashboardPort}.`);
|
|
191
|
-
}
|
|
192
179
|
function isBrokerAlreadyRunningError(message) {
|
|
193
180
|
return /another broker instance is already running in this directory/i.test(message);
|
|
194
181
|
}
|
|
@@ -543,420 +530,6 @@ async function waitForBrokerReadiness(paths, deps, waitMs, requireApi) {
|
|
|
543
530
|
}
|
|
544
531
|
return latest;
|
|
545
532
|
}
|
|
546
|
-
function pickDashboardStaticDir(candidates, deps) {
|
|
547
|
-
const existingCandidates = Array.from(new Set(candidates)).filter((candidate) => deps.fs.existsSync(candidate));
|
|
548
|
-
if (existingCandidates.length === 0) {
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
const pageMarkerPriority = [
|
|
552
|
-
['metrics.html', path.join('metrics', 'index.html')],
|
|
553
|
-
['app.html'],
|
|
554
|
-
['index.html'],
|
|
555
|
-
];
|
|
556
|
-
for (const markerGroup of pageMarkerPriority) {
|
|
557
|
-
const withMarker = existingCandidates.find((candidate) => markerGroup.some((marker) => deps.fs.existsSync(path.join(candidate, marker))));
|
|
558
|
-
if (withMarker) {
|
|
559
|
-
return withMarker;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return existingCandidates[0];
|
|
563
|
-
}
|
|
564
|
-
function getHomeDashboardRoot(deps) {
|
|
565
|
-
const homeDir = deps.env.HOME || deps.env.USERPROFILE || os.homedir();
|
|
566
|
-
return path.join(homeDir, '.agentworkforce/relay', 'dashboard');
|
|
567
|
-
}
|
|
568
|
-
function getPriorDashboardRoot(deps) {
|
|
569
|
-
const homeDir = deps.env.HOME || deps.env.USERPROFILE || '';
|
|
570
|
-
if (!homeDir) {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
return path.join(homeDir, '.relay', 'dashboard');
|
|
574
|
-
}
|
|
575
|
-
function getDashboardRootFromBinary(dashboardBinary, deps) {
|
|
576
|
-
if (!dashboardBinary || dashboardBinary.endsWith('.js') || dashboardBinary.endsWith('.ts')) {
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
const binaryDir = path.dirname(dashboardBinary);
|
|
580
|
-
if (path.basename(binaryDir) !== 'bin') {
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
const homeDir = deps.env.HOME || deps.env.USERPROFILE || '';
|
|
584
|
-
const resolvedBinaryDir = path.resolve(binaryDir);
|
|
585
|
-
const ignoredBinDirs = [
|
|
586
|
-
homeDir ? path.join(homeDir, '.local', 'bin') : null,
|
|
587
|
-
path.join('/usr/local', 'bin'),
|
|
588
|
-
]
|
|
589
|
-
.filter((candidate) => Boolean(candidate))
|
|
590
|
-
.map((candidate) => path.resolve(candidate));
|
|
591
|
-
if (ignoredBinDirs.includes(resolvedBinaryDir)) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
return path.join(path.dirname(binaryDir), 'dashboard');
|
|
595
|
-
}
|
|
596
|
-
function resolveDashboardStaticDir(dashboardBinary, deps) {
|
|
597
|
-
const explicitStaticDir = deps.env.RELAY_DASHBOARD_STATIC_DIR ?? deps.env.STATIC_DIR;
|
|
598
|
-
if (explicitStaticDir && explicitStaticDir.trim()) {
|
|
599
|
-
return explicitStaticDir;
|
|
600
|
-
}
|
|
601
|
-
if (!dashboardBinary) {
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
if (dashboardBinary.endsWith('.js') || dashboardBinary.endsWith('.ts')) {
|
|
605
|
-
const dashboardServerOutDir = path.resolve(path.dirname(dashboardBinary), '..', 'out');
|
|
606
|
-
const siblingDashboardOutDir = path.resolve(path.dirname(dashboardBinary), '..', '..', 'dashboard', 'out');
|
|
607
|
-
return pickDashboardStaticDir([dashboardServerOutDir, siblingDashboardOutDir], deps);
|
|
608
|
-
}
|
|
609
|
-
// Installs place UI assets under the install dir (~/.agentworkforce/relay/dashboard/out
|
|
610
|
-
// by default, or next to a custom install's bin/ directory). ~/.relay/dashboard/out is
|
|
611
|
-
// read as a fallback for installs predating that move.
|
|
612
|
-
const installDashboardRoot = getDashboardRootFromBinary(dashboardBinary, deps);
|
|
613
|
-
const priorDashboardRoot = getPriorDashboardRoot(deps);
|
|
614
|
-
const candidates = [
|
|
615
|
-
installDashboardRoot ? path.join(installDashboardRoot, 'out') : null,
|
|
616
|
-
path.join(getHomeDashboardRoot(deps), 'out'),
|
|
617
|
-
priorDashboardRoot ? path.join(priorDashboardRoot, 'out') : null,
|
|
618
|
-
].filter((candidate) => Boolean(candidate));
|
|
619
|
-
return pickDashboardStaticDir(candidates, deps);
|
|
620
|
-
}
|
|
621
|
-
function normalizeLocalhostRelayUrl(relayUrl) {
|
|
622
|
-
try {
|
|
623
|
-
const parsed = new URL(relayUrl);
|
|
624
|
-
if (parsed.hostname === 'localhost') {
|
|
625
|
-
parsed.hostname = '127.0.0.1';
|
|
626
|
-
}
|
|
627
|
-
return parsed.toString().replace(/\/+$/, '');
|
|
628
|
-
}
|
|
629
|
-
catch {
|
|
630
|
-
return relayUrl;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
function getDefaultDashboardRelayUrl(apiPort) {
|
|
634
|
-
return normalizeLocalhostRelayUrl(`http://localhost:${apiPort}`);
|
|
635
|
-
}
|
|
636
|
-
function resolveDashboardRelayUrl(apiPort, deps) {
|
|
637
|
-
const explicitRelayUrl = deps.env.RELAY_DASHBOARD_RELAY_URL;
|
|
638
|
-
if (explicitRelayUrl && explicitRelayUrl.trim()) {
|
|
639
|
-
return normalizeLocalhostRelayUrl(explicitRelayUrl.trim());
|
|
640
|
-
}
|
|
641
|
-
return getDefaultDashboardRelayUrl(apiPort);
|
|
642
|
-
}
|
|
643
|
-
function isDebugLikeLoggingEnabled(deps) {
|
|
644
|
-
const rawLevel = String(deps.env.RUST_LOG ?? '').toLowerCase();
|
|
645
|
-
return rawLevel.includes('debug') || rawLevel.includes('trace');
|
|
646
|
-
}
|
|
647
|
-
function getDashboardSpawnEnv(deps, relayUrl, enableVerboseLogging, relayApiKey, brokerApiKey) {
|
|
648
|
-
const env = {
|
|
649
|
-
...deps.env,
|
|
650
|
-
RELAY_URL: relayUrl,
|
|
651
|
-
VERBOSE: enableVerboseLogging || deps.env.VERBOSE === 'true' ? 'true' : deps.env.VERBOSE,
|
|
652
|
-
};
|
|
653
|
-
// Pass the workspace key so the dashboard can make Agent Relay calls
|
|
654
|
-
// (e.g. posting thread replies) without requiring a relaycast.json file.
|
|
655
|
-
if (relayApiKey) {
|
|
656
|
-
if (!env.RELAY_WORKSPACE_KEY) {
|
|
657
|
-
env.RELAY_WORKSPACE_KEY = relayApiKey;
|
|
658
|
-
}
|
|
659
|
-
if (!env.RELAY_API_KEY) {
|
|
660
|
-
env.RELAY_API_KEY = relayApiKey;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Pass the broker API key so the dashboard can authenticate with the
|
|
664
|
-
// broker's HTTP API (e.g. /api/spawn, /api/spawned).
|
|
665
|
-
if (brokerApiKey) {
|
|
666
|
-
env.RELAY_BROKER_API_KEY = brokerApiKey;
|
|
667
|
-
}
|
|
668
|
-
return env;
|
|
669
|
-
}
|
|
670
|
-
function getDashboardSpawnArgs(paths, port, apiPort, dashboardBinary, relayUrl, enableVerboseLogging, deps) {
|
|
671
|
-
const args = ['--port', String(port), '--data-dir', paths.dataDir];
|
|
672
|
-
args.push('--relay-url', relayUrl);
|
|
673
|
-
const staticDir = resolveDashboardStaticDir(dashboardBinary, deps);
|
|
674
|
-
if (staticDir) {
|
|
675
|
-
args.push('--static-dir', staticDir);
|
|
676
|
-
}
|
|
677
|
-
if (enableVerboseLogging) {
|
|
678
|
-
args.push('--verbose');
|
|
679
|
-
}
|
|
680
|
-
return args;
|
|
681
|
-
}
|
|
682
|
-
function normalizeDashboardPath(rawDashboardPath) {
|
|
683
|
-
const trimmed = rawDashboardPath?.trim();
|
|
684
|
-
if (!trimmed)
|
|
685
|
-
return undefined;
|
|
686
|
-
if (trimmed.startsWith('/')) {
|
|
687
|
-
return trimmed;
|
|
688
|
-
}
|
|
689
|
-
return `/${trimmed}`;
|
|
690
|
-
}
|
|
691
|
-
function startDashboard(paths, port, apiPort, deps, enableVerboseLogging, dashboardBinaryOverride, relayApiKey, brokerApiKey) {
|
|
692
|
-
const dashboardBinary = dashboardBinaryOverride === undefined ? deps.findDashboardBinary() : dashboardBinaryOverride;
|
|
693
|
-
const relayUrl = resolveDashboardRelayUrl(apiPort, deps);
|
|
694
|
-
const shouldEnableVerbose = enableVerboseLogging || isDebugLikeLoggingEnabled(deps);
|
|
695
|
-
const args = getDashboardSpawnArgs(paths, port, apiPort, dashboardBinary, relayUrl, shouldEnableVerbose, deps);
|
|
696
|
-
const launchTarget = dashboardBinary
|
|
697
|
-
? dashboardBinary.endsWith('.js')
|
|
698
|
-
? `node ${dashboardBinary}`
|
|
699
|
-
: dashboardBinary
|
|
700
|
-
: 'npx --yes @agent-relay/dashboard-server@latest';
|
|
701
|
-
const spawnOpts = {
|
|
702
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
703
|
-
env: getDashboardSpawnEnv(deps, relayUrl, shouldEnableVerbose, relayApiKey, brokerApiKey),
|
|
704
|
-
};
|
|
705
|
-
if (shouldEnableVerbose) {
|
|
706
|
-
deps.log(`[dashboard] Starting: ${launchTarget} ${args.join(' ')}`);
|
|
707
|
-
}
|
|
708
|
-
let child;
|
|
709
|
-
if (dashboardBinary) {
|
|
710
|
-
// If the binary is a .js file (local dev), run it with node
|
|
711
|
-
if (dashboardBinary.endsWith('.js')) {
|
|
712
|
-
child = deps.spawnProcess('node', [dashboardBinary, ...args], spawnOpts);
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
child = deps.spawnProcess(dashboardBinary, args, spawnOpts);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
child = deps.spawnProcess('npx', ['--yes', '@agent-relay/dashboard-server@latest', ...args], spawnOpts);
|
|
720
|
-
}
|
|
721
|
-
// Capture stderr for error reporting
|
|
722
|
-
const childAny = child;
|
|
723
|
-
let stderrBuf = '';
|
|
724
|
-
const logChunk = (chunk, logger, prefix) => {
|
|
725
|
-
if (!shouldEnableVerbose) {
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
const text = chunk.toString();
|
|
729
|
-
for (const line of text.split(/\r?\n/)) {
|
|
730
|
-
const trimmed = line.trim();
|
|
731
|
-
if (trimmed) {
|
|
732
|
-
logger(`[dashboard] ${prefix}: ${trimmed}`);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
childAny.stdout?.on?.('data', (chunk) => {
|
|
737
|
-
logChunk(chunk, deps.log, 'stdout');
|
|
738
|
-
});
|
|
739
|
-
childAny.stderr?.on?.('data', (chunk) => {
|
|
740
|
-
stderrBuf += chunk.toString();
|
|
741
|
-
logChunk(chunk, deps.warn, 'stderr');
|
|
742
|
-
});
|
|
743
|
-
// Report early crashes
|
|
744
|
-
childAny.on?.('exit', (...exitArgs) => {
|
|
745
|
-
const code = exitArgs[0];
|
|
746
|
-
const signal = exitArgs[1];
|
|
747
|
-
if (code !== null && code !== 0) {
|
|
748
|
-
deps.error(`Dashboard process exited with code ${code}`);
|
|
749
|
-
if (stderrBuf.trim()) {
|
|
750
|
-
deps.error(stderrBuf.trim().split('\n').slice(-5).join('\n'));
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
else if (signal && signal !== 'SIGINT' && signal !== 'SIGTERM') {
|
|
754
|
-
deps.error(`Dashboard process killed by signal ${signal}`);
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
return child;
|
|
758
|
-
}
|
|
759
|
-
async function resolveStartedDashboardPort(process, preferredPort, deps) {
|
|
760
|
-
return new Promise((resolve) => {
|
|
761
|
-
let resolved = false;
|
|
762
|
-
const processAny = process;
|
|
763
|
-
const detach = () => {
|
|
764
|
-
process.stdout?.off?.('data', extractPort);
|
|
765
|
-
process.stdout?.removeListener?.('data', extractPort);
|
|
766
|
-
process.stderr?.off?.('data', extractPort);
|
|
767
|
-
process.stderr?.removeListener?.('data', extractPort);
|
|
768
|
-
processAny.off?.('exit', handleExit);
|
|
769
|
-
processAny.removeListener?.('exit', handleExit);
|
|
770
|
-
clearTimeout(timer);
|
|
771
|
-
};
|
|
772
|
-
const timer = setTimeout(() => {
|
|
773
|
-
if (resolved)
|
|
774
|
-
return;
|
|
775
|
-
resolved = true;
|
|
776
|
-
detach();
|
|
777
|
-
deps.warn(`Dashboard did not report its bound port quickly; assuming requested port ${preferredPort}`);
|
|
778
|
-
resolve(preferredPort);
|
|
779
|
-
}, 3000);
|
|
780
|
-
const finalize = (port) => {
|
|
781
|
-
if (resolved)
|
|
782
|
-
return;
|
|
783
|
-
resolved = true;
|
|
784
|
-
detach();
|
|
785
|
-
resolve(port);
|
|
786
|
-
};
|
|
787
|
-
const handleExit = (...exitArgs) => {
|
|
788
|
-
const code = exitArgs[0];
|
|
789
|
-
const signal = exitArgs[1];
|
|
790
|
-
if (resolved) {
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
resolved = true;
|
|
794
|
-
detach();
|
|
795
|
-
if (code !== null && code !== 0) {
|
|
796
|
-
deps.warn(`Dashboard exited before reporting its port (code: ${code}).`);
|
|
797
|
-
}
|
|
798
|
-
else if (signal && signal !== 'SIGINT' && signal !== 'SIGTERM') {
|
|
799
|
-
deps.warn(`Dashboard exited before reporting its port (signal: ${signal}).`);
|
|
800
|
-
}
|
|
801
|
-
else {
|
|
802
|
-
deps.warn('Dashboard exited before reporting its bound port.');
|
|
803
|
-
}
|
|
804
|
-
resolve(null);
|
|
805
|
-
};
|
|
806
|
-
const extractPort = (...chunkArgs) => {
|
|
807
|
-
const firstChunk = chunkArgs[0];
|
|
808
|
-
if (!firstChunk) {
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
const chunk = Buffer.isBuffer(firstChunk)
|
|
812
|
-
? firstChunk
|
|
813
|
-
: typeof firstChunk === 'string'
|
|
814
|
-
? Buffer.from(firstChunk)
|
|
815
|
-
: Buffer.from(JSON.stringify(firstChunk));
|
|
816
|
-
const match = chunk.toString().match(/Server running at http:\/\/localhost:(\d+)/i);
|
|
817
|
-
if (!match?.[1]) {
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const parsed = Number.parseInt(match[1], 10);
|
|
821
|
-
if (!Number.isNaN(parsed)) {
|
|
822
|
-
finalize(parsed);
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
process.stdout?.on?.('data', extractPort);
|
|
826
|
-
process.stderr?.on?.('data', extractPort);
|
|
827
|
-
processAny.on?.('exit', handleExit);
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
/**
|
|
831
|
-
* Check if the cached dashboard UI assets match the installed dashboard-server
|
|
832
|
-
* binary version. If they are stale (or missing a version marker), re-download
|
|
833
|
-
* the latest assets from the relay-dashboard GitHub release.
|
|
834
|
-
*/
|
|
835
|
-
async function refreshDashboardAssetsIfStale(dashboardBinary, deps) {
|
|
836
|
-
if (!dashboardBinary || dashboardBinary.endsWith('.js') || dashboardBinary.endsWith('.ts')) {
|
|
837
|
-
// Dev mode or npx — skip
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
// Get installed binary version (async to avoid blocking event loop)
|
|
841
|
-
let binaryVersion;
|
|
842
|
-
try {
|
|
843
|
-
const versionResult = await deps.execCommand(`${JSON.stringify(dashboardBinary)} --version`);
|
|
844
|
-
binaryVersion = versionResult.stdout.trim();
|
|
845
|
-
}
|
|
846
|
-
catch {
|
|
847
|
-
return; // Can't determine version — skip
|
|
848
|
-
}
|
|
849
|
-
if (!binaryVersion) {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const targetDir = getDashboardRootFromBinary(dashboardBinary, deps) ?? getHomeDashboardRoot(deps);
|
|
853
|
-
const assetsDir = path.join(targetDir, 'out');
|
|
854
|
-
const versionFile = path.join(targetDir, '.version');
|
|
855
|
-
// Check if assets match the binary version
|
|
856
|
-
try {
|
|
857
|
-
const cachedVersion = deps.fs.readFileSync(versionFile, 'utf-8').trim();
|
|
858
|
-
if (cachedVersion === binaryVersion) {
|
|
859
|
-
return; // Up to date
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
catch {
|
|
863
|
-
// No version file — need to download if assets exist but are unversioned,
|
|
864
|
-
// or if assets don't exist at all
|
|
865
|
-
if (deps.fs.existsSync(assetsDir)) {
|
|
866
|
-
// Assets exist but no version marker — they're from an old install
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
// No assets at all — need to download
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
deps.log(`Updating dashboard UI assets (${binaryVersion})...`);
|
|
873
|
-
const uiUrl = 'https://github.com/AgentWorkforce/relay-dashboard/releases/latest/download/dashboard-ui.tar.gz';
|
|
874
|
-
let tempDir;
|
|
875
|
-
let tempFile;
|
|
876
|
-
try {
|
|
877
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `dashboard-ui-${deps.pid}-`));
|
|
878
|
-
tempFile = path.join(tempDir, 'dashboard-ui.tar.gz');
|
|
879
|
-
// Download (async to avoid blocking event loop during network I/O)
|
|
880
|
-
await deps.execCommand(`curl -fsSL --max-time 30 ${JSON.stringify(uiUrl)} -o ${JSON.stringify(tempFile)}`);
|
|
881
|
-
// Verify it's a valid gzip
|
|
882
|
-
const header = Buffer.alloc(2);
|
|
883
|
-
const fd = fs.openSync(tempFile, 'r');
|
|
884
|
-
fs.readSync(fd, header, 0, 2, 0);
|
|
885
|
-
fs.closeSync(fd);
|
|
886
|
-
if (header[0] !== 0x1f || header[1] !== 0x8b) {
|
|
887
|
-
if (tempFile)
|
|
888
|
-
deps.fs.unlinkSync(tempFile);
|
|
889
|
-
return; // Not a valid gzip file
|
|
890
|
-
}
|
|
891
|
-
// Remove old assets and extract (async to avoid blocking event loop)
|
|
892
|
-
deps.fs.rmSync(assetsDir, { recursive: true, force: true });
|
|
893
|
-
deps.fs.mkdirSync(targetDir, { recursive: true });
|
|
894
|
-
await deps.execCommand(`tar -xzf ${JSON.stringify(tempFile)} -C ${JSON.stringify(targetDir)}`);
|
|
895
|
-
if (tempFile)
|
|
896
|
-
deps.fs.unlinkSync(tempFile);
|
|
897
|
-
// Write version marker only after confirming extraction succeeded
|
|
898
|
-
if (deps.fs.existsSync(path.join(assetsDir, 'index.html'))) {
|
|
899
|
-
deps.fs.writeFileSync(versionFile, binaryVersion);
|
|
900
|
-
deps.log(`Dashboard UI assets updated to ${binaryVersion}`);
|
|
901
|
-
}
|
|
902
|
-
else {
|
|
903
|
-
deps.warn('Dashboard UI extraction may be incomplete — skipping version marker');
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
catch {
|
|
907
|
-
// Best-effort — don't block startup
|
|
908
|
-
try {
|
|
909
|
-
if (tempFile)
|
|
910
|
-
deps.fs.unlinkSync(tempFile);
|
|
911
|
-
}
|
|
912
|
-
catch {
|
|
913
|
-
/* ignore */
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
finally {
|
|
917
|
-
try {
|
|
918
|
-
if (tempDir)
|
|
919
|
-
deps.fs.rmSync(tempDir, { recursive: true, force: true });
|
|
920
|
-
}
|
|
921
|
-
catch {
|
|
922
|
-
/* ignore */
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
async function startDashboardWithFallback(paths, dashboardPort, apiPort, deps, enableVerboseLogging, relayApiKey, brokerApiKey) {
|
|
927
|
-
const preferredBinary = deps.findDashboardBinary();
|
|
928
|
-
await refreshDashboardAssetsIfStale(preferredBinary, deps);
|
|
929
|
-
let process = startDashboard(paths, dashboardPort, apiPort, deps, enableVerboseLogging, preferredBinary, relayApiKey, brokerApiKey);
|
|
930
|
-
let port = await resolveStartedDashboardPort(process, dashboardPort, deps);
|
|
931
|
-
if (port === null && preferredBinary) {
|
|
932
|
-
deps.warn('Retrying dashboard startup using npx @agent-relay/dashboard-server@latest');
|
|
933
|
-
process = startDashboard(paths, dashboardPort, apiPort, deps, enableVerboseLogging, null, relayApiKey, brokerApiKey);
|
|
934
|
-
port = await resolveStartedDashboardPort(process, dashboardPort, deps);
|
|
935
|
-
}
|
|
936
|
-
return { process, port };
|
|
937
|
-
}
|
|
938
|
-
async function waitForDashboard(port, process, deps, isShuttingDown) {
|
|
939
|
-
for (let i = 0; i < 20; i++) {
|
|
940
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
941
|
-
if (process.killed) {
|
|
942
|
-
if (!isShuttingDown()) {
|
|
943
|
-
deps.warn(`Warning: Dashboard process exited before becoming ready on port ${port}`);
|
|
944
|
-
}
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
try {
|
|
948
|
-
const resp = await fetch(`http://localhost:${port}/health`);
|
|
949
|
-
if (resp.ok)
|
|
950
|
-
return; // Dashboard is up
|
|
951
|
-
}
|
|
952
|
-
catch {
|
|
953
|
-
// Not ready yet
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
if (!isShuttingDown()) {
|
|
957
|
-
deps.warn(`Warning: Dashboard not responding on port ${port} after 10s`);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
533
|
async function discoverExistingBrokerApiPort(preferredApiPort, maxAttempts, deps) {
|
|
961
534
|
const attempts = Math.max(1, maxAttempts);
|
|
962
535
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|