total-recall-brain 3.6.2 โ†’ 3.6.4

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.
@@ -735,3 +735,237 @@ body {
735
735
  ::-webkit-scrollbar-thumb:hover {
736
736
  background: #45475a;
737
737
  }
738
+
739
+ /* ==================== PANEL 5: COLLABORATION ==================== */
740
+
741
+ .collab-section {
742
+ display: flex;
743
+ flex-direction: column;
744
+ padding: 16px;
745
+ height: 100%;
746
+ }
747
+
748
+ .collab-subtext {
749
+ font-size: 12px;
750
+ color: #a6adc8;
751
+ margin-bottom: 16px;
752
+ line-height: 1.4;
753
+ }
754
+
755
+ .collab-form {
756
+ display: flex;
757
+ flex-direction: column;
758
+ gap: 10px;
759
+ }
760
+
761
+ .collab-form input {
762
+ width: 100%;
763
+ padding: 10px 14px;
764
+ background: #1e1e2e;
765
+ border: 1px solid #313244;
766
+ border-radius: 10px;
767
+ color: #cdd6f4;
768
+ font-size: 14px;
769
+ outline: none;
770
+ transition: border-color 0.2s ease;
771
+ }
772
+
773
+ .collab-form input:focus {
774
+ border-color: #cba6f7;
775
+ }
776
+
777
+ .collab-user-bar {
778
+ display: flex;
779
+ justify-content: space-between;
780
+ align-items: center;
781
+ padding: 8px 10px;
782
+ background: #181825;
783
+ border: 1px solid #313244;
784
+ border-radius: 10px;
785
+ margin-bottom: 12px;
786
+ font-size: 13px;
787
+ color: #cdd6f4;
788
+ flex-shrink: 0;
789
+ }
790
+
791
+ .collab-logout-btn {
792
+ background: transparent;
793
+ border: none;
794
+ color: #f38ba8;
795
+ font-size: 11px;
796
+ font-weight: 600;
797
+ cursor: pointer;
798
+ padding: 2px 6px;
799
+ }
800
+
801
+ .collab-logout-btn:hover {
802
+ text-decoration: underline;
803
+ }
804
+
805
+ .collab-control-group {
806
+ display: flex;
807
+ flex-direction: column;
808
+ gap: 6px;
809
+ margin-bottom: 8px;
810
+ flex-shrink: 0;
811
+ }
812
+
813
+ .collab-control-group label {
814
+ font-size: 10px;
815
+ font-weight: 700;
816
+ text-transform: uppercase;
817
+ color: #6c7086;
818
+ letter-spacing: 0.5px;
819
+ }
820
+
821
+ .collab-panel-header {
822
+ display: flex;
823
+ justify-content: space-between;
824
+ align-items: center;
825
+ margin-top: 12px;
826
+ margin-bottom: 6px;
827
+ flex-shrink: 0;
828
+ }
829
+
830
+ .collab-panel-header h4 {
831
+ font-size: 11px;
832
+ font-weight: 700;
833
+ text-transform: uppercase;
834
+ color: #6c7086;
835
+ letter-spacing: 0.5px;
836
+ }
837
+
838
+ .collab-list {
839
+ flex: 1;
840
+ min-height: 80px;
841
+ max-height: 140px;
842
+ overflow-y: auto;
843
+ border: 1px solid #313244;
844
+ background: #181825;
845
+ border-radius: 10px;
846
+ padding: 8px 10px;
847
+ display: flex;
848
+ flex-direction: column;
849
+ gap: 6px;
850
+ margin-bottom: 8px;
851
+ }
852
+
853
+ .collab-annotation-item {
854
+ background: #1e1e2e;
855
+ border: 1px solid #313244;
856
+ border-radius: 8px;
857
+ padding: 8px 10px;
858
+ font-size: 12px;
859
+ line-height: 1.4;
860
+ }
861
+
862
+ .collab-annotation-item .author {
863
+ font-size: 10px;
864
+ color: #89b4fa;
865
+ font-weight: 600;
866
+ margin-bottom: 2px;
867
+ }
868
+
869
+ .collab-annotation-item .text {
870
+ color: #cdd6f4;
871
+ word-break: break-word;
872
+ }
873
+
874
+ .collab-annotation-item .excerpt {
875
+ font-size: 11px;
876
+ color: #a6adc8;
877
+ font-style: italic;
878
+ border-left: 2px solid #89b4fa;
879
+ padding-left: 6px;
880
+ margin-bottom: 4px;
881
+ }
882
+
883
+ .collab-annotation-item .time {
884
+ font-size: 9px;
885
+ color: #585b70;
886
+ text-align: right;
887
+ margin-top: 4px;
888
+ }
889
+
890
+ .collab-annotation-form {
891
+ display: flex;
892
+ flex-direction: column;
893
+ flex-shrink: 0;
894
+ }
895
+
896
+ .collab-annotation-form textarea {
897
+ width: 100%;
898
+ height: 48px;
899
+ min-height: 48px;
900
+ padding: 6px 8px;
901
+ background: #1e1e2e;
902
+ border: 1px solid #313244;
903
+ border-radius: 8px;
904
+ color: #cdd6f4;
905
+ font-size: 12px;
906
+ resize: none;
907
+ outline: none;
908
+ font-family: inherit;
909
+ transition: border-color 0.2s ease;
910
+ }
911
+
912
+ .collab-annotation-form textarea:focus {
913
+ border-color: #cba6f7;
914
+ }
915
+
916
+ .collab-chat-box {
917
+ flex: 2;
918
+ min-height: 100px;
919
+ max-height: 200px;
920
+ border: 1px solid #313244;
921
+ background: #181825;
922
+ border-radius: 10px;
923
+ padding: 10px;
924
+ display: flex;
925
+ flex-direction: column;
926
+ gap: 8px;
927
+ overflow-y: auto;
928
+ margin-bottom: 6px;
929
+ }
930
+
931
+ .collab-input-bar {
932
+ padding: 0 !important;
933
+ border-top: none !important;
934
+ margin-bottom: 0 !important;
935
+ }
936
+
937
+ .collab-input-bar textarea {
938
+ height: 34px !important;
939
+ max-height: 34px !important;
940
+ font-size: 12px !important;
941
+ border-radius: 8px !important;
942
+ padding: 8px !important;
943
+ }
944
+
945
+ .collab-input-bar button {
946
+ height: 34px !important;
947
+ width: 50px !important;
948
+ border-radius: 8px !important;
949
+ font-size: 11px !important;
950
+ }
951
+
952
+ /* Pulse animation for presence badge */
953
+ .pulse {
954
+ animation: pulse-glow 2s infinite;
955
+ }
956
+
957
+ @keyframes pulse-glow {
958
+ 0% {
959
+ transform: scale(0.95);
960
+ box-shadow: 0 0 0 0 rgba(166, 227, 161, 0.7);
961
+ }
962
+ 70% {
963
+ transform: scale(1);
964
+ box-shadow: 0 0 0 5px rgba(166, 227, 161, 0);
965
+ }
966
+ 100% {
967
+ transform: scale(0.95);
968
+ box-shadow: 0 0 0 0 rgba(166, 227, 161, 0);
969
+ }
970
+ }
971
+
@@ -22,6 +22,7 @@
22
22
  <button class="tab-btn" data-tab="chat">๐Ÿ’ฌ Chat</button>
23
23
  <button class="tab-btn" data-tab="research">๐Ÿ”ฌ Research</button>
24
24
  <button class="tab-btn" data-tab="settings">โš™๏ธ Settings</button>
25
+ <button class="tab-btn" data-tab="collab">๐Ÿ‘ฅ Collab</button>
25
26
  </nav>
26
27
 
27
28
  <!-- Tab Panels Container -->
@@ -128,6 +129,77 @@
128
129
  </div>
129
130
  </section>
130
131
 
132
+ <!-- Panel 5: Collaboration -->
133
+ <section id="tab-collab" class="tab-panel">
134
+ <!-- Auth View -->
135
+ <div id="collab-auth-view" class="collab-section">
136
+ <h3>๐Ÿ‘ฅ Join Collab Network</h3>
137
+ <p class="collab-subtext">Register or log in to sync annotations and chat on websites live.</p>
138
+ <div class="collab-form">
139
+ <input type="text" id="collab-username" placeholder="Username" />
140
+ <input type="password" id="collab-password" placeholder="Password" />
141
+ <div style="display: flex; gap: 8px; margin-top: 8px;">
142
+ <button id="btn-collab-login" class="action-btn accent" style="flex: 1;">Login</button>
143
+ <button id="btn-collab-register" class="action-btn" style="flex: 1;">Register</button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <!-- Main Collaboration View -->
149
+ <div id="collab-main-view" class="collab-section hidden" style="display: flex; flex-direction: column; height: 100%;">
150
+ <div class="collab-user-bar">
151
+ <span>๐Ÿ‘ค <strong id="collab-user-display"></strong></span>
152
+ <button id="btn-collab-logout" class="collab-logout-btn">Logout</button>
153
+ </div>
154
+
155
+ <div class="collab-control-group">
156
+ <label for="collab-group-select">Workspace Group</label>
157
+ <select id="collab-group-select" class="settings-select"></select>
158
+ </div>
159
+
160
+ <div class="collab-action-grid" style="display: flex; gap: 8px; margin-bottom: 8px; margin-top: 4px;">
161
+ <button id="btn-collab-show-join" class="action-btn" style="padding: 6px; font-size: 11px;">Join Group</button>
162
+ <button id="btn-collab-show-create" class="action-btn" style="padding: 6px; font-size: 11px;">Create Group</button>
163
+ </div>
164
+
165
+ <div id="collab-join-group-row" class="collab-action-row hidden" style="display: flex; gap: 6px; margin-bottom: 8px;">
166
+ <input type="text" id="collab-join-code" placeholder="Invite Code" style="flex: 1; padding: 6px; font-size: 12px; background: #1e1e2e; border: 1px solid #313244; color: #cdd6f4; border-radius: 6px;" />
167
+ <button id="btn-collab-submit-join" class="action-btn accent" style="flex-shrink: 0; padding: 6px 12px; font-size: 11px; height: 28px;">Join</button>
168
+ </div>
169
+
170
+ <div id="collab-create-group-row" class="collab-action-row hidden" style="display: flex; gap: 6px; margin-bottom: 8px;">
171
+ <input type="text" id="collab-create-name" placeholder="Group Name" style="flex: 1; padding: 6px; font-size: 12px; background: #1e1e2e; border: 1px solid #313244; color: #cdd6f4; border-radius: 6px;" />
172
+ <button id="btn-collab-submit-create" class="action-btn accent" style="flex-shrink: 0; padding: 6px 12px; font-size: 11px; height: 28px;">Create</button>
173
+ </div>
174
+
175
+ <!-- Pinned Annotations -->
176
+ <div class="collab-panel-header">
177
+ <h4>๐Ÿ“Œ Notes on Page</h4>
178
+ </div>
179
+ <div id="collab-annotations-list" class="collab-list"></div>
180
+
181
+ <!-- Add Annotation Form -->
182
+ <div class="collab-annotation-form">
183
+ <textarea id="collab-note-input" placeholder="Pin a note to this page..."></textarea>
184
+ <button id="btn-collab-save-note" class="action-btn accent" style="width: 100%; margin-top: 4px; padding: 6px; font-size: 12px;">Pin Note</button>
185
+ </div>
186
+
187
+ <!-- Real-time Site Chat -->
188
+ <div class="collab-panel-header" style="margin-top: 8px;">
189
+ <h4>๐Ÿ’ฌ Site Chat Room</h4>
190
+ <div style="display: flex; align-items: center; gap: 4px;">
191
+ <span class="pulse" style="width: 6px; height: 6px; background: #a6e3a1; box-shadow: 0 0 4px #a6e3a1; border-radius: 50%;"></span>
192
+ <span id="collab-presence-count" style="font-size: 10px; color: #a6e3a1;">1 online</span>
193
+ </div>
194
+ </div>
195
+ <div id="collab-chat-messages" class="chat-messages collab-chat-box"></div>
196
+ <div class="chat-input-bar collab-input-bar">
197
+ <textarea id="collab-chat-input" placeholder="Send a message to team..."></textarea>
198
+ <button id="btn-collab-send-chat" class="chat-send-btn">Send</button>
199
+ </div>
200
+ </div>
201
+ </section>
202
+
131
203
  </div>
132
204
  </div>
133
205
  <script src="../lib/preconfigured.js"></script>
@@ -676,6 +676,413 @@ Provide helpful analysis grounded on both the user's brain memory and this page
676
676
  }
