agent-relay-server 0.4.39 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/package.json +1 -1
- package/public/dashboard.js +233 -17
- package/public/icons/agent-relay-192.png +0 -0
- package/public/icons/agent-relay-512.png +0 -0
- package/public/icons/agent-relay.svg +14 -0
- package/public/index.html +276 -4
- package/public/manifest.webmanifest +33 -0
- package/public/sw.js +58 -0
- package/src/cli.ts +80 -17
- package/src/connectors.ts +256 -0
- package/src/db.ts +544 -25
- package/src/index.ts +25 -1
- package/src/routes.ts +632 -26
- package/src/security.ts +2 -1
- package/src/sse.ts +21 -1
- package/src/types.ts +152 -3
package/public/index.html
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="theme-color" content="#0d1117">
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Agent Relay">
|
|
10
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
6
11
|
<title>Agent Relay</title>
|
|
7
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230d1117'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
|
|
13
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
14
|
+
<link rel="apple-touch-icon" href="/icons/agent-relay-192.png">
|
|
8
15
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css">
|
|
9
16
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
17
|
<style>
|
|
@@ -210,10 +217,20 @@
|
|
|
210
217
|
<span class="badge bg-success text-white" x-show="onlineCount > 0" x-text="onlineCount"></span>
|
|
211
218
|
</span>
|
|
212
219
|
</a>
|
|
220
|
+
<a href="#" class="nav-link" :class="{ active: view === 'orchestrators' }" @click.prevent="switchView('orchestrators')">
|
|
221
|
+
<i class="ti ti-server-2"></i>Orchestrators
|
|
222
|
+
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
223
|
+
<span class="badge bg-success text-white" x-show="orchestrators.filter(o => o.status === 'online').length > 0" x-text="orchestrators.filter(o => o.status === 'online').length"></span>
|
|
224
|
+
</span>
|
|
225
|
+
</a>
|
|
213
226
|
<a href="#" class="nav-link" :class="{ active: view === 'channels' }" @click.prevent="switchView('channels')">
|
|
214
227
|
<i class="ti ti-messages"></i>Channels
|
|
215
228
|
<span class="badge bg-success text-white ms-auto" x-show="channelCards.filter((item) => item.ready).length > 0" x-text="channelCards.filter((item) => item.ready).length"></span>
|
|
216
229
|
</a>
|
|
230
|
+
<a href="#" class="nav-link" :class="{ active: view === 'connectors' }" @click.prevent="switchView('connectors')">
|
|
231
|
+
<i class="ti ti-plug"></i>Connectors
|
|
232
|
+
<span class="badge bg-secondary text-white ms-auto" x-show="connectorCards.length > 0" x-text="connectorCards.length"></span>
|
|
233
|
+
</a>
|
|
217
234
|
<a href="#" class="nav-link" :class="{ active: view === 'integrations' }" @click.prevent="switchView('integrations')">
|
|
218
235
|
<i class="ti ti-plug-connected"></i>Integrations
|
|
219
236
|
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
@@ -286,7 +303,7 @@
|
|
|
286
303
|
|
|
287
304
|
<!-- Mobile nav -->
|
|
288
305
|
<div class="mobile-nav d-none border-bottom p-2 gap-1 position-fixed top-0 w-100 bg-dark" style="z-index:50">
|
|
289
|
-
<template x-for="v in ['overview','agents','channels','integrations','inbox','activity','pairs','messages','work','tasks','analytics']">
|
|
306
|
+
<template x-for="v in ['overview','agents','channels','connectors','integrations','inbox','activity','pairs','messages','work','tasks','analytics']">
|
|
290
307
|
<button class="btn btn-sm" :class="view === v ? 'btn-primary' : 'btn-ghost-secondary'" @click="switchView(v)" x-text="v.charAt(0).toUpperCase() + v.slice(1)"></button>
|
|
291
308
|
</template>
|
|
292
309
|
</div>
|
|
@@ -658,6 +675,185 @@
|
|
|
658
675
|
</template>
|
|
659
676
|
</div>
|
|
660
677
|
|
|
678
|
+
<!-- ==================== ORCHESTRATORS ==================== -->
|
|
679
|
+
<div x-show="view === 'orchestrators'" x-cloak class="fade-in">
|
|
680
|
+
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
681
|
+
<h2 class="page-title mb-0">Orchestrators</h2>
|
|
682
|
+
<span class="badge bg-success-lt" x-show="orchestrators.filter(o => o.status === 'online').length > 0"
|
|
683
|
+
x-text="orchestrators.filter(o => o.status === 'online').length + ' online'"></span>
|
|
684
|
+
<div class="ms-auto">
|
|
685
|
+
<button class="btn btn-sm btn-primary" @click="openOrchestratorSpawn()" :disabled="orchestrators.filter(o => o.status === 'online').length === 0">
|
|
686
|
+
<i class="ti ti-plus me-1"></i>Spawn Agent
|
|
687
|
+
</button>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
|
|
691
|
+
<template x-if="orchestrators.length > 0">
|
|
692
|
+
<div class="row g-3">
|
|
693
|
+
<template x-for="orch in orchestrators" :key="orch.id">
|
|
694
|
+
<div class="col-12 col-lg-6">
|
|
695
|
+
<div class="card">
|
|
696
|
+
<div class="card-body">
|
|
697
|
+
<div class="d-flex align-items-center mb-3">
|
|
698
|
+
<i class="ti ti-server-2 me-2" style="font-size: 24px"></i>
|
|
699
|
+
<div>
|
|
700
|
+
<h3 class="mb-0" x-text="orch.hostname"></h3>
|
|
701
|
+
<small class="text-secondary" x-text="orch.id"></small>
|
|
702
|
+
</div>
|
|
703
|
+
<span class="ms-auto badge" :class="orch.status === 'online' ? 'bg-success' : 'bg-secondary'" x-text="orch.status"></span>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<div class="d-flex gap-3 mb-3 text-secondary" style="font-size: 13px">
|
|
707
|
+
<span><i class="ti ti-folder me-1"></i><span x-text="orch.baseDir"></span></span>
|
|
708
|
+
<span><i class="ti ti-key me-1"></i><span x-text="(orch.envKeys?.length || 0) + ' env vars'"></span></span>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div class="d-flex gap-1 mb-3">
|
|
712
|
+
<template x-for="provider in orch.providers" :key="provider">
|
|
713
|
+
<span class="badge" :class="provider === 'claude' ? 'bg-orange-lt' : 'bg-blue-lt'" x-text="provider"></span>
|
|
714
|
+
</template>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
<template x-if="orch.managedAgents?.length > 0">
|
|
718
|
+
<div>
|
|
719
|
+
<h4 class="mb-2" style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--tblr-secondary)">
|
|
720
|
+
Managed Agents (<span x-text="orch.managedAgents.length"></span>)
|
|
721
|
+
</h4>
|
|
722
|
+
<div class="list-group list-group-flush">
|
|
723
|
+
<template x-for="agent in orch.managedAgents" :key="agent.tmuxSession">
|
|
724
|
+
<div class="list-group-item px-0 py-2 d-flex align-items-center gap-2">
|
|
725
|
+
<span class="badge" :class="agent.provider === 'claude' ? 'bg-orange-lt' : 'bg-blue-lt'" x-text="agent.provider" style="font-size: 10px"></span>
|
|
726
|
+
<span x-text="agent.label || agent.tmuxSession" style="font-size: 13px"></span>
|
|
727
|
+
<small class="text-secondary" x-text="agent.cwd" style="font-size: 11px"></small>
|
|
728
|
+
<div class="ms-auto d-flex gap-1">
|
|
729
|
+
<button class="btn btn-sm btn-ghost-warning p-1" title="Restart"
|
|
730
|
+
@click="orchestratorAction(orch.id, 'restart', agent.agentId || agent.tmuxSession)">
|
|
731
|
+
<i class="ti ti-refresh" style="font-size: 14px"></i>
|
|
732
|
+
</button>
|
|
733
|
+
<button class="btn btn-sm btn-ghost-danger p-1" title="Shutdown"
|
|
734
|
+
@click="orchestratorAction(orch.id, 'shutdown', agent.agentId || agent.tmuxSession)">
|
|
735
|
+
<i class="ti ti-power" style="font-size: 14px"></i>
|
|
736
|
+
</button>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
</template>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
</template>
|
|
743
|
+
<template x-if="!orch.managedAgents?.length">
|
|
744
|
+
<p class="text-secondary mb-0" style="font-size: 13px">No managed agents</p>
|
|
745
|
+
</template>
|
|
746
|
+
|
|
747
|
+
<div class="d-flex gap-1 mt-3">
|
|
748
|
+
<button class="btn btn-sm btn-primary" @click="openOrchestratorSpawnFor(orch.id)" :disabled="orch.status !== 'online'">
|
|
749
|
+
<i class="ti ti-plus me-1"></i>Spawn
|
|
750
|
+
</button>
|
|
751
|
+
<button class="btn btn-sm btn-ghost-danger" @click="deleteOrchestrator(orch.id)">
|
|
752
|
+
<i class="ti ti-trash me-1"></i>Remove
|
|
753
|
+
</button>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
<div class="card-footer text-secondary" style="font-size: 12px">
|
|
757
|
+
Last seen: <span x-text="timeAgo(orch.lastSeen)"></span>
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</template>
|
|
762
|
+
</div>
|
|
763
|
+
</template>
|
|
764
|
+
|
|
765
|
+
<template x-if="orchestrators.length === 0">
|
|
766
|
+
<div class="card">
|
|
767
|
+
<div class="card-body text-center py-5">
|
|
768
|
+
<i class="ti ti-server-2 mb-3" style="font-size: 48px; color: var(--tblr-secondary)"></i>
|
|
769
|
+
<h3>No orchestrators registered</h3>
|
|
770
|
+
<p class="text-secondary">Install and start <code>agent-relay-orchestrator</code> on each host to enable remote agent spawning.</p>
|
|
771
|
+
<pre class="text-start mx-auto" style="max-width: 500px; font-size: 12px">npm install -g agent-relay-orchestrator
|
|
772
|
+
agent-relay-orchestrator init
|
|
773
|
+
agent-relay-orchestrator</pre>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
</template>
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
<!-- Orchestrator Spawn Modal -->
|
|
780
|
+
<div class="modal" :class="{ show: orchestratorSpawnOpen }" tabindex="-1" :style="orchestratorSpawnOpen ? 'display:block' : 'display:none'" @click.self="orchestratorSpawnOpen = false">
|
|
781
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
782
|
+
<div class="modal-content">
|
|
783
|
+
<div class="modal-header">
|
|
784
|
+
<h5 class="modal-title">Spawn Agent</h5>
|
|
785
|
+
<button type="button" class="btn-close" @click="orchestratorSpawnOpen = false"></button>
|
|
786
|
+
</div>
|
|
787
|
+
<div class="modal-body">
|
|
788
|
+
<div class="mb-3">
|
|
789
|
+
<label class="form-label">Orchestrator</label>
|
|
790
|
+
<select class="form-select" x-model="spawnOrchId">
|
|
791
|
+
<template x-for="orch in orchestrators.filter(o => o.status === 'online')" :key="orch.id">
|
|
792
|
+
<option :value="orch.id" x-text="orch.hostname + ' (' + orch.providers.join(', ') + ')'"></option>
|
|
793
|
+
</template>
|
|
794
|
+
</select>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="mb-3">
|
|
797
|
+
<label class="form-label">Provider</label>
|
|
798
|
+
<select class="form-select" x-model="spawnProvider">
|
|
799
|
+
<template x-for="p in spawnAvailableProviders" :key="p">
|
|
800
|
+
<option :value="p" x-text="p"></option>
|
|
801
|
+
</template>
|
|
802
|
+
</select>
|
|
803
|
+
</div>
|
|
804
|
+
<div class="mb-3">
|
|
805
|
+
<label class="form-label">Working Directory</label>
|
|
806
|
+
<div class="input-group">
|
|
807
|
+
<input type="text" class="form-control" x-model="spawnCwd" placeholder="Leave empty for base directory">
|
|
808
|
+
<button class="btn btn-outline-secondary" @click="browseOrchestratorDirs()" type="button">
|
|
809
|
+
<i class="ti ti-folder"></i>
|
|
810
|
+
</button>
|
|
811
|
+
</div>
|
|
812
|
+
<template x-if="spawnDirListing">
|
|
813
|
+
<div class="border rounded mt-2 p-2" style="max-height: 200px; overflow-y: auto; font-size: 13px">
|
|
814
|
+
<template x-if="spawnDirListing.parent">
|
|
815
|
+
<a href="#" class="d-block py-1 text-secondary" @click.prevent="spawnCwd = spawnDirListing.parent; browseOrchestratorDirs()">
|
|
816
|
+
<i class="ti ti-arrow-up me-1"></i>..
|
|
817
|
+
</a>
|
|
818
|
+
</template>
|
|
819
|
+
<template x-for="entry in spawnDirListing.entries" :key="entry.path">
|
|
820
|
+
<a href="#" class="d-block py-1" @click.prevent="spawnCwd = entry.path; browseOrchestratorDirs()">
|
|
821
|
+
<i class="ti ti-folder me-1"></i><span x-text="entry.name"></span>
|
|
822
|
+
</a>
|
|
823
|
+
</template>
|
|
824
|
+
</div>
|
|
825
|
+
</template>
|
|
826
|
+
</div>
|
|
827
|
+
<div class="mb-3">
|
|
828
|
+
<label class="form-label">Label</label>
|
|
829
|
+
<input type="text" class="form-control" x-model="spawnLabel" placeholder="e.g. backend, reviewer">
|
|
830
|
+
</div>
|
|
831
|
+
<div class="mb-3">
|
|
832
|
+
<label class="form-label">Approval Mode</label>
|
|
833
|
+
<select class="form-select" x-model="spawnApproval">
|
|
834
|
+
<option value="guarded">Guarded (block destructive ops)</option>
|
|
835
|
+
<option value="open">Open (no restrictions)</option>
|
|
836
|
+
<option value="read-only">Read-only (observe only)</option>
|
|
837
|
+
</select>
|
|
838
|
+
</div>
|
|
839
|
+
<template x-if="spawnProvider === 'claude'">
|
|
840
|
+
<div class="mb-3">
|
|
841
|
+
<label class="form-label">Initial Prompt</label>
|
|
842
|
+
<textarea class="form-control" x-model="spawnPrompt" rows="3" placeholder="You are a headless relay agent..."></textarea>
|
|
843
|
+
</div>
|
|
844
|
+
</template>
|
|
845
|
+
</div>
|
|
846
|
+
<div class="modal-footer">
|
|
847
|
+
<button class="btn btn-secondary" @click="orchestratorSpawnOpen = false">Cancel</button>
|
|
848
|
+
<button class="btn btn-primary" @click="submitOrchestratorSpawn()">
|
|
849
|
+
<i class="ti ti-rocket me-1"></i>Spawn
|
|
850
|
+
</button>
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="modal-backdrop fade show" x-show="orchestratorSpawnOpen" x-cloak></div>
|
|
856
|
+
|
|
661
857
|
<!-- ==================== CHANNELS ==================== -->
|
|
662
858
|
<div x-show="view === 'channels'" x-cloak class="fade-in">
|
|
663
859
|
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
@@ -726,6 +922,75 @@
|
|
|
726
922
|
</template>
|
|
727
923
|
</div>
|
|
728
924
|
|
|
925
|
+
<!-- ==================== CONNECTORS ==================== -->
|
|
926
|
+
<div x-show="view === 'connectors'" x-cloak class="fade-in">
|
|
927
|
+
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
928
|
+
<h2 class="page-title mb-0">Connectors</h2>
|
|
929
|
+
<span class="badge bg-secondary-lt" x-show="connectorCards.length > 0" x-text="connectorCards.length + ' registered'"></span>
|
|
930
|
+
<div class="ms-auto">
|
|
931
|
+
<button class="btn btn-sm btn-ghost-secondary" @click="fetchConnectors()" title="Refresh">
|
|
932
|
+
<i class="ti ti-refresh"></i>
|
|
933
|
+
</button>
|
|
934
|
+
</div>
|
|
935
|
+
</div>
|
|
936
|
+
|
|
937
|
+
<div class="row g-3">
|
|
938
|
+
<template x-for="connector in connectorCards" :key="connector.id">
|
|
939
|
+
<div class="col-md-6 col-xl-4">
|
|
940
|
+
<div class="card">
|
|
941
|
+
<div class="card-body">
|
|
942
|
+
<div class="d-flex align-items-start gap-2">
|
|
943
|
+
<span class="agent-type-icon agent mt-0">
|
|
944
|
+
<i class="ti" :class="connectorPresence(connector).icon"></i>
|
|
945
|
+
</span>
|
|
946
|
+
<div class="flex-grow-1 min-width-0">
|
|
947
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
948
|
+
<span class="fw-bold text-truncate" x-text="connector.displayName || connector.id"></span>
|
|
949
|
+
<span class="badge" :class="'bg-' + connectorPresence(connector).tone + '-lt'">
|
|
950
|
+
<i class="ti me-1" :class="connectorPresence(connector).icon"></i><span x-text="connectorPresence(connector).label"></span>
|
|
951
|
+
</span>
|
|
952
|
+
<span class="badge bg-secondary-lt" x-text="connector.kind"></span>
|
|
953
|
+
</div>
|
|
954
|
+
<div class="text-secondary small mt-1" x-text="connector.description || connector.packageName || connector.binary"></div>
|
|
955
|
+
<div class="d-flex gap-1 mt-2 flex-wrap">
|
|
956
|
+
<template x-for="capability in (connector.capabilities || [])" :key="capability">
|
|
957
|
+
<span class="badge bg-cyan-lt" x-text="capability"></span>
|
|
958
|
+
</template>
|
|
959
|
+
</div>
|
|
960
|
+
<div class="text-secondary small mt-2 d-flex gap-2 flex-wrap">
|
|
961
|
+
<span x-text="'v' + connector.version"></span>
|
|
962
|
+
<span x-show="connector.runtime?.detail" x-text="connector.runtime.detail"></span>
|
|
963
|
+
<span x-show="connector.runtime?.updatedAt" x-text="'Updated ' + timeAgo(connector.runtime.updatedAt)"></span>
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
<div class="d-flex gap-1 mt-3 flex-wrap">
|
|
968
|
+
<template x-for="action in ['status','doctor','start','stop','restart','enable','disable']" :key="connector.id + action">
|
|
969
|
+
<button
|
|
970
|
+
class="btn btn-sm btn-ghost-secondary"
|
|
971
|
+
x-show="connector.manifest?.commands?.[action]"
|
|
972
|
+
@click="runConnectorAction(connector, action)"
|
|
973
|
+
:title="action.charAt(0).toUpperCase() + action.slice(1)">
|
|
974
|
+
<i class="ti" :class="action === 'doctor' ? 'ti-stethoscope' : action === 'status' ? 'ti-activity' : action === 'start' ? 'ti-player-play' : action === 'stop' ? 'ti-player-stop' : action === 'restart' ? 'ti-refresh' : action === 'enable' ? 'ti-toggle-right' : 'ti-toggle-left'"></i>
|
|
975
|
+
</button>
|
|
976
|
+
</template>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
</template>
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
<template x-if="connectorCards.length === 0">
|
|
985
|
+
<div class="card">
|
|
986
|
+
<div class="card-body text-center text-secondary py-5">
|
|
987
|
+
<i class="ti ti-plug-off" style="font-size:48px; opacity:0.3"></i>
|
|
988
|
+
<p class="mt-2">No connectors registered</p>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</template>
|
|
992
|
+
</div>
|
|
993
|
+
|
|
729
994
|
<!-- ==================== INTEGRATIONS ==================== -->
|
|
730
995
|
<div x-show="view === 'integrations'" x-cloak class="fade-in">
|
|
731
996
|
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
@@ -979,7 +1244,7 @@
|
|
|
979
1244
|
<template x-if="m.subject">
|
|
980
1245
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
981
1246
|
</template>
|
|
982
|
-
<div class="msg-body" x-text="m
|
|
1247
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
983
1248
|
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
|
|
984
1249
|
<button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
|
|
985
1250
|
<i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
|
|
@@ -1234,7 +1499,7 @@
|
|
|
1234
1499
|
<template x-if="m.subject">
|
|
1235
1500
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
1236
1501
|
</template>
|
|
1237
|
-
<div class="msg-body" x-text="m
|
|
1502
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
1238
1503
|
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
|
|
1239
1504
|
<button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
|
|
1240
1505
|
<i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
|
|
@@ -2203,7 +2468,7 @@
|
|
|
2203
2468
|
<template x-if="m.subject">
|
|
2204
2469
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
2205
2470
|
</template>
|
|
2206
|
-
<div class="msg-body" x-text="m
|
|
2471
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
2207
2472
|
</div>
|
|
2208
2473
|
</template>
|
|
2209
2474
|
<template x-if="threadMessages.length === 0">
|
|
@@ -2296,6 +2561,13 @@
|
|
|
2296
2561
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@latest/dist/apexcharts.min.js"></script>
|
|
2297
2562
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
|
2298
2563
|
<script src="dashboard.js"></script>
|
|
2564
|
+
<script>
|
|
2565
|
+
if ("serviceWorker" in navigator) {
|
|
2566
|
+
window.addEventListener("load", () => {
|
|
2567
|
+
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
</script>
|
|
2299
2571
|
|
|
2300
2572
|
</body>
|
|
2301
2573
|
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Agent Relay",
|
|
3
|
+
"short_name": "Relay",
|
|
4
|
+
"description": "Local control panel for Agent Relay agents, channels, tasks, and messages.",
|
|
5
|
+
"id": "/",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#0d1117",
|
|
10
|
+
"theme_color": "#0d1117",
|
|
11
|
+
"orientation": "any",
|
|
12
|
+
"categories": ["developer", "productivity", "utilities"],
|
|
13
|
+
"icons": [
|
|
14
|
+
{
|
|
15
|
+
"src": "/icons/agent-relay.svg",
|
|
16
|
+
"sizes": "any",
|
|
17
|
+
"type": "image/svg+xml",
|
|
18
|
+
"purpose": "any maskable"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "/icons/agent-relay-192.png",
|
|
22
|
+
"sizes": "192x192",
|
|
23
|
+
"type": "image/png",
|
|
24
|
+
"purpose": "any maskable"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"src": "/icons/agent-relay-512.png",
|
|
28
|
+
"sizes": "512x512",
|
|
29
|
+
"type": "image/png",
|
|
30
|
+
"purpose": "any maskable"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const CACHE_NAME = "agent-relay-dashboard-v1";
|
|
2
|
+
const APP_SHELL = [
|
|
3
|
+
"/",
|
|
4
|
+
"/index.html",
|
|
5
|
+
"/dashboard.js",
|
|
6
|
+
"/manifest.webmanifest",
|
|
7
|
+
"/icons/agent-relay.svg",
|
|
8
|
+
"/icons/agent-relay-192.png",
|
|
9
|
+
"/icons/agent-relay-512.png",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
self.addEventListener("install", (event) => {
|
|
13
|
+
event.waitUntil(
|
|
14
|
+
caches.open(CACHE_NAME)
|
|
15
|
+
.then((cache) => cache.addAll(APP_SHELL))
|
|
16
|
+
.then(() => self.skipWaiting()),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
self.addEventListener("activate", (event) => {
|
|
21
|
+
event.waitUntil(
|
|
22
|
+
caches.keys()
|
|
23
|
+
.then((names) => Promise.all(names
|
|
24
|
+
.filter((name) => name !== CACHE_NAME)
|
|
25
|
+
.map((name) => caches.delete(name))))
|
|
26
|
+
.then(() => self.clients.claim()),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
self.addEventListener("fetch", (event) => {
|
|
31
|
+
const request = event.request;
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
|
|
34
|
+
if (request.method !== "GET" || url.origin !== self.location.origin || url.pathname.startsWith("/api/")) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (request.headers.get("accept")?.includes("text/event-stream")) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
event.respondWith(
|
|
43
|
+
fetch(request)
|
|
44
|
+
.then((response) => {
|
|
45
|
+
if (response.ok && APP_SHELL.includes(url.pathname === "/" ? "/" : url.pathname)) {
|
|
46
|
+
const copy = response.clone();
|
|
47
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
})
|
|
51
|
+
.catch(async () => {
|
|
52
|
+
const cached = await caches.match(request);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
if (request.mode === "navigate") return caches.match("/index.html");
|
|
55
|
+
throw new Error("offline");
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -485,11 +485,13 @@ async function handleMessageCommand(args: string[], defaults: { claimable?: bool
|
|
|
485
485
|
const message = await apiRequest("POST", "/api/messages", {
|
|
486
486
|
from,
|
|
487
487
|
to: target,
|
|
488
|
+
kind: claimable ? "task" : "chat",
|
|
488
489
|
subject,
|
|
489
490
|
channel,
|
|
490
491
|
body,
|
|
491
492
|
replyTo,
|
|
492
493
|
claimable,
|
|
494
|
+
payload: claimable ? { title: subject || "Claimable task" } : undefined,
|
|
493
495
|
idempotencyKey,
|
|
494
496
|
});
|
|
495
497
|
if (json) console.log(JSON.stringify(message, null, 2));
|
|
@@ -606,9 +608,16 @@ async function detectAgentId(): Promise<string | undefined> {
|
|
|
606
608
|
const explicit = process.env.AGENT_RELAY_ID;
|
|
607
609
|
if (explicit) return explicit;
|
|
608
610
|
|
|
611
|
+
const contextMatch = currentAgentContextId();
|
|
612
|
+
if (contextMatch) return contextMatch;
|
|
613
|
+
|
|
609
614
|
const cwd = process.cwd();
|
|
615
|
+
const explicitCodexState = process.env.AGENT_RELAY_CODEX_STATE_PATH
|
|
616
|
+
? readCodexState(process.env.AGENT_RELAY_CODEX_STATE_PATH)
|
|
617
|
+
: null;
|
|
618
|
+
if (explicitCodexState?.agentId) return explicitCodexState.agentId;
|
|
619
|
+
|
|
610
620
|
const stateCandidates = [
|
|
611
|
-
process.env.AGENT_RELAY_CODEX_STATE_PATH,
|
|
612
621
|
resolve(cwd, "codex/runtime/live-state.json"),
|
|
613
622
|
...collectCodexStateFiles(),
|
|
614
623
|
].filter((path): path is string => Boolean(path));
|
|
@@ -616,7 +625,7 @@ async function detectAgentId(): Promise<string | undefined> {
|
|
|
616
625
|
const codexMatch = newestCodexAgentId(stateCandidates, cwd);
|
|
617
626
|
if (codexMatch) return codexMatch;
|
|
618
627
|
|
|
619
|
-
const claudeMatch =
|
|
628
|
+
const claudeMatch = currentClaudeAgentId();
|
|
620
629
|
if (claudeMatch) return claudeMatch;
|
|
621
630
|
|
|
622
631
|
try {
|
|
@@ -624,18 +633,77 @@ async function detectAgentId(): Promise<string | undefined> {
|
|
|
624
633
|
const cwdAgents = agents
|
|
625
634
|
.filter((agent) => agent.status !== "offline" && agent.ready !== false && agent.meta?.cwd === cwd && typeof agent.id === "string")
|
|
626
635
|
.sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0));
|
|
627
|
-
|
|
636
|
+
const uniqueAgentIds = uniqueStrings(cwdAgents.map((agent) => agent.id!));
|
|
637
|
+
return uniqueAgentIds.length === 1 ? uniqueAgentIds[0] : undefined;
|
|
628
638
|
} catch {
|
|
629
639
|
return undefined;
|
|
630
640
|
}
|
|
631
641
|
}
|
|
632
642
|
|
|
643
|
+
function currentAgentContextId(): string | undefined {
|
|
644
|
+
const explicitPath = process.env.AGENT_RELAY_CONTEXT_PATH;
|
|
645
|
+
if (explicitPath) {
|
|
646
|
+
const explicit = readAgentContext(explicitPath);
|
|
647
|
+
if (explicit?.agentId) return explicit.agentId;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const candidates = collectAgentContextFiles();
|
|
651
|
+
const matches = candidates
|
|
652
|
+
.map((path) => readAgentContext(path))
|
|
653
|
+
.filter((context): context is { agentId: string; updatedAtMs: number; matchEnv: Array<{ name: string; value: string }> } => Boolean(context))
|
|
654
|
+
.filter((context) => context.matchEnv.some((match) => process.env[match.name] === match.value))
|
|
655
|
+
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
656
|
+
|
|
657
|
+
const uniqueAgentIds = uniqueStrings(matches.map((context) => context.agentId));
|
|
658
|
+
return uniqueAgentIds.length === 1 ? uniqueAgentIds[0] : undefined;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function readAgentContext(path: string): { agentId: string; updatedAtMs: number; matchEnv: Array<{ name: string; value: string }> } | null {
|
|
662
|
+
if (!existsSync(path)) return null;
|
|
663
|
+
try {
|
|
664
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
665
|
+
agentId?: unknown;
|
|
666
|
+
updatedAt?: unknown;
|
|
667
|
+
matchEnv?: unknown;
|
|
668
|
+
};
|
|
669
|
+
if (typeof parsed.agentId !== "string" || !parsed.agentId) return null;
|
|
670
|
+
const matchEnv = Array.isArray(parsed.matchEnv)
|
|
671
|
+
? parsed.matchEnv.flatMap((item) => {
|
|
672
|
+
if (!item || typeof item !== "object") return [];
|
|
673
|
+
const record = item as { name?: unknown; value?: unknown };
|
|
674
|
+
return typeof record.name === "string" && typeof record.value === "string"
|
|
675
|
+
? [{ name: record.name, value: record.value }]
|
|
676
|
+
: [];
|
|
677
|
+
})
|
|
678
|
+
: [];
|
|
679
|
+
const stat = statSync(path);
|
|
680
|
+
const updatedAt = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
681
|
+
return {
|
|
682
|
+
agentId: parsed.agentId,
|
|
683
|
+
matchEnv,
|
|
684
|
+
updatedAtMs: Number.isFinite(updatedAt) ? updatedAt : stat.mtimeMs,
|
|
685
|
+
};
|
|
686
|
+
} catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function collectAgentContextFiles(): string[] {
|
|
692
|
+
const roots = [
|
|
693
|
+
join(process.env.HOME || "", ".agent-relay", "contexts"),
|
|
694
|
+
].filter((root) => root && existsSync(root));
|
|
695
|
+
const files: string[] = [];
|
|
696
|
+
for (const root of roots) collectFiles(root, ".json", files, 2);
|
|
697
|
+
return files;
|
|
698
|
+
}
|
|
699
|
+
|
|
633
700
|
function newestCodexAgentId(paths: string[], cwd: string): string | undefined {
|
|
634
701
|
const states = paths
|
|
635
702
|
.map((path) => readCodexState(path))
|
|
636
703
|
.filter((state): state is { agentId: string; cwd?: string; updatedAtMs: number } => Boolean(state))
|
|
637
704
|
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
638
|
-
|
|
705
|
+
const cwdAgentIds = uniqueStrings(states.filter((state) => state.cwd === cwd).map((state) => state.agentId));
|
|
706
|
+
return cwdAgentIds.length === 1 ? cwdAgentIds[0] : undefined;
|
|
639
707
|
}
|
|
640
708
|
|
|
641
709
|
function readCodexState(path: string): { agentId: string; cwd?: string; updatedAtMs: number } | null {
|
|
@@ -678,29 +746,24 @@ function collectFiles(dir: string, name: string, output: string[], depth: number
|
|
|
678
746
|
try {
|
|
679
747
|
const stat = statSync(path);
|
|
680
748
|
if (stat.isDirectory()) collectFiles(path, name, output, depth - 1);
|
|
681
|
-
else if (entry === name) output.push(path);
|
|
749
|
+
else if (name.startsWith(".") ? entry.endsWith(name) : entry === name) output.push(path);
|
|
682
750
|
} catch {
|
|
683
751
|
// Ignore state files that disappear while scanning.
|
|
684
752
|
}
|
|
685
753
|
}
|
|
686
754
|
}
|
|
687
755
|
|
|
688
|
-
function
|
|
689
|
-
|
|
756
|
+
function currentClaudeAgentId(): string | undefined {
|
|
757
|
+
const sessionKey = process.env.CLAUDE_CODE_SESSION_ID || String(process.ppid || "");
|
|
758
|
+
if (!sessionKey) return undefined;
|
|
759
|
+
const safeSessionKey = sessionKey.replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
760
|
+
const statePath = join("/tmp", `agent-relay-instance-${safeSessionKey}.state`);
|
|
761
|
+
if (!existsSync(statePath)) return undefined;
|
|
690
762
|
try {
|
|
691
|
-
|
|
692
|
-
.filter((entry) => entry.startsWith("agent-relay-instance-") && entry.endsWith(".state"))
|
|
693
|
-
.map((entry) => join("/tmp", entry))
|
|
694
|
-
.map((path) => ({ path, mtimeMs: statSync(path).mtimeMs }))
|
|
695
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
696
|
-
for (const candidate of candidates) {
|
|
697
|
-
const id = readFileSync(candidate.path, "utf8").split(/\r?\n/)[0]?.trim();
|
|
698
|
-
if (id) return id;
|
|
699
|
-
}
|
|
763
|
+
return readFileSync(statePath, "utf8").split(/\r?\n/)[0]?.trim() || undefined;
|
|
700
764
|
} catch {
|
|
701
765
|
return undefined;
|
|
702
766
|
}
|
|
703
|
-
return undefined;
|
|
704
767
|
}
|
|
705
768
|
|
|
706
769
|
function formatPairs(pairs: any[]): string {
|