total-recall-brain 3.2.2 → 3.3.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.
@@ -1,7 +1,7 @@
1
1
  // Total Recall — Background Service Worker
2
2
  // Handles context menus, message routing, and passive browsing buffer.
3
3
 
4
- importScripts('lib/brain-client.js');
4
+ importScripts('lib/preconfigured.js', 'lib/brain-client.js');
5
5
 
6
6
  // Destructure for convenience after importScripts populates self.BrainClient
7
7
  const { share, search, healthCheck } = self.BrainClient;
@@ -131,4 +131,19 @@
131
131
 
132
132
  // Wait 2 seconds after page load to avoid interfering with page rendering
133
133
  setTimeout(queryBrain, 2000);
134
+
135
+ // ---- Listen for page context requests ----
136
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
137
+ if (msg.type === 'GET_PAGE_TEXT') {
138
+ const selection = window.getSelection().toString().trim();
139
+ const pageText = document.body.innerText.slice(0, 10000); // limit to 10k chars to avoid token blowout
140
+ sendResponse({
141
+ url: location.href,
142
+ title: document.title,
143
+ selection,
144
+ pageText
145
+ });
146
+ }
147
+ return true;
148
+ });
134
149
  })();
@@ -2,46 +2,85 @@
2
2
  // Shared by background.js, popup, sidepanel, and options page.
3
3
  // Exposed as `self.BrainClient` namespace (no ES-module bundler required).
4
4
 
5
- const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
6
-
7
- async function getConfig() {
8
- const config = await chrome.storage.sync.get(['brainUrl', 'pat']);
9
- return {
10
- brainUrl: config.brainUrl || DEFAULT_BRAIN_URL,
11
- pat: config.pat || ''
12
- };
13
- }
14
-
15
- async function brainFetch(path, options = {}) {
16
- const { brainUrl, pat } = await getConfig();
17
- const url = `${brainUrl}${path}`;
18
- const headers = { 'Content-Type': 'application/json', ...options.headers };
19
- if (pat) headers['Authorization'] = `Bearer ${pat}`;
20
-
21
- const res = await fetch(url, { ...options, headers });
22
- if (res.status === 401) {
23
- chrome.action.setBadgeText({ text: '!' });
24
- chrome.action.setBadgeBackgroundColor({ color: '#ef4444' });
25
- throw new Error('Authentication failed — check your PAT token');
5
+ (function () {
6
+ const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
7
+
8
+ async function getConfig() {
9
+ const config = await chrome.storage.sync.get(['brainUrl', 'pat']);
10
+ const pre = self.PreConfigured || {};
11
+ return {
12
+ brainUrl: config.brainUrl || pre.brainUrl || DEFAULT_BRAIN_URL,
13
+ pat: config.pat || pre.pat || ''
14
+ };
15
+ }
16
+
17
+ async function brainFetch(path, options = {}) {
18
+ const { brainUrl, pat } = await getConfig();
19
+ const url = `${brainUrl}${path}`;
20
+ const headers = { 'Content-Type': 'application/json', ...options.headers };
21
+ if (pat) headers['Authorization'] = `Bearer ${pat}`;
22
+
23
+ const res = await fetch(url, { ...options, headers });
24
+ if (res.status === 401) {
25
+ chrome.action.setBadgeText({ text: '!' });
26
+ chrome.action.setBadgeBackgroundColor({ color: '#ef4444' });
27
+ throw new Error('Authentication failed — check your PAT token');
28
+ }
29
+ if (!res.ok) throw new Error(`Brain API error: ${res.status}`);
30
+ return res.json();
31
+ }
32
+
33
+ async function share(data) {
34
+ return brainFetch('/api/share', { method: 'POST', body: JSON.stringify(data) });
35
+ }
36
+
37
+ async function search(query, topK = 3) {
38
+ return brainFetch('/api/memory/search/semantic', {
39
+ method: 'POST',
40
+ body: JSON.stringify({ query, top_k: topK })
41
+ });
26
42
  }
27
- if (!res.ok) throw new Error(`Brain API error: ${res.status}`);
28
- return res.json();
29
- }
30
-
31
- async function share(data) {
32
- return brainFetch('/api/share', { method: 'POST', body: JSON.stringify(data) });
33
- }
34
-
35
- async function search(query, topK = 3) {
36
- return brainFetch('/api/memory/search/semantic', {
37
- method: 'POST',
38
- body: JSON.stringify({ query, top_k: topK })
39
- });
40
- }
41
-
42
- async function healthCheck() {
43
- return brainFetch('/health');
44
- }
45
-
46
- // Expose as a global namespace for non-module contexts (service worker, popup, etc.)
47
- self.BrainClient = { getConfig, brainFetch, share, search, healthCheck };
43
+
44
+ async function healthCheck() {
45
+ return brainFetch('/health');
46
+ }
47
+
48
+ async function chat(messages, options = {}) {
49
+ const { brainUrl, pat } = await getConfig();
50
+ const url = `${brainUrl}/v1/chat/completions`;
51
+ const headers = { 'Content-Type': 'application/json' };
52
+ if (pat) headers['Authorization'] = `Bearer ${pat}`;
53
+ if (options.sessionId) headers['x-session-id'] = options.sessionId;
54
+
55
+ const res = await fetch(url, {
56
+ method: 'POST',
57
+ headers,
58
+ body: JSON.stringify({
59
+ messages,
60
+ model: options.model || 'gemini',
61
+ brainId: options.brainId || 'global',
62
+ groundingNodes: options.groundingNodes || []
63
+ })
64
+ });
65
+ if (res.status === 401) throw new Error('Authentication failed — check your PAT token');
66
+ if (!res.ok) throw new Error(`Brain API error: ${res.status}`);
67
+ return res.json();
68
+ }
69
+
70
+ // Self-heal stale storage: if preconfigured values exist, automatically write them to chrome.storage.sync
71
+ try {
72
+ if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
73
+ chrome.storage.sync.get(['brainUrl', 'pat']).then((config) => {
74
+ const pre = self.PreConfigured || {};
75
+ if (pre.pat && (!config.pat || config.pat !== pre.pat)) {
76
+ chrome.storage.sync.set({ brainUrl: pre.brainUrl, pat: pre.pat });
77
+ }
78
+ });
79
+ }
80
+ } catch (e) {
81
+ // Ignore in contexts where storage sync isn't available
82
+ }
83
+
84
+ // Expose as a global namespace for non-module contexts (service worker, popup, etc.)
85
+ self.BrainClient = { getConfig, brainFetch, share, search, healthCheck, chat };
86
+ })();
@@ -0,0 +1,5 @@
1
+ // Exposes default configuration dynamically injected by the server
2
+ self.PreConfigured = {
3
+ brainUrl: '',
4
+ pat: ''
5
+ };
@@ -3,7 +3,7 @@
3
3
  "name": "Total Recall — Brain Extension",