677
677
  }
678
678
 
679
+ // ==================== COLLABORATION MODULE ====================
680
+ const collabAuthView = document.getElementById('collab-auth-view');
681
+ const collabMainView = document.getElementById('collab-main-view');
682
+ const collabUsernameInput = document.getElementById('collab-username');
683
+ const collabPasswordInput = document.getElementById('collab-password');
684
+ const btnCollabLogin = document.getElementById('btn-collab-login');
685
+ const btnCollabRegister = document.getElementById('btn-collab-register');
686
+
687
+ const collabUserDisplay = document.getElementById('collab-user-display');
688
+ const btnCollabLogout = document.getElementById('btn-collab-logout');
689
+ const collabGroupSelect = document.getElementById('collab-group-select');
690
+
691
+ const btnCollabShowJoin = document.getElementById('btn-collab-show-join');
692
+ const btnCollabShowCreate = document.getElementById('btn-collab-show-create');
693
+ const collabJoinGroupRow = document.getElementById('collab-join-group-row');
694
+ const collabJoinCode = document.getElementById('collab-join-code');
695
+ const btnCollabSubmitJoin = document.getElementById('btn-collab-submit-join');
696
+
697
+ const collabCreateGroupRow = document.getElementById('collab-create-group-row');
698
+ const collabCreateName = document.getElementById('collab-create-name');
699
+ const btnCollabSubmitCreate = document.getElementById('btn-collab-submit-create');
700
+
701
+ const collabAnnotationsList = document.getElementById('collab-annotations-list');
702
+ const collabNoteInput = document.getElementById('collab-note-input');
703
+ const btnCollabSaveNote = document.getElementById('btn-collab-save-note');
704
+
705
+ const collabPresenceCount = document.getElementById('collab-presence-count');
706
+ const collabChatMessages = document.getElementById('collab-chat-messages');
707
+ const collabChatInput = document.getElementById('collab-chat-input');
708
+ const btnCollabSendChat = document.getElementById('btn-collab-send-chat');
709
+
710
+ let collabToken = '';
711
+ let collabUsername = '';
712
+ let collabActiveUrl = '';
713
+ let collabSocket = null;
714
+ let collabGroups = [];
715
+ let collabSelectedGroup = null;
716
+
717
+ async function initCollab() {
718
+ const data = await chrome.storage.local.get(['collabToken', 'collabUsername']);
719
+ if (data.collabToken && data.collabUsername) {
720
+ collabToken = data.collabToken;
721
+ collabUsername = data.collabUsername;
722
+ showCollabMain();
723
+ } else {
724
+ showCollabAuth();
725
+ }
726
+ }
727
+
728
+ function showCollabAuth() {
729
+ collabAuthView.classList.remove('hidden');
730
+ collabMainView.classList.add('hidden');
731
+ closeCollabSocket();
732
+ }
733
+
734
+ async function showCollabMain() {
735
+ collabAuthView.classList.add('hidden');
736
+ collabMainView.classList.remove('hidden');
737
+ collabUserDisplay.textContent = collabUsername;
738
+ await fetchCollabGroups();
739
+ updateCollabActiveUrl();
740
+ }
741
+
742
+ async function fetchCollabGroups() {
743
+ try {
744
+ const config = await self.BrainClient.getConfig();
745
+ const res = await fetch(`${config.brainUrl}/api/collab/groups`, {
746
+ headers: { 'Authorization': `Bearer ${collabToken}` }
747
+ });
748
+ if (res.ok) {
749
+ collabGroups = await res.json();
750
+ renderCollabGroupsDropdown();
751
+ } else if (res.status === 401) {
752
+ logoutCollab();
753
+ }
754
+ } catch (err) {
755
+ console.error('Failed to fetch collab groups:', err);
756
+ }
757
+ }
758
+
759
+ function renderCollabGroupsDropdown() {
760
+ collabGroupSelect.innerHTML = collabGroups.map(g =>
761
+ `<option value="${g.code}">${escapeHtml(g.name)} (${g.code})</option>`
762
+ ).join('');
763
+ if (collabGroups.length > 0) {
764
+ const activeCode = collabGroupSelect.value;
765
+ collabSelectedGroup = collabGroups.find(g => g.code === activeCode) || collabGroups[0];
766
+ } else {
767
+ collabSelectedGroup = null;
768
+ }
769
+ }
770
+
771
+ async function updateCollabActiveUrl() {
772
+ try {
773
+ const tab = await getCurrentTab();
774
+ if (tab && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('about:') && !tab.url.startsWith('chrome-extension://')) {
775
+ const cleanUrl = tab.url.split('#')[0];
776
+ if (cleanUrl !== collabActiveUrl) {
777
+ collabActiveUrl = cleanUrl;
778
+ if (collabToken) {
779
+ await fetchCollabAnnotations();
780
+ connectCollabSocket();
781
+ }
782
+ }
783
+ } else {
784
+ collabActiveUrl = '';
785
+ collabAnnotationsList.innerHTML = '<div class="memories-empty">No active web tab</div>';
786
+ closeCollabSocket();
787
+ }
788
+ } catch (err) {
789
+ console.error(err);
790
+ }
791
+ }
792
+
793
+ async function fetchCollabAnnotations() {
794
+ if (!collabActiveUrl || !collabToken) return;
795
+ try {
796
+ const config = await self.BrainClient.getConfig();
797
+ const res = await fetch(`${config.brainUrl}/api/collab/annotations?url=${encodeURIComponent(collabActiveUrl)}`, {
798
+ headers: { 'Authorization': `Bearer ${collabToken}` }
799
+ });
800
+ if (res.ok) {
801
+ const annotations = await res.json();
802
+ renderCollabAnnotations(annotations);
803
+ }
804
+ } catch (err) {
805
+ console.error('Failed to fetch annotations:', err);
806
+ }
807
+ }
808
+
809
+ function renderCollabAnnotations(list) {
810
+ if (!list.length) {
811
+ collabAnnotationsList.innerHTML = '<div class="memories-empty">No notes on this page</div>';
812
+ return;
813
+ }
814
+ collabAnnotationsList.innerHTML = list.map(a => `
815
+ <div class="collab-annotation-item">
816
+ <div class="author">๐Ÿ‘ค ${escapeHtml(a.author)}</div>
817
+ ${a.excerpt ? `<div class="excerpt">${escapeHtml(a.excerpt)}</div>` : ''}
818
+ <div class="text">${escapeHtml(a.text)}</div>
819
+ <div class="time">${formatTime(a.created_at)}</div>
820
+ </div>
821
+ `).join('');
822
+ }
823
+
824
+ function connectCollabSocket() {
825
+ if (!collabToken || !collabActiveUrl) return;
826
+ closeCollabSocket();
827
+
828
+ self.BrainClient.getConfig().then(config => {
829
+ const serverUrl = new URL(config.brainUrl);
830
+ const wsProto = serverUrl.protocol === 'https:' ? 'wss:' : 'ws:';
831
+ const wsUrl = `${wsProto}//${serverUrl.host}/collab-ws?token=${collabToken}`;
832
+
833
+ const ws = new WebSocket(wsUrl);
834
+ collabSocket = ws;
835
+
836
+ ws.onopen = () => {
837
+ ws.send(JSON.stringify({ type: 'SUBSCRIBE', url: collabActiveUrl }));
838
+ collabPresenceCount.textContent = 'Connected';
839
+ };
840
+
841
+ ws.onmessage = (event) => {
842
+ try {
843
+ const data = JSON.parse(event.data);
844
+ if (data.type === 'CHAT_MESSAGE') {
845
+ appendCollabChatBubble(data.username, data.text, data.created_at);
846
+ } else if (data.type === 'USER_JOINED') {
847
+ appendCollabChatBubble('System', `${data.username} joined this page`, new Date().toISOString(), true);
848
+ } else if (data.type === 'USER_LEFT') {
849
+ appendCollabChatBubble('System', `${data.username} left`, new Date().toISOString(), true);
850
+ } else if (data.type === 'ANNOTATION_ADDED') {
851
+ fetchCollabAnnotations();
852
+ }
853
+ } catch (err) {
854
+ console.error(err);
855
+ }
856
+ };
857
+
858
+ ws.onclose = () => {
859
+ collabPresenceCount.textContent = 'Disconnected';
860
+ };
861
+
862
+ ws.onerror = () => {
863
+ collabPresenceCount.textContent = 'Error';
864
+ };
865
+ });
866
+ }
867
+
868
+ function closeCollabSocket() {
869
+ if (collabSocket) {
870
+ try {
871
+ collabSocket.close();
872
+ } catch {}
873
+ collabSocket = null;
874
+ }
875
+ }
876
+
877
+ function appendCollabChatBubble(sender, text, timestamp, isSystem = false) {
878
+ const bubble = document.createElement('div');
879
+ if (isSystem) {
880
+ bubble.className = 'chat-bubble system';
881
+ bubble.textContent = text;
882
+ } else {
883
+ const isMe = sender.toLowerCase() === collabUsername.toLowerCase();
884
+ bubble.className = `chat-bubble ${isMe ? 'user' : 'assistant'}`;
885
+ bubble.innerHTML = `<span style="font-size: 10px; opacity: 0.8; font-weight: bold; display: block; margin-bottom: 2px;">${escapeHtml(sender)}</span>${escapeHtml(text)}`;
886
+ }
887
+ collabChatMessages.appendChild(bubble);
888
+ collabChatMessages.scrollTop = collabChatMessages.scrollHeight;
889
+ }
890
+
891
+ async function registerCollab(username, password) {
892
+ try {
893
+ const config = await self.BrainClient.getConfig();
894
+ const res = await fetch(`${config.brainUrl}/api/collab/auth/register`, {
895
+ method: 'POST',
896
+ headers: { 'Content-Type': 'application/json' },
897
+ body: JSON.stringify({ username, password })
898
+ });
899
+ const data = await res.json();
900
+ if (res.ok) {
901
+ collabToken = data.token;
902
+ collabUsername = data.username;
903
+ await chrome.storage.local.set({ collabToken: data.token, collabUsername: data.username });
904
+ showToast('Registered successfully!');
905
+ showCollabMain();
906
+ } else {
907
+ showToast('Registration failed: ' + (data.error || 'Unknown error'), true);
908
+ }
909
+ } catch (err) {
910
+ showToast('Registration error: ' + err.message, true);
911
+ }
912
+ }
913
+
914
+ async function loginCollab(username, password) {
915
+ try {
916
+ const config = await self.BrainClient.getConfig();
917
+ const res = await fetch(`${config.brainUrl}/api/collab/auth/login`, {
918
+ method: 'POST',
919
+ headers: { 'Content-Type': 'application/json' },
920
+ body: JSON.stringify({ username, password })
921
+ });
922
+ const data = await res.json();
923
+ if (res.ok) {
924
+ collabToken = data.token;
925
+ collabUsername = data.username;
926
+ await chrome.storage.local.set({ collabToken: data.token, collabUsername: data.username });
927
+ showToast('Logged in successfully!');
928
+ showCollabMain();
929
+ } else {
930
+ showToast('Login failed: ' + (data.error || 'Unknown error'), true);
931
+ }
932
+ } catch (err) {
933
+ showToast('Login error: ' + err.message, true);
934
+ }
935
+ }
936
+
937
+ async function logoutCollab() {
938
+ collabToken = '';
939
+ collabUsername = '';
940
+ await chrome.storage.local.remove(['collabToken', 'collabUsername']);
941
+ showCollabAuth();
942
+ showToast('Logged out');
943
+ }
944
+
945
+ // --- Buttons / Forms bindings ---
946
+ btnCollabLogin.addEventListener('click', () => {
947
+ const u = collabUsernameInput.value.trim();
948
+ const p = collabPasswordInput.value.trim();
949
+ if (u && p) loginCollab(u, p);
950
+ });
951
+
952
+ btnCollabRegister.addEventListener('click', () => {
953
+ const u = collabUsernameInput.value.trim();
954
+ const p = collabPasswordInput.value.trim();
955
+ if (u && p) registerCollab(u, p);
956
+ });
957
+
958
+ btnCollabLogout.addEventListener('click', logoutCollab);
959
+
960
+ btnCollabShowJoin.addEventListener('click', () => {
961
+ collabJoinGroupRow.classList.toggle('hidden');
962
+ collabCreateGroupRow.classList.add('hidden');
963
+ });
964
+
965
+ btnCollabShowCreate.addEventListener('click', () => {
966
+ collabCreateGroupRow.classList.toggle('hidden');
967
+ collabJoinGroupRow.classList.add('hidden');
968
+ });
969
+
970
+ btnCollabSubmitJoin.addEventListener('click', async () => {
971
+ const code = collabJoinCode.value.trim();
972
+ if (!code) return;
973
+ try {
974
+ const config = await self.BrainClient.getConfig();
975
+ const res = await fetch(`${config.brainUrl}/api/collab/groups/join`, {
976
+ method: 'POST',
977
+ headers: {
978
+ 'Content-Type': 'application/json',
979
+ 'Authorization': `Bearer ${collabToken}`
980
+ },
981
+ body: JSON.stringify({ code })
982
+ });
983
+ const data = await res.json();
984
+ if (res.ok) {
985
+ showToast('Joined group!');
986
+ collabJoinCode.value = '';
987
+ collabJoinGroupRow.classList.add('hidden');
988
+ await fetchCollabGroups();
989
+ } else {
990
+ showToast('Join error: ' + data.error, true);
991
+ }
992
+ } catch (err) {
993
+ showToast('Failed to join: ' + err.message, true);
994
+ }
995
+ });
996
+
997
+ btnCollabSubmitCreate.addEventListener('click', async () => {
998
+ const name = collabCreateName.value.trim();
999
+ if (!name) return;
1000
+ try {
1001
+ const config = await self.BrainClient.getConfig();
1002
+ const res = await fetch(`${config.brainUrl}/api/collab/groups`, {
1003
+ method: 'POST',
1004
+ headers: {
1005
+ 'Content-Type': 'application/json',
1006
+ 'Authorization': `Bearer ${collabToken}`
1007
+ },
1008
+ body: JSON.stringify({ name })
1009
+ });
1010
+ const data = await res.json();
1011
+ if (res.ok) {
1012
+ showToast('Group created!');
1013
+ collabCreateName.value = '';
1014
+ collabCreateGroupRow.classList.add('hidden');
1015
+ await fetchCollabGroups();
1016
+ } else {
1017
+ showToast('Create error: ' + data.error, true);
1018
+ }
1019
+ } catch (err) {
1020
+ showToast('Failed to create group: ' + err.message, true);
1021
+ }
1022
+ });
1023
+
1024
+ collabGroupSelect.addEventListener('change', () => {
1025
+ const activeCode = collabGroupSelect.value;
1026
+ collabSelectedGroup = collabGroups.find(g => g.code === activeCode) || null;
1027
+ fetchCollabAnnotations();
1028
+ });
1029
+
1030
+ btnCollabSaveNote.addEventListener('click', async () => {
1031
+ const text = collabNoteInput.value.trim();
1032
+ if (!text || !collabActiveUrl || !collabSelectedGroup) return;
1033
+ try {
1034
+ const config = await self.BrainClient.getConfig();
1035
+ const res = await fetch(`${config.brainUrl}/api/collab/annotations`, {
1036
+ method: 'POST',
1037
+ headers: {
1038
+ 'Content-Type': 'application/json',
1039
+ 'Authorization': `Bearer ${collabToken}`
1040
+ },
1041
+ body: JSON.stringify({
1042
+ url: collabActiveUrl,
1043
+ groupCode: collabSelectedGroup.code,
1044
+ text
1045
+ })
1046
+ });
1047
+ if (res.ok) {
1048
+ showToast('Note pinned!');
1049
+ collabNoteInput.value = '';
1050
+ fetchCollabAnnotations();
1051
+ } else {
1052
+ const errData = await res.json();
1053
+ showToast('Pin note error: ' + errData.error, true);
1054
+ }
1055
+ } catch (err) {
1056
+ showToast('Failed to pin note: ' + err.message, true);
1057
+ }
1058
+ });
1059
+
1060
+ btnCollabSendChat.addEventListener('click', () => {
1061
+ const text = collabChatInput.value.trim();
1062
+ if (text && collabSocket && collabSocket.readyState === WebSocket.OPEN) {
1063
+ collabSocket.send(JSON.stringify({ type: 'CHAT_MESSAGE', text }));
1064
+ collabChatInput.value = '';
1065
+ }
1066
+ });
1067
+
1068
+ collabChatInput.addEventListener('keydown', (e) => {
1069
+ if (e.key === 'Enter' && !e.shiftKey) {
1070
+ e.preventDefault();
1071
+ btnCollabSendChat.click();
1072
+ }
1073
+ });
1074
+
1075
+ // Listen for tab active changes
1076
+ chrome.tabs.onActivated.addListener(() => {
1077
+ updateCollabActiveUrl();
1078
+ });
1079
+
1080
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1081
+ if (changeInfo.url) {
1082
+ updateCollabActiveUrl();
1083
+ }
1084
+ });
1085
+
679
1086
  // ---- Event listeners ----
