total-recall-brain 3.2.3 → 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/lib/brain-client.js +73 -52
- 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.html +1 -0
- package/extension/popup/popup.js +29 -4
- package/extension/sidepanel/sidepanel.html +1 -0
- package/extension/sidepanel/sidepanel.js +24 -6
- 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;
|
|
@@ -2,64 +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
|
-
|
|
5
|
+
(function () {
|
|
6
|
+
const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
|
|
6
7
|
|
|
7
|
-
async function getConfig() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
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
|
+
}
|
|
14
32
|
|
|
15
|
-
async function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
|
19
|
-
if (pat) headers['Authorization'] = `Bearer ${pat}`;
|
|
33
|
+
async function share(data) {
|
|
34
|
+
return brainFetch('/api/share', { method: 'POST', body: JSON.stringify(data) });
|
|
35
|
+
}
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
43
|
|
|
31
|
-
async function
|
|
32
|
-
|
|
33
|
-
}
|
|
44
|
+
async function healthCheck() {
|
|
45
|
+
return brainFetch('/health');
|
|
46
|
+
}
|
|
34
47
|
|
|
35
|
-
async function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
|
48
69
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
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
|
+
}
|
|
63
83
|
|
|
64
|
-
// Expose as a global namespace for non-module contexts (service worker, popup, etc.)
|
|
65
|
-
self.BrainClient = { getConfig, brainFetch, share, search, healthCheck, chat };
|
|
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
|
@@ -59,14 +59,27 @@
|
|
|
59
59
|
btnRemember.classList.add('loading');
|
|
60
60
|
try {
|
|
61
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
|
+
|
|
62
75
|
const payload = {
|
|
63
|
-
url:
|
|
76
|
+
url: targetUrl && !targetUrl.startsWith('chrome://') && !targetUrl.startsWith('about:') && !targetUrl.startsWith('chrome-extension://') ? targetUrl : undefined,
|
|
64
77
|
title: tab?.title || '',
|
|
65
78
|
action: 'remember',
|
|
66
79
|
source: 'chrome-extension-popup'
|
|
67
80
|
};
|
|
68
|
-
const note = noteInput.value.trim();
|
|
69
81
|
if (note) payload.excerpt = note;
|
|
82
|
+
|
|
70
83
|
await sendShare(payload);
|
|
71
84
|
showStatus('Page remembered!');
|
|
72
85
|
noteInput.value = '';
|
|
@@ -88,14 +101,26 @@
|
|
|
88
101
|
btnResearch.classList.add('loading');
|
|
89
102
|
try {
|
|
90
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
|
+
|
|
91
116
|
const payload = {
|
|
92
|
-
url:
|
|
117
|
+
url: targetUrl && !targetUrl.startsWith('chrome://') && !targetUrl.startsWith('about:') && !targetUrl.startsWith('chrome-extension://') ? targetUrl : undefined,
|
|
93
118
|
title: tab?.title || '',
|
|
94
119
|
action: 'research',
|
|
95
120
|
source: 'chrome-extension-popup'
|
|
96
121
|
};
|
|
97
|
-
const note = noteInput.value.trim();
|
|
98
122
|
if (note) payload.excerpt = note;
|
|
123
|
+
|
|
99
124
|
await sendShare(payload);
|
|
100
125
|
showStatus('Research queued!');
|
|
101
126
|
noteInput.value = '';
|
|
@@ -183,9 +183,15 @@
|
|
|
183
183
|
btnRemember.classList.add('loading');
|
|
184
184
|
try {
|
|
185
185
|
const tab = await getCurrentTab();
|
|
186
|
+
if (!tab || !tab.url) {
|
|
187
|
+
throw new Error('No active webpage found to capture. Make sure you are on a webpage tab.');
|
|
188
|
+
}
|
|
189
|
+
if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:') || tab.url.startsWith('chrome-extension://')) {
|
|
190
|
+
throw new Error('Cannot capture system pages.');
|
|
191
|
+
}
|
|
186
192
|
await sendShare({
|
|
187
193
|
url: tab.url,
|
|
188
|
-
title: tab.title,
|
|
194
|
+
title: tab.title || 'Untitled Page',
|
|
189
195
|
action: 'remember',
|
|
190
196
|
source: 'chrome-extension-sidepanel'
|
|
191
197
|
});
|
|
@@ -208,9 +214,15 @@
|
|
|
208
214
|
btnResearch.classList.add('loading');
|
|
209
215
|
try {
|
|
210
216
|
const tab = await getCurrentTab();
|
|
217
|
+
if (!tab || !tab.url) {
|
|
218
|
+
throw new Error('No active webpage found to capture. Make sure you are on a webpage tab.');
|
|
219
|
+
}
|
|
220
|
+
if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:') || tab.url.startsWith('chrome-extension://')) {
|
|
221
|
+
throw new Error('Cannot capture system pages.');
|
|
222
|
+
}
|
|
211
223
|
await sendShare({
|
|
212
224
|
url: tab.url,
|
|
213
|
-
title: tab.title,
|
|
225
|
+
title: tab.title || 'Untitled Page',
|
|
214
226
|
action: 'research',
|
|
215
227
|
source: 'chrome-extension-sidepanel'
|
|
216
228
|
});
|
|
@@ -245,13 +257,19 @@
|
|
|
245
257
|
btnSaveNote.disabled = true;
|
|
246
258
|
btnSaveNote.classList.add('loading');
|
|
247
259
|
try {
|
|
248
|
-
|
|
260
|
+
let tab = null;
|
|
261
|
+
try {
|
|
262
|
+
tab = await getCurrentTab();
|
|
263
|
+
} catch {
|
|
264
|
+
// ignore
|
|
265
|
+
}
|
|
266
|
+
const hasValidUrl = tab && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('about:') && !tab.url.startsWith('chrome-extension://');
|
|
249
267
|
await sendShare({
|
|
250
|
-
url: tab.url,
|
|
251
|
-
title: `Note: ${text.slice(0, 50)}
|
|
268
|
+
url: hasValidUrl ? tab.url : undefined,
|
|
269
|
+
title: hasValidUrl ? `Note: ${text.slice(0, 50)}` : undefined,
|
|
252
270
|
excerpt: text,
|
|
253
271
|
action: 'remember',
|
|
254
|
-
category: '
|
|
272
|
+
category: 'facts',
|
|
255
273
|
source: 'chrome-extension-sidepanel'
|
|
256
274
|
});
|
|
257
275
|
noteInput.value = '';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo, useState, useEffect } from 'react'
|
|
2
|
-
import { getApiBase, listApiKeys, connectClient, fetchActiveIntegrations } from '../api'
|
|
2
|
+
import { getApiBase, listApiKeys, connectClient, fetchActiveIntegrations, fetchExtensionStatus } from '../api'
|
|
3
3
|
import type { ApiKey } from '../api'
|
|
4
4
|
|
|
5
5
|
type Preset = {
|
|
@@ -68,6 +68,7 @@ export default function IntegrationsPage() {
|
|
|
68
68
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
|
69
69
|
const [keys, setKeys] = useState<ApiKey[]>([])
|
|
70
70
|
const [activeIdes, setActiveIdes] = useState<string[]>([])
|
|
71
|
+
const [extStatus, setExtStatus] = useState<{ available: boolean; connected: boolean } | null>(null)
|
|
71
72
|
|
|
72
73
|
const baseUrl = useMemo(() => {
|
|
73
74
|
const configured = getApiBase()
|
|
@@ -91,6 +92,12 @@ export default function IntegrationsPage() {
|
|
|
91
92
|
})
|
|
92
93
|
.catch(() => {})
|
|
93
94
|
|
|
95
|
+
fetchExtensionStatus()
|
|
96
|
+
.then(res => {
|
|
97
|
+
if (active) setExtStatus(res)
|
|
98
|
+
})
|
|
99
|
+
.catch(() => {})
|
|
100
|
+
|
|
94
101
|
return () => { active = false }
|
|
95
102
|
}, [])
|
|
96
103
|
|
|
@@ -221,6 +228,138 @@ export default function IntegrationsPage() {
|
|
|
221
228
|
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
|
222
229
|
gap: 16,
|
|
223
230
|
}}>
|
|
231
|
+
{/* Chrome Extension Integration Card */}
|
|
232
|
+
<div style={{
|
|
233
|
+
border: '1px solid var(--border)',
|
|
234
|
+
background: 'var(--bg-secondary)',
|
|
235
|
+
borderRadius: 12,
|
|
236
|
+
padding: 18,
|
|
237
|
+
display: 'flex',
|
|
238
|
+
flexDirection: 'column',
|
|
239
|
+
gap: 12,
|
|
240
|
+
position: 'relative',
|
|
241
|
+
}}>
|
|
242
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
|
|
243
|
+
<div>
|
|
244
|
+
<h3 style={{ margin: '0 0 4px' }}>Chrome Extension</h3>
|
|
245
|
+
<div style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>Browser Overlay Panel & Capture</div>
|
|
246
|
+
</div>
|
|
247
|
+
{extStatus ? (
|
|
248
|
+
extStatus.connected ? (
|
|
249
|
+
<span style={{
|
|
250
|
+
display: 'inline-flex',
|
|
251
|
+
alignItems: 'center',
|
|
252
|
+
gap: 6,
|
|
253
|
+
padding: '4px 8px',
|
|
254
|
+
borderRadius: 6,
|
|
255
|
+
background: 'rgba(16, 185, 129, 0.1)',
|
|
256
|
+
border: '1px solid rgba(16, 185, 129, 0.3)',
|
|
257
|
+
fontSize: 12,
|
|
258
|
+
color: '#34d399',
|
|
259
|
+
fontWeight: 500,
|
|
260
|
+
}}>
|
|
261
|
+
<span style={{
|
|
262
|
+
width: 8,
|
|
263
|
+
height: 8,
|
|
264
|
+
borderRadius: '50%',
|
|
265
|
+
background: '#10b981',
|
|
266
|
+
boxShadow: '0 0 8px #10b981',
|
|
267
|
+
}} />
|
|
268
|
+
Connected
|
|
269
|
+
</span>
|
|
270
|
+
) : (
|
|
271
|
+
<span style={{
|
|
272
|
+
display: 'inline-flex',
|
|
273
|
+
alignItems: 'center',
|
|
274
|
+
gap: 6,
|
|
275
|
+
padding: '4px 8px',
|
|
276
|
+
borderRadius: 6,
|
|
277
|
+
background: 'rgba(245, 158, 11, 0.1)',
|
|
278
|
+
border: '1px solid rgba(245, 158, 11, 0.3)',
|
|
279
|
+
fontSize: 12,
|
|
280
|
+
color: '#fbbf24',
|
|
281
|
+
fontWeight: 500,
|
|
282
|
+
}}>
|
|
283
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f59e0b' }} />
|
|
284
|
+
Not Connected
|
|
285
|
+
</span>
|
|
286
|
+
)
|
|
287
|
+
) : (
|
|
288
|
+
<span style={{
|
|
289
|
+
display: 'inline-flex',
|
|
290
|
+
alignItems: 'center',
|
|
291
|
+
gap: 6,
|
|
292
|
+
padding: '4px 8px',
|
|
293
|
+
borderRadius: 6,
|
|
294
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
295
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
296
|
+
fontSize: 12,
|
|
297
|
+
color: 'var(--text-tertiary)'
|
|
298
|
+
}}>
|
|
299
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#94a3b8' }} />
|
|
300
|
+
Checking...
|
|
301
|
+
</span>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
306
|
+
Scopes utilized: <code>chat:write, memory:read, memory:write</code>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div style={{
|
|
310
|
+
background: 'var(--bg-primary)',
|
|
311
|
+
border: '1px solid var(--border)',
|
|
312
|
+
borderRadius: 8,
|
|
313
|
+
padding: 10,
|
|
314
|
+
fontSize: 13,
|
|
315
|
+
color: 'var(--text-secondary)',
|
|
316
|
+
lineHeight: '1.4'
|
|
317
|
+
}}>
|
|
318
|
+
Install the Total Recall extension to passively capture context, store quick notes, and access the active brain search overlay inside any Chrome tab.
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div style={{
|
|
322
|
+
background: 'var(--bg-primary)',
|
|
323
|
+
border: '1px solid var(--border)',
|
|
324
|
+
borderRadius: 8,
|
|
325
|
+
padding: 10,
|
|
326
|
+
fontFamily: 'monospace',
|
|
327
|
+
fontSize: 11,
|
|
328
|
+
whiteSpace: 'pre-wrap',
|
|
329
|
+
color: 'var(--text-tertiary)',
|
|
330
|
+
}}>
|
|
331
|
+
1. Download zip & unzip locally.{"\n"}
|
|
332
|
+
2. Go to chrome://extensions in browser.{"\n"}
|
|
333
|
+
3. Enable "Developer mode" (top right).{"\n"}
|
|
334
|
+
4. Click "Load unpacked" & select unzipped folder.
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div style={{ display: 'flex', gap: 10, marginTop: 'auto', paddingTop: 8 }}>
|
|
338
|
+
<a
|
|
339
|
+
href={`${baseUrl}/api/extension/download`}
|
|
340
|
+
download
|
|
341
|
+
className="btn btn-sm btn-primary"
|
|
342
|
+
style={{
|
|
343
|
+
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
|
|
344
|
+
border: 'none',
|
|
345
|
+
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.3)',
|
|
346
|
+
fontWeight: 600,
|
|
347
|
+
cursor: 'pointer',
|
|
348
|
+
textDecoration: 'none',
|
|
349
|
+
display: 'inline-flex',
|
|
350
|
+
alignItems: 'center',
|
|
351
|
+
justifyContent: 'center',
|
|
352
|
+
padding: '6px 12px',
|
|
353
|
+
borderRadius: '6px',
|
|
354
|
+
color: '#fff',
|
|
355
|
+
fontSize: '12px'
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
⬇ Download Extension Zip
|
|
359
|
+
</a>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
224
363
|
{filteredPresets.map(preset => {
|
|
225
364
|
const snippet = preset.snippet(baseUrl)
|
|
226
365
|
|
package/package.json
CHANGED
package/src/cli/init.mjs
CHANGED
|
@@ -533,6 +533,36 @@ export default async function init(args) {
|
|
|
533
533
|
if (opts.token) brainCfg.token = opts.token;
|
|
534
534
|
fs.writeFileSync(path.join(cfgDir, 'brain.json'), JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
|
|
535
535
|
logOk(`Registered brain at ${opts.brain}. Run \`npx total-recall sync\` to pull instructions.`);
|
|
536
|
+
} else {
|
|
537
|
+
// Zero-config bootstrap: auto-generate localhost brain.json and valid Developer PAT out-of-the-box
|
|
538
|
+
const cfgDir = path.join(brainDir, 'config');
|
|
539
|
+
fs.mkdirSync(cfgDir, { recursive: true });
|
|
540
|
+
const brainJsonPath = path.join(cfgDir, 'brain.json');
|
|
541
|
+
let needsBootstrap = false;
|
|
542
|
+
let brainCfg = {};
|
|
543
|
+
if (fs.existsSync(brainJsonPath)) {
|
|
544
|
+
try {
|
|
545
|
+
brainCfg = JSON.parse(fs.readFileSync(brainJsonPath, 'utf8'));
|
|
546
|
+
if (!brainCfg.url || !brainCfg.token) needsBootstrap = true;
|
|
547
|
+
} catch {
|
|
548
|
+
needsBootstrap = true;
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
needsBootstrap = true;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (needsBootstrap) {
|
|
555
|
+
try {
|
|
556
|
+
const { issueKey } = await import('../server/keys.mjs');
|
|
557
|
+
const keyData = issueKey('Default Local Developer Key', { scopes: ['*'] });
|
|
558
|
+
brainCfg.url = brainCfg.url || 'http://localhost:3000';
|
|
559
|
+
brainCfg.token = brainCfg.token || keyData.token;
|
|
560
|
+
fs.writeFileSync(brainJsonPath, JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
|
|
561
|
+
logOk(`Bootstrap configuration successfully generated and pre-authorized at ${brainCfg.url}`);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
logWarn(`Could not auto-generate bootstrap developer key: ${err.message}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
536
566
|
}
|
|
537
567
|
|
|
538
568
|
let dashboardUrl = '';
|
package/src/cli/setup.mjs
CHANGED
|
@@ -541,8 +541,17 @@ export default async function setup(args) {
|
|
|
541
541
|
], { stdio: 'inherit' });
|
|
542
542
|
if (result.status !== 0) throw new Error('Local deploy failed');
|
|
543
543
|
ok('Local deploy complete!');
|
|
544
|
+
|
|
545
|
+
// Auto-issue a local developer key for zero-config extension / IDE connection
|
|
546
|
+
try {
|
|
547
|
+
const { issueKey } = await import('../server/keys.mjs');
|
|
548
|
+
const keyData = issueKey('Default Local Developer Key', { scopes: ['*'] });
|
|
549
|
+
pat = keyData.token;
|
|
550
|
+
} catch (err) {
|
|
551
|
+
warn(`Could not auto-generate local developer key: ${err.message}`);
|
|
552
|
+
}
|
|
544
553
|
}
|
|
545
|
-
brainUrl = 'http://
|
|
554
|
+
brainUrl = 'http://localhost:3000';
|
|
546
555
|
}
|
|
547
556
|
|
|
548
557
|
// ── Step 3: Domain (optional) ────────────────────────────────────────────────
|
package/src/server/rest.mjs
CHANGED
|
@@ -728,12 +728,17 @@ router.get('/api/brain/export', requireAuth, requireScope('brain:export'), async
|
|
|
728
728
|
|
|
729
729
|
// ─── Chrome Extension Download ───────────────────────────────────────────────
|
|
730
730
|
|
|
731
|
-
/**
|
|
732
|
-
* GET /api/extension/download
|
|
733
|
-
* Streams the Chrome extension directory as a .zip file for easy installation.
|
|
734
|
-
* No auth required — the extension itself is not sensitive.
|
|
735
|
-
*/
|
|
736
731
|
router.get('/api/extension/download', async (_req, res) => {
|
|
732
|
+
let preconfiguredPath = '';
|
|
733
|
+
let originalContent = '';
|
|
734
|
+
const cleanup = () => {
|
|
735
|
+
if (preconfiguredPath && originalContent) {
|
|
736
|
+
try {
|
|
737
|
+
fs.writeFileSync(preconfiguredPath, originalContent, 'utf8');
|
|
738
|
+
} catch {}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
737
742
|
try {
|
|
738
743
|
// Extension lives at <package-root>/extension/
|
|
739
744
|
const extDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../extension');
|
|
@@ -741,6 +746,31 @@ router.get('/api/extension/download', async (_req, res) => {
|
|
|
741
746
|
return res.status(404).json({ error: 'Chrome extension not found in this installation.' });
|
|
742
747
|
}
|
|
743
748
|
|
|
749
|
+
// Read local brain configuration to get active URL and token
|
|
750
|
+
const brainPath = path.join(CONFIG_DIR, 'brain.json');
|
|
751
|
+
let brainUrl = '';
|
|
752
|
+
let patToken = '';
|
|
753
|
+
if (fs.existsSync(brainPath)) {
|
|
754
|
+
try {
|
|
755
|
+
const brain = JSON.parse(fs.readFileSync(brainPath, 'utf8'));
|
|
756
|
+
brainUrl = brain.url || '';
|
|
757
|
+
patToken = brain.token || '';
|
|
758
|
+
} catch {}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Overwrite preconfigured.js temporarily with injected values
|
|
762
|
+
preconfiguredPath = path.join(extDir, 'lib', 'preconfigured.js');
|
|
763
|
+
if (fs.existsSync(preconfiguredPath)) {
|
|
764
|
+
originalContent = fs.readFileSync(preconfiguredPath, 'utf8');
|
|
765
|
+
const injectedContent = `// Exposes default configuration dynamically injected by the server
|
|
766
|
+
self.PreConfigured = {
|
|
767
|
+
brainUrl: "${brainUrl}",
|
|
768
|
+
pat: "${patToken}"
|
|
769
|
+
};
|
|
770
|
+
`;
|
|
771
|
+
fs.writeFileSync(preconfiguredPath, injectedContent, 'utf8');
|
|
772
|
+
}
|
|
773
|
+
|
|
744
774
|
res.setHeader('Content-Type', 'application/zip');
|
|
745
775
|
res.setHeader('Content-Disposition', 'attachment; filename="total-recall-extension.zip"');
|
|
746
776
|
|
|
@@ -754,12 +784,20 @@ router.get('/api/extension/download', async (_req, res) => {
|
|
|
754
784
|
res.setHeader('Content-Disposition', 'attachment; filename="total-recall-extension.tar.gz"');
|
|
755
785
|
const tar = spawn('tar', ['czf', '-', '-C', path.dirname(extDir), 'extension'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
756
786
|
tar.stdout.pipe(res);
|
|
757
|
-
tar.on('error', err => { if (!res.headersSent) serverError(res, err); });
|
|
758
|
-
tar.on('close', () => { if (!res.writableEnded) res.end(); });
|
|
787
|
+
tar.on('error', err => { cleanup(); if (!res.headersSent) serverError(res, err); });
|
|
788
|
+
tar.on('close', () => { cleanup(); if (!res.writableEnded) res.end(); });
|
|
789
|
+
} else {
|
|
790
|
+
cleanup();
|
|
759
791
|
}
|
|
760
792
|
});
|
|
761
|
-
zip.on('close', code => {
|
|
762
|
-
|
|
793
|
+
zip.on('close', code => {
|
|
794
|
+
cleanup();
|
|
795
|
+
if (code !== 0 && !res.writableEnded) res.end();
|
|
796
|
+
});
|
|
797
|
+
} catch (err) {
|
|
798
|
+
cleanup();
|
|
799
|
+
serverError(res, err);
|
|
800
|
+
}
|
|
763
801
|
});
|
|
764
802
|
|
|
765
803
|
/**
|