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,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.2",
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
  /**