680
1087
  searchInput.addEventListener('input', (e) => debounceSearch(e.target.value));
681
1088
 
@@ -684,6 +1091,7 @@ Provide helpful analysis grounded on both the user's brain memory and this page
684
1091
  loadSettingsState().then(() => {
685
1092
  loadRecentCaptures();
686
1093
  recordRememberInvariants();
1094
+ initCollab();
687
1095
  });
688
1096
 
689
1097
  // Set intervals
@@ -696,3 +1104,5 @@ Provide helpful analysis grounded on both the user's brain memory and this page
696
1104
  }
697
1105
  }, 30000);
698
1106
  })();
1107
+
1108
+
@@ -530,6 +530,13 @@ export async function triggerDream(): Promise<{ success: boolean; status: string
530
530
  return res.json()
531
531
  }
532
532
 
533
+ export async function runAgentDiagnostics(): Promise<{ success: boolean; output: string }> {
534
+ const res = await apiFetch(`${API_BASE}/api/diagnostics/agents`, { method: 'POST' })
535
+ if (!res.ok) throw new Error(`Diagnostics API error: ${res.status}`)
536
+ return res.json()
537
+ }
538
+
539
+
533
540
  export async function fetchGraph(): Promise<{ nodes: MemoryNode[]; routes: unknown[] }> {
534
541
  const res = await apiFetch(`${API_BASE}/api/graph`)
535
542
  if (!res.ok) throw new Error(`Graph API error: ${res.status}`)
@@ -166,7 +166,7 @@ function CollabDashboard({ token, username, onLogout, onSelectSandbox }: { token
166
166
  <p style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>Share annotations and real-time page chat rooms</p>
167
167
  </div>
168
168
  <div style={{ display: 'flex', gap: '10px' }}>
169
- <button className="btn btn-purple" onClick={onSelectSandbox}>๐ŸŒ Open Page Sandbox</button>
169
+ <button className="btn btn-purple" onClick={onSelectSandbox}>๐ŸŒ Open Browse Workspace</button>
170
170
  <button className="btn btn-ghost" onClick={onLogout} style={{ color: '#f87171' }}>Logout</button>
171
171
  </div>
172
172
  </div>
@@ -219,7 +219,7 @@ function CollabDashboard({ token, username, onLogout, onSelectSandbox }: { token
219
219
  function CollabSandbox({ token, username, onBack }: { token: string, username: string, onBack: () => void }) {
220
220
  const [groups, setGroups] = useState<CollabGroup[]>([]);
221
221
  const [selectedGroup, setSelectedGroup] = useState<CollabGroup | null>(null);
222
- const [simulatedUrl, setSimulatedUrl] = useState('https://news.ycombinator.com');
222
+ const [activeBrowseUrl, setActiveBrowseUrl] = useState('https://news.ycombinator.com');
223
223
  const [activeUrl, setActiveUrl] = useState('https://news.ycombinator.com');
224
224
  const [annotations, setAnnotations] = useState<Annotation[]>([]);
225
225
  const [noteText, setNoteText] = useState('');
@@ -313,9 +313,9 @@ function CollabSandbox({ token, username, onBack }: { token: string, username: s
313
313
 
314
314
  const handleUrlSubmit = (e: React.FormEvent) => {
315
315
  e.preventDefault();
316
- if (!simulatedUrl.trim()) return;
316
+ if (!activeBrowseUrl.trim()) return;
317
317
  setChatMessages([]); setSystemLogs([]); setActiveUsers([]);
318
- setActiveUrl(simulatedUrl);
318
+ setActiveUrl(activeBrowseUrl);
319
319
  };
320
320
 
321
321
  const handleAddAnnotation = async (e: React.FormEvent) => {
@@ -348,14 +348,14 @@ function CollabSandbox({ token, username, onBack }: { token: string, username: s
348
348
  <div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'space-between', background: 'rgba(255,255,255,0.02)', padding: '12px 20px', borderRadius: '8px', border: '1px solid var(--border)', flexWrap: 'wrap' }}>
349
349
  <button className="btn btn-ghost" onClick={onBack}>โ† Back to Workspaces</button>
350
350
  <form onSubmit={handleUrlSubmit} style={{ display: 'flex', gap: '10px', flex: '1', minWidth: '250px' }}>
351
- <input type="text" value={simulatedUrl} onChange={e => setSimulatedUrl(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 12px', borderRadius: '6px', flex: '1', fontFamily: 'monospace' }} />
352
- <button type="submit" className="btn btn-purple">Simulate Visit</button>
351
+ <input type="text" value={activeBrowseUrl} onChange={e => setActiveBrowseUrl(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 12px', borderRadius: '6px', flex: '1', fontFamily: 'monospace' }} />
352
+ <button type="submit" className="btn btn-purple">Connect URL</button>
353
353
  </form>
354
354
  <div style={{ fontSize: '13px', color: wsConnected ? '#34d399' : '#f87171' }}>โ— {wsConnected ? 'Connected' : 'Disconnected'}</div>
355
355
  </div>
356
356
 
357
357
  <div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
358
- Simulated browser context: <strong style={{ color: 'var(--purple-primary)', fontFamily: 'monospace' }}>{activeUrl}</strong>
358
+ Active browse context URL: <strong style={{ color: 'var(--purple-primary)', fontFamily: 'monospace' }}>{activeUrl}</strong>
359
359
  </div>
360
360
 
361
361
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 340px', gap: '20px', height: '500px' }}>
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { fetchHealth, fetchLogs } from '../api';
2
+ import { fetchHealth, fetchLogs, runAgentDiagnostics } from '../api';
3
3
  import type { HealthData } from '../types';
4
4
 
5
5
  export default function DeploymentsPage() {
@@ -9,9 +9,9 @@ export default function DeploymentsPage() {
9
9
  const [error, setError] = useState<string | null>(null);
10
10
  const [success, setSuccess] = useState<string | null>(null);
11
11
 
12
- // Model Pull Form state
13
- const [modelToPull, setModelToPull] = useState('gemma4:26b');
14
- const [pulling, setPulling] = useState(false);
12
+ // CLI Agent Diagnostics state
13
+ const [runningDiagnostics, setRunningDiagnostics] = useState(false);
14
+ const [diagnosticLogs, setDiagnosticLogs] = useState<string | null>(null);
15
15
 
16
16
  const consoleEndRef = useRef<HTMLDivElement>(null);
17
17
 
@@ -44,29 +44,36 @@ export default function DeploymentsPage() {
44
44
  }
45
45
  }, [logs]);
46
46
 
47
- const handlePullModel = async (e: React.FormEvent) => {
47
+ const handleRunDiagnostics = async (e: React.FormEvent) => {
48
48
  e.preventDefault();
49
- setPulling(true);
49
+ setRunningDiagnostics(true);
50
50
  setError(null);
51
51
  setSuccess(null);
52
+ setDiagnosticLogs(null);
52
53
  try {
53
- // Simulate/trigger model pull trigger enqueued in task manager
54
- setSuccess(`Model pull job successfully enqueued for "${modelToPull}" in the background task runner.`);
55
- setTimeout(() => setSuccess(null), 5000);
54
+ const res = await runAgentDiagnostics();
55
+ setDiagnosticLogs(res.output);
56
+ if (res.success) {
57
+ setSuccess('CLI Agent diagnostics completed successfully.');
58
+ } else {
59
+ setError('CLI Agent diagnostics finished with warnings or missing binaries.');
60
+ }
61
+ void fetchSystemData();
56
62
  } catch (err: unknown) {
57
- setError((err as Error).message || 'Model pull request failed.');
63
+ setError((err as Error).message || 'Diagnostics execution failed.');
58
64
  } finally {
59
- setPulling(false);
65
+ setRunningDiagnostics(false);
60
66
  }
61
67
  };
62
68
 
63
- const MODELS_CATALOG = [
64
- { id: 'gemma4:26b', name: 'Gemma 4 (26B Parameter)', size: '15.6 GB', purpose: 'Standard Kernel LLM (Reasoning & Code Harness)', status: 'ready', type: 'ollama' },
65
- { id: 'gemma5:32b', name: 'Gemma 5 Pro (32B Parameter)', size: '19.2 GB', purpose: 'Advanced Reasoning & Deliberation Engine', status: 'available', type: 'ollama' },
66
- { id: 'text-embedding-004', name: 'Gemini Text Embeddings v4', size: 'Cloud-native', purpose: 'High-Dimensional Dense Vectorization', status: 'ready', type: 'gemini' },
67
- { id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', size: 'Cloud-native', purpose: 'Frontier Advanced Programming', status: 'ready', type: 'claude' },
69
+ const AGENTS_LIST = [
70
+ { id: 'antigravity', name: 'Antigravity (Gemini SDK)', desc: 'Primary core developer agent' },
71
+ { id: 'gemini', name: 'Gemini CLI', desc: 'Direct Gemini assistant binary' },
72
+ { id: 'claude', name: 'Claude Code', desc: 'Anthropic developer CLI wrapper' },
73
+ { id: 'codex', name: 'Codex CLI', desc: 'OpenAI agent binary integration' }
68
74
  ];
69
75
 
76
+
70
77
  return (
71
78
  <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflowY: 'auto' }}>
72
79
  <div className="page-header" style={{ marginBottom: 24 }}>
@@ -146,7 +153,7 @@ export default function DeploymentsPage() {
146
153
 
147
154
  </div>
148
155
 
149
- {/* Model Catalog & Pull form */}
156
+ {/* CLI Agents Catalog & Diagnostics */}
150
157
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24 }}>
151
158
 
152
159
  {/* Catalog */}
@@ -154,31 +161,31 @@ export default function DeploymentsPage() {
154
161
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
155
162
  <span style={{ fontSize: 20 }}>๐Ÿง </span>
156
163
  <div>
157
- <h3 style={{ fontSize: 14, fontWeight: 600 }}>Kernel LLM Catalog</h3>
158
- <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Provisioned local intelligence engines</p>
164
+ <h3 style={{ fontSize: 14, fontWeight: 600 }}>CLI Reasoning Agents</h3>
165
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Headless CLI reasoning agents configured on system</p>
159
166
  </div>
160
167
  </div>
161
168
 
162
169
  <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
163
- {MODELS_CATALOG.map((m) => {
164
- const activeModel = health?.ollama_models?.includes(m.id) || m.type !== 'ollama';
170
+ {AGENTS_LIST.map((a) => {
171
+ const activeAgent = health?.cli_agents?.includes(a.id);
165
172
  return (
166
- <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 8, border: '1px solid var(--border)' }}>
173
+ <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 8, border: '1px solid var(--border)' }}>
167
174
  <div style={{ flex: 1 }}>
168
175
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
169
- <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{m.name}</span>
176
+ <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{a.name}</span>
170
177
  <span className="badge" style={{
171
178
  fontSize: 9,
172
179
  padding: '2px 6px',
173
- background: activeModel ? 'rgba(0, 206, 201, 0.1)' : 'rgba(255, 255, 255, 0.04)',
174
- color: activeModel ? '#00cec9' : 'var(--text-tertiary)',
175
- border: activeModel ? '1px solid rgba(0, 206, 201, 0.2)' : '1px solid var(--border)'
180
+ background: activeAgent ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
181
+ color: activeAgent ? '#10b981' : '#ef4444',
182
+ border: activeAgent ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(239, 68, 68, 0.2)'
176
183
  }}>
177
- {activeModel ? 'active' : 'available'}
184
+ {activeAgent ? 'available' : 'missing'}
178
185
  </span>
179
186
  </div>
180
- <div style={{ fontSize: 10, color: 'var(--text-secondary)', marginTop: 2 }}>{m.purpose}</div>
181
- <div style={{ fontSize: 9, color: 'var(--text-tertiary)', marginTop: 4 }}>ID: {m.id} โ€ข Size: {m.size}</div>
187
+ <div style={{ fontSize: 10, color: 'var(--text-secondary)', marginTop: 2 }}>{a.desc}</div>
188
+ <div style={{ fontSize: 9, color: 'var(--text-tertiary)', marginTop: 4 }}>Registry ID: {a.id}</div>
182
189
  </div>
183
190
  </div>
184
191
  );
@@ -186,42 +193,24 @@ export default function DeploymentsPage() {
186
193
  </div>
187
194
  </div>
188
195
 
189
- {/* Pull Model Form */}
196
+ {/* Diagnostics Form */}
190
197
  <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 20 }}>
191
198
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
192
199
  <span style={{ fontSize: 20 }}>๐Ÿ“ก</span>
193
200
  <div>
194
- <h3 style={{ fontSize: 14, fontWeight: 600 }}>Pull Catalog Model</h3>
195
- <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Trigger high-performance model downloads in background</p>
201
+ <h3 style={{ fontSize: 14, fontWeight: 600 }}>CLI Agent Diagnostics</h3>
202
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Trigger active paths check and verify registered agents</p>
196
203
  </div>
197
204
  </div>
198
205
 
199
- <form onSubmit={handlePullModel} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
200
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
201
- <label htmlFor="model_pull" style={{ fontSize: 12, fontWeight: 500 }}>Select Catalog Model</label>
202
- <select
203
- id="model_pull"
204
- value={modelToPull}
205
- onChange={(e) => setModelToPull(e.target.value)}
206
- style={{
207
- background: 'var(--bg-tertiary)',
208
- color: 'var(--text-primary)',
209
- border: '1px solid var(--border)',
210
- padding: '8px 12px',
211
- borderRadius: 6,
212
- outline: 'none',
213
- fontSize: 13
214
- }}
215
- >
216
- <option value="gemma4:26b">Gemma 4 (26B Parameter) โ€” 15.6 GB</option>
217
- <option value="gemma5:32b">Gemma 5 Pro (32B Parameter) โ€” 19.2 GB</option>
218
- <option value="llama3:70b">Llama 3 (70B Parameter) โ€” 40 GB</option>
219
- </select>
220
- </div>
206
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
207
+ <p style={{ fontSize: 12, lineHeight: 1.5, color: 'var(--text-secondary)' }}>
208
+ This executes a diagnostic check on registered reasoning agents (equivalent to running <code>npx total-recall upgrade --agents</code> in the background) to audit paths and verify binaries.
209
+ </p>
221
210
 
222
211
  <button
223
- type="submit"
224
- disabled={pulling}
212
+ onClick={handleRunDiagnostics}
213
+ disabled={runningDiagnostics}
225
214
  style={{
226
215
  background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
227
216
  color: '#fff',
@@ -230,12 +219,32 @@ export default function DeploymentsPage() {
230
219
  fontWeight: 500,
231
220
  border: 'none',
232
221
  marginTop: 8,
233
- cursor: pulling ? 'not-allowed' : 'pointer'
222
+ cursor: runningDiagnostics ? 'not-allowed' : 'pointer'
234
223
  }}
235
224
  >
236
- {pulling ? 'โณ Initiating download...' : '๐Ÿš€ Trigger Model Download'}
225
+ {runningDiagnostics ? 'โณ Running Audit...' : '๐Ÿš€ Run Diagnostics Audit'}
237
226
  </button>
238
- </form>
227
+ </div>
228
+
229
+ {diagnosticLogs && (
230
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 10 }}>
231
+ <span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)' }}>Diagnostics Console Output:</span>
232
+ <pre style={{
233
+ background: '#07070a',
234
+ border: '1px solid var(--border)',
235
+ padding: 10,
236
+ borderRadius: 6,
237
+ fontFamily: 'var(--font-mono)',
238
+ fontSize: 11,
239
+ color: '#e6edf3',
240
+ whiteSpace: 'pre-wrap',
241
+ maxHeight: 150,
242
+ overflowY: 'auto'
243
+ }}>
244
+ {diagnosticLogs}
245
+ </pre>
246
+ </div>
247
+ )}
239
248
  </div>
240
249
 
241
250
  </div>
@@ -21,6 +21,26 @@ export default function HealthPage() {
21
21
  const [isUpdating, setIsUpdating] = useState(false)
22
22
  const [showConfirmModal, setShowConfirmModal] = useState(false)
23
23
  const [updateError, setUpdateError] = useState('')
24
+ const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
25
+ const [checkMessage, setCheckMessage] = useState('')
26
+
27
+ const handleManualCheckUpdate = async () => {
28
+ setIsCheckingUpdate(true)
29
+ setCheckMessage('')
30
+ setUpdateError('')
31
+ try {
32
+ const data = await checkUpdate()
33
+ setUpdateInfo(data)
34
+ if (!data.updateAvailable) {
35
+ setCheckMessage('System is up to date!')
36
+ setTimeout(() => setCheckMessage(''), 3000)
37
+ }
38
+ } catch (e) {
39
+ setUpdateError(`Failed to check updates: ${(e as Error).message}`)
40
+ } finally {
41
+ setIsCheckingUpdate(false)
42
+ }
43
+ }
24
44
 
25
45
  useEffect(() => {
26
46
  let active = true
@@ -168,7 +188,7 @@ export default function HealthPage() {
168
188
  <h1>System Health</h1>
169
189
  <p>Live monitoring of your sovereign brain {lastPoll && <span>ยท Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
170
190
  </div>
171
- {updateInfo?.updateAvailable && (
191
+ {updateInfo?.updateAvailable ? (
172
192
  <div className="badge badge-success" style={{
173
193
  display: 'flex',
174
194
  alignItems: 'center',
@@ -199,6 +219,30 @@ export default function HealthPage() {
199
219
  Update Now
200
220
  </button>
201
221
  </div>
222
+ ) : (
223
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
224
+ {checkMessage && (
225
+ <span style={{ color: '#3fb950', fontSize: 14, fontWeight: 500, animation: 'fadeIn 0.3s ease' }}>
226
+ {checkMessage}
227
+ </span>
228
+ )}
229
+ <button
230
+ className="btn btn-secondary"
231
+ disabled={isCheckingUpdate}
232
+ onClick={handleManualCheckUpdate}
233
+ style={{
234
+ fontSize: 13,
235
+ padding: '8px 16px',
236
+ borderRadius: 8,
237
+ cursor: 'pointer',
238
+ display: 'flex',
239
+ alignItems: 'center',
240
+ gap: 6
241
+ }}
242
+ >
243
+ {isCheckingUpdate ? 'Checking...' : 'Check for Updates'}
244
+ </button>
245
+ </div>
202
246
  )}
203
247
  </div>
204
248
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.6.2",
3
+ "version": "3.6.4",
4
4
  "description": "Sovereign AI system โ€” database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -468,6 +468,25 @@ router.get('/api/vault/status', requireAuth, requireScope('memory:read'), async
468
468
  }
469
469
  });
470
470
 
471
+ /**
472
+ * POST /api/diagnostics/agents
473
+ * Run `upgrade --agents` diagnostics checks and return the console text output.
474
+ */
475
+ router.post('/api/diagnostics/agents', requireAuth, requireScope('health:read'), async (req, res) => {
476
+ try {
477
+ const result = spawnSync('node', [path.join(ROOT, 'bin', 'total-recall.mjs'), 'upgrade', '--agents'], {
478
+ encoding: 'utf8',
479
+ cwd: ROOT,
480
+ env: { ...process.env }
481
+ });
482
+ const output = (result.stdout || '') + (result.stderr || '');
483
+ res.json({ success: result.status === 0, output });
484
+ } catch (err) {
485
+ serverError(res, err);
486
+ }
487
+ });
488
+
489
+
471
490
  // โ”€โ”€โ”€ Keys โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ (moved to ./routes/keys.mjs)
472
491
  // โ”€โ”€โ”€ Sessions โ”€โ”€โ”€โ”€โ”€โ”€โ”€ (moved to ./routes/sessions.mjs)
473
492
  // โ”€โ”€โ”€ Sandbox โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -67,6 +67,10 @@
67
67
  "method": "POST",
68
68
  "path": "/api/context/stream"
69
69
  },
70
+ {
71
+ "method": "POST",
72
+ "path": "/api/diagnostics/agents"
73
+ },
70
74
  {
71
75
  "method": "POST",
72
76
  "path": "/api/dream"