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.
- package/extension/background.js +1 -1
- package/extension/content-script.js +15 -0
- package/extension/lib/brain-client.js +81 -42
- package/extension/lib/preconfigured.js +5 -0
- package/extension/manifest.json +1 -1
- package/extension/options/options.html +1 -0
- package/extension/options/options.js +3 -2
- package/extension/popup/popup.css +6 -0
- package/extension/popup/popup.html +1 -0
- package/extension/popup/popup.js +47 -4
- package/extension/sidepanel/sidepanel.css +426 -29
- package/extension/sidepanel/sidepanel.html +119 -20
- package/extension/sidepanel/sidepanel.js +498 -29
- package/frontend/src/pages/IntegrationsPage.tsx +140 -1
- package/package.json +1 -1
- package/src/cli/init.mjs +30 -0
- package/src/cli/setup.mjs +10 -1
- package/src/server/rest.mjs +47 -9
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
})();
|
package/extension/manifest.json
CHANGED
|
@@ -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": {
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
'captureGranularity'
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
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)) {
|
package/extension/popup/popup.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|