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.
@@ -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
- const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
5
+ (function () {
6
+ const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
6
7
 
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
- }
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 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}`;
33
+ async function share(data) {
34
+ return brainFetch('/api/share', { method: 'POST', body: JSON.stringify(data) });
35
+ }
20
36
 
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');
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 share(data) {
32
- return brainFetch('/api/share', { method: 'POST', body: JSON.stringify(data) });
33
- }
44
+ async function healthCheck() {
45
+ return brainFetch('/health');
46
+ }
34
47
 
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
- }
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
- async function chat(messages, options = {}) {
43
- const { brainUrl, pat } = await getConfig();
44
- const url = `${brainUrl}/v1/chat/completions`;
45
- const headers = { 'Content-Type': 'application/json' };
46
- if (pat) headers['Authorization'] = `Bearer ${pat}`;
47
- if (options.sessionId) headers['x-session-id'] = options.sessionId;
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
- const res = await fetch(url, {
50
- method: 'POST',
51
- headers,
52
- body: JSON.stringify({
53
- messages,
54
- model: options.model || 'gemini',
55
- brainId: options.brainId || 'global',
56
- groundingNodes: options.groundingNodes || []
57
- })
58
- });
59
- if (res.status === 401) throw new Error('Authentication failed — check your PAT token');
60
- if (!res.ok) throw new Error(`Brain API error: ${res.status}`);
61
- return res.json();
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
+ })();
@@ -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)) {
@@ -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>
@@ -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: tab?.url || urlInput.value,
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: tab?.url || urlInput.value,
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 = '';
@@ -130,6 +130,7 @@
130
130
 
131
131
  </div>
132
132
  </div>
133
+ <script src="../lib/preconfigured.js"></script>
133
134
  <script src="../lib/brain-client.js"></script>
134
135
  <script src="sidepanel.js"></script>
135
136
  </body>
@@ -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
- const tab = await getCurrentTab();
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: 'fact',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.2.3",
3
+ "version": "3.3.0",
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": {
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://127.0.0.1:3000';
554
+ brainUrl = 'http://localhost:3000';
546
555
  }
547
556
 
548
557
  // ── Step 3: Domain (optional) ────────────────────────────────────────────────
@@ -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 => { if (code !== 0 && !res.writableEnded) res.end(); });
762
- } catch (err) { serverError(res, err); }
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
  /**