4
4
  "version": "0.1.0",
5
5
  "description": "Contextual memory overlay, Share-to-Brain, and passive browsing context for Total Recall",
6
- "permissions": ["activeTab", "contextMenus", "sidePanel", "storage"],
6
+ "permissions": ["activeTab", "contextMenus", "sidePanel", "storage", "tabs"],
7
7
  "optional_permissions": ["history", "webNavigation"],
8
8
  "host_permissions": ["http://127.0.0.1:*/*", "http://localhost:*/*"],
9
9
  "background": {
@@ -66,6 +66,7 @@
66
66
  </div>
67
67
  </div>
68
68
 
69
+ <script src="../lib/preconfigured.js"></script>
69
70
  <script src="../lib/brain-client.js"></script>
70
71
  <script src="options.js"></script>
71
72
  </body>
@@ -20,8 +20,9 @@
20
20
  'captureGranularity'
21
21
  ]);
22
22
 
23
- brainUrlInput.value = settings.brainUrl || 'http://127.0.0.1:3000';
24
- patInput.value = settings.pat || '';
23
+ const pre = self.PreConfigured || {};
24
+ brainUrlInput.value = settings.brainUrl || pre.brainUrl || 'http://127.0.0.1:3000';
25
+ patInput.value = settings.pat || pre.pat || '';
25
26
 
26
27
  // Blocklist: stored as array, displayed one per line
