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
|
@@ -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
|
/**
|