27
28
  if (Array.isArray(settings.blocklist)) {
@@ -115,6 +115,12 @@ body {
115
115
  transform: scale(0.97);
116
116
  }
117
117
 
118
+ .btn:disabled, .btn.loading {
119
+ opacity: 0.6;
120
+ cursor: not-allowed;
121
+ transform: none !important;
122
+ }
123
+
118
124
  .btn.primary {
119
125
  background: #89b4fa;
120
126
  color: #11111b;
@@ -38,6 +38,7 @@
38
38
  <div id="status-msg" class="status-msg hidden"></div>
39
39
  </div>
40
40
 
41
+ <script src="../lib/preconfigured.js"></script>
41
42
  <script src="../lib/brain-client.js"></script>
42
43
  <script src="popup.js"></script>
43
44
  </body>
@@ -52,41 +52,84 @@
52
52
 
53
53
  // ---- Remember ----
54
54
  btnRemember.addEventListener('click', async () => {
55
+ if (btnRemember.disabled) return;
56
+ const originalText = btnRemember.innerHTML;
57
+ btnRemember.innerHTML = '⏳ Remembering...';
58
+ btnRemember.disabled = true;
59
+ btnRemember.classList.add('loading');
55
60
  try {
56
61
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
62
+ const note = noteInput.value.trim();
63
+ const targetUrl = tab?.url || urlInput.value || '';
64
+
65
+ // If we don't have a URL, we must have a note/excerpt
66
+ if (!targetUrl && !note) {
67
+ throw new Error('Please enter a note or browse to a webpage to remember.');
68
+ }
69
+ if (targetUrl && (targetUrl.startsWith('chrome://') || targetUrl.startsWith('about:') || targetUrl.startsWith('chrome-extension://'))) {
70
+ if (!note) {
71
+ throw new Error('Cannot capture system pages. Please enter a note to save.');
72
+ }
73
+ }
74
+
57
75
  const payload = {
58
- url: tab?.url || urlInput.value,
76
+ url: targetUrl && !targetUrl.startsWith('chrome://') && !targetUrl.startsWith('about:') && !targetUrl.startsWith('chrome-extension://') ? targetUrl : undefined,
59
77
  title: tab?.title || '',
60
78
  action: 'remember',
61
79
  source: 'chrome-extension-popup'
62
80
  };
63
- const note = noteInput.value.trim();
64
81
  if (note) payload.excerpt = note;
82
+
65
83
  await sendShare(payload);
66
84
  showStatus('Page remembered!');
67
85
  noteInput.value = '';
68
86
  } catch (err) {
69
87
  showStatus('Error: ' + err.message, true);
88
+ } finally {
89
+ btnRemember.innerHTML = originalText;
90
+ btnRemember.disabled = false;
91
+ btnRemember.classList.remove('loading');
70
92
  }
71
93
  });
72
94
 
73
95
  // ---- Research ----
74
96
  btnResearch.addEventListener('click', async () => {
97
+ if (btnResearch.disabled) return;
98
+ const originalText = btnResearch.innerHTML;
99
+ btnResearch.innerHTML = '⏳ Researching...';
100
+ btnResearch.disabled = true;
101
+ btnResearch.classList.add('loading');
75
102
  try {
76
103
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
104
+ const note = noteInput.value.trim();
105
+ const targetUrl = tab?.url || urlInput.value || '';
106
+
107
+ if (!targetUrl && !note) {
108
+ throw new Error('Please enter research notes or browse to a webpage to queue research.');
109
+ }
110
+ if (targetUrl && (targetUrl.startsWith('chrome://') || targetUrl.startsWith('about:') || targetUrl.startsWith('chrome-extension://'))) {
111
+ if (!note) {
112
+ throw new Error('Cannot research system pages. Please enter research notes.');
113
+ }
114
+ }
115
+
77
116
  const payload = {
78
- url: tab?.url || urlInput.value,
117
+ url: targetUrl && !targetUrl.startsWith('chrome://') && !targetUrl.startsWith('about:') && !targetUrl.startsWith('chrome-extension://') ? targetUrl : undefined,
79
118
  title: tab?.title || '',
80
119
  action: 'research',
81
120
  source: 'chrome-extension-popup'
82
121
  };
83
- const note = noteInput.value.trim();
84
122
  if (note) payload.excerpt = note;
123
+
85
124
  await sendShare(payload);
86
125
  showStatus('Research queued!');
87
126
  noteInput.value = '';
88
127
  } catch (err) {
89
128
  showStatus('Error: ' + err.message, true);
129
+ } finally {
130
+ btnResearch.innerHTML = originalText;
131
+ btnResearch.disabled = false;
132
+ btnResearch.classList.remove('loading');
90
133
  }
91
134
  });
92
135