total-recall-brain 3.1.2 → 3.2.2

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.
Files changed (63) hide show
  1. package/extension/background.js +99 -0
  2. package/extension/content-script.css +16 -0
  3. package/extension/content-script.js +134 -0
  4. package/extension/icons/README.md +27 -0
  5. package/extension/icons/icon-128.png +0 -0
  6. package/extension/icons/icon-16.png +0 -0
  7. package/extension/icons/icon-48.png +0 -0
  8. package/extension/lib/brain-client.js +47 -0
  9. package/extension/manifest.json +39 -0
  10. package/extension/options/options.css +218 -0
  11. package/extension/options/options.html +72 -0
  12. package/extension/options/options.js +99 -0
  13. package/extension/popup/popup.css +221 -0
  14. package/extension/popup/popup.html +44 -0
  15. package/extension/popup/popup.js +107 -0
  16. package/extension/sidepanel/sidepanel.css +340 -0
  17. package/extension/sidepanel/sidepanel.html +38 -0
  18. package/extension/sidepanel/sidepanel.js +229 -0
  19. package/frontend/README.md +73 -0
  20. package/frontend/index.html +17 -0
  21. package/frontend/package.json +31 -0
  22. package/frontend/src/App.css +2 -0
  23. package/frontend/src/App.tsx +390 -0
  24. package/frontend/src/api.ts +688 -0
  25. package/frontend/src/assets/hero.png +0 -0
  26. package/frontend/src/assets/react.svg +1 -0
  27. package/frontend/src/assets/vite.svg +1 -0
  28. package/frontend/src/components/BrainSelector.tsx +272 -0
  29. package/frontend/src/components/DaemonLogsTab.tsx +54 -0
  30. package/frontend/src/components/Graph3D.tsx +850 -0
  31. package/frontend/src/components/MarkdownUtils.tsx +99 -0
  32. package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
  33. package/frontend/src/components/TaskQueueTab.tsx +185 -0
  34. package/frontend/src/index.css +1589 -0
  35. package/frontend/src/main.tsx +10 -0
  36. package/frontend/src/pages/ApiKeysPage.tsx +288 -0
  37. package/frontend/src/pages/AutomationsPage.tsx +354 -0
  38. package/frontend/src/pages/ChatPage.tsx +831 -0
  39. package/frontend/src/pages/DeploymentsPage.tsx +275 -0
  40. package/frontend/src/pages/FilesPage.tsx +384 -0
  41. package/frontend/src/pages/HealthPage.tsx +91 -0
  42. package/frontend/src/pages/IntegrationsPage.tsx +382 -0
  43. package/frontend/src/pages/LoginPage.tsx +237 -0
  44. package/frontend/src/pages/MemoryPage.tsx +1192 -0
  45. package/frontend/src/pages/SandboxPage.tsx +113 -0
  46. package/frontend/src/pages/SettingsPage.tsx +662 -0
  47. package/frontend/src/pages/SkillsPage.tsx +618 -0
  48. package/frontend/src/pages/TasksPage.tsx +392 -0
  49. package/frontend/src/pages/UsagePage.tsx +260 -0
  50. package/frontend/src/types.ts +163 -0
  51. package/frontend/tsconfig.app.json +25 -0
  52. package/frontend/tsconfig.json +7 -0
  53. package/frontend/tsconfig.node.json +24 -0
  54. package/frontend/vite.config.ts +24 -0
  55. package/package.json +8 -1
  56. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
  57. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
  58. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
  59. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
  60. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
  61. package/src/cli/backup.mjs +172 -1
  62. package/src/cli/setup.mjs +24 -0
  63. package/src/server/rest.mjs +56 -7
@@ -0,0 +1,91 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { fetchHealth } from '../api'
3
+ import type { HealthData } from '../types'
4
+
5
+ function formatUptime(seconds: number): string {
6
+ const d = Math.floor(seconds / 86400)
7
+ const h = Math.floor((seconds % 86400) / 3600)
8
+ const m = Math.floor((seconds % 3600) / 60)
9
+ if (d > 0) return `${d}d ${h}h ${m}m`
10
+ if (h > 0) return `${h}h ${m}m`
11
+ return `${m}m ${seconds % 60}s`
12
+ }
13
+
14
+ export default function HealthPage() {
15
+ const [health, setHealth] = useState<HealthData | null>(null)
16
+ const [error, setError] = useState('')
17
+ const [lastPoll, setLastPoll] = useState<Date | null>(null)
18
+
19
+ useEffect(() => {
20
+ let active = true
21
+ const poll = async () => {
22
+ try {
23
+ const data = await fetchHealth()
24
+ if (active) {
25
+ setHealth(data)
26
+ setError('')
27
+ setLastPoll(new Date())
28
+ }
29
+ } catch (e) {
30
+ if (active) setError((e as Error).message)
31
+ }
32
+ }
33
+
34
+ poll()
35
+ const interval = setInterval(poll, 5000)
36
+ return () => { active = false; clearInterval(interval) }
37
+ }, [])
38
+
39
+ return (
40
+ <div className="page">
41
+ <div className="page-header">
42
+ <h1>System Health</h1>
43
+ <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
44
+ </div>
45
+
46
+ {error && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ {error}</div>}
47
+
48
+ {health ? (
49
+ <>
50
+ <div className="health-grid">
51
+ <div className="stat-card healthy">
52
+ <div className="value" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
53
+ <span className="pulse" /> {health.status}
54
+ </div>
55
+ <div className="label">Status</div>
56
+ </div>
57
+ <div className="stat-card">
58
+ <div className="value" style={{ color: 'var(--accent)' }}>{health.version}</div>
59
+ <div className="label">Version</div>
60
+ </div>
61
+ <div className="stat-card">
62
+ <div className="value" style={{ color: 'var(--text-primary)' }}>{formatUptime(health.uptime_seconds)}</div>
63
+ <div className="label">Uptime</div>
64
+ </div>
65
+ <div className="stat-card">
66
+ <div className="value" style={{ color: 'var(--text-primary)', fontSize: 16 }}>{new Date(health.timestamp).toLocaleString()}</div>
67
+ <div className="label">Server Time</div>
68
+ </div>
69
+ </div>
70
+
71
+ <div className="health-log">
72
+ <h3>System Info</h3>
73
+ <table>
74
+ <tbody>
75
+ <tr><th>Status</th><td>{health.status}</td></tr>
76
+ <tr><th>Version</th><td>{health.version}</td></tr>
77
+ <tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
78
+ <tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
79
+ <tr><th>Polling</th><td>Every 5 seconds</td></tr>
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ </>
84
+ ) : !error ? (
85
+ <div className="health-grid">
86
+ {[1, 2, 3, 4].map(i => <div key={i} className="skeleton" style={{ height: 100 }} />)}
87
+ </div>
88
+ ) : null}
89
+ </div>
90
+ )
91
+ }
@@ -0,0 +1,382 @@
1
+ import { useMemo, useState, useEffect } from 'react'
2
+ import { getApiBase, listApiKeys, connectClient, fetchActiveIntegrations } from '../api'
3
+ import type { ApiKey } from '../api'
4
+
5
+ type Preset = {
6
+ id: string
7
+ name: string
8
+ surface: string
9
+ scopes: string
10
+ command: string
11
+ snippet: (baseUrl: string) => string
12
+ }
13
+
14
+ const PRESETS: Preset[] = [
15
+ {
16
+ id: 'antigravity',
17
+ name: 'Antigravity',
18
+ surface: 'AGENTS.md',
19
+ scopes: 'ssss:read, memory:read',
20
+ command: 'npx total-recall connect antigravity',
21
+ snippet: baseUrl => `Read AGENTS.md from the repo root.\nDiscovery: ${baseUrl}/.well-known/total-recall.json`
22
+ },
23
+ {
24
+ id: 'cursor',
25
+ name: 'Cursor',
26
+ surface: '.cursor/rules/total-recall.mdc',
27
+ scopes: 'ssss:read, memory:read',
28
+ command: 'npx total-recall connect cursor',
29
+ snippet: baseUrl => `Discovery: ${baseUrl}/.well-known/total-recall.json`
30
+ },
31
+ {
32
+ id: 'claude-code',
33
+ name: 'Claude Code',
34
+ surface: 'CLAUDE.md',
35
+ scopes: 'ssss:read, memory:read',
36
+ command: 'npx total-recall connect claude-code',
37
+ snippet: () => `Read CLAUDE.md from the repo root.`
38
+ },
39
+ {
40
+ id: 'codex',
41
+ name: 'Codex',
42
+ surface: 'AGENTS.md',
43
+ scopes: 'ssss:read, memory:read',
44
+ command: 'npx total-recall connect codex',
45
+ snippet: baseUrl => `Read AGENTS.md from the repo root.\nDiscovery: ${baseUrl}/.well-known/total-recall.json`
46
+ },
47
+ {
48
+ id: 'ultrachat',
49
+ name: 'UltraChat',
50
+ surface: 'OpenAI-compatible provider',
51
+ scopes: 'models:read, chat:write, ssss:read',
52
+ command: 'npx total-recall connect ultrachat --brain <brain-url>',
53
+ snippet: baseUrl => `Base URL: ${baseUrl}/v1\nModel: total-recall/gemma4\nModels: ${baseUrl}/v1/models`
54
+ },
55
+ {
56
+ id: 'generic',
57
+ name: 'Generic Client',
58
+ surface: 'REST + OpenAI-compatible API',
59
+ scopes: 'models:read, chat:write, memory:read',
60
+ command: 'npx total-recall connect generic --brain <brain-url>',
61
+ snippet: baseUrl => `POST ${baseUrl}/v1/chat/completions\nGET ${baseUrl}/api/ssss\nAuthorization: Bearer <PAT>`
62
+ }
63
+ ]
64
+
65
+ export default function IntegrationsPage() {
66
+ const [copied, setCopied] = useState<string | null>(null)
67
+ const [connecting, setConnecting] = useState<string | null>(null)
68
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
69
+ const [keys, setKeys] = useState<ApiKey[]>([])
70
+ const [activeIdes, setActiveIdes] = useState<string[]>([])
71
+
72
+ const baseUrl = useMemo(() => {
73
+ const configured = getApiBase()
74
+ if (configured) return configured.replace(/\/$/, '')
75
+ return window.location.origin
76
+ }, [])
77
+
78
+ useEffect(() => {
79
+ let active = true
80
+ listApiKeys()
81
+ .then(res => {
82
+ if (active) setKeys(res.keys || [])
83
+ })
84
+ .catch(() => {})
85
+
86
+ fetchActiveIntegrations()
87
+ .then(res => {
88
+ if (active && res.success && Array.isArray(res.active)) {
89
+ setActiveIdes(res.active)
90
+ }
91
+ })
92
+ .catch(() => {})
93
+
94
+ return () => { active = false }
95
+ }, [])
96
+
97
+ const filteredPresets = useMemo(() => {
98
+ if (activeIdes.length === 0) return PRESETS
99
+ return PRESETS.filter(preset => {
100
+ if (['ultrachat', 'generic'].includes(preset.id)) {
101
+ return true
102
+ }
103
+ return activeIdes.includes(preset.id)
104
+ })
105
+ }, [activeIdes])
106
+
107
+ const copy = async (id: string, value: string) => {
108
+ await navigator.clipboard.writeText(value)
109
+ setCopied(id)
110
+ setTimeout(() => setCopied(null), 1800)
111
+ }
112
+
113
+ const handleConnect = async (presetId: string) => {
114
+ setConnecting(presetId)
115
+ setMessage(null)
116
+ try {
117
+ const result = await connectClient(presetId, baseUrl)
118
+ if (result.success) {
119
+ setMessage({ type: 'success', text: `Successfully connected ${presetId}! All config files have been automatically updated.` })
120
+ // Refresh keys to show registered/connected status badge
121
+ const res = await listApiKeys()
122
+ setKeys(res.keys || [])
123
+ } else {
124
+ setMessage({ type: 'error', text: `Failed to connect: ${result.message}` })
125
+ }
126
+ } catch (e) {
127
+ setMessage({ type: 'error', text: (e as Error).message || 'Failed to connect.' })
128
+ } finally {
129
+ setConnecting(null)
130
+ }
131
+ }
132
+
133
+ return (
134
+ <div className="page">
135
+ <div className="page-header">
136
+ <h1>Integrations</h1>
137
+ <p>Connect IDEs, UltraChat, and scripts to this Total Recall brain</p>
138
+ </div>
139
+
140
+ {message && (
141
+ <div style={{
142
+ padding: '12px 16px',
143
+ borderRadius: 8,
144
+ marginBottom: 16,
145
+ fontSize: 14,
146
+ fontWeight: 500,
147
+ background: message.type === 'success' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
148
+ border: message.type === 'success' ? '1px solid rgba(16, 185, 129, 0.3)' : '1px solid rgba(239, 68, 68, 0.3)',
149
+ color: message.type === 'success' ? '#34d399' : '#f87171',
150
+ display: 'flex',
151
+ justifyContent: 'space-between',
152
+ alignItems: 'center',
153
+ backdropFilter: 'blur(8px)',
154
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
155
+ }}>
156
+ <span>{message.text}</span>
157
+ <button
158
+ onClick={() => setMessage(null)}
159
+ style={{
160
+ background: 'none',
161
+ border: 'none',
162
+ color: 'inherit',
163
+ cursor: 'pointer',
164
+ fontSize: 18,
165
+ padding: '0 4px',
166
+ lineHeight: '1',
167
+ }}
168
+ >
169
+ ×
170
+ </button>
171
+ </div>
172
+ )}
173
+
174
+ {/* Explanatory Banner */}
175
+ <div style={{
176
+ background: 'rgba(30, 41, 59, 0.4)',
177
+ backdropFilter: 'blur(8px)',
178
+ border: '1px solid var(--border)',
179
+ borderRadius: 12,
180
+ padding: 20,
181
+ marginBottom: 22,
182
+ color: 'var(--text-secondary)',
183
+ fontSize: 14,
184
+ lineHeight: '1.5',
185
+ }}>
186
+ <strong style={{ color: 'var(--text-primary)', display: 'block', marginBottom: 6 }}>💡 How Integrations Work</strong>
187
+ Total Recall acts as a secure, local-first semantic memory hub. To give your development environments (Cursor, Claude Code, etc.) access to your memory vault and instruction layers, they communicate as API clients using secure tokens.
188
+ <br />
189
+ <span style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>
190
+ The status badges below dynamically inspect whether your workspace has successfully registered its connection token and communicated with your active brain.
191
+ </span>
192
+ </div>
193
+
194
+ <div style={{
195
+ border: '1px solid var(--border)',
196
+ background: 'var(--bg-secondary)',
197
+ borderRadius: 12,
198
+ padding: 18,
199
+ marginBottom: 22,
200
+ display: 'grid',
201
+ gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
202
+ gap: 12,
203
+ fontSize: 13,
204
+ }}>
205
+ <div>
206
+ <div style={{ color: 'var(--text-tertiary)', marginBottom: 4 }}>Discovery</div>
207
+ <code>{baseUrl}/.well-known/total-recall.json</code>
208
+ </div>
209
+ <div>
210
+ <div style={{ color: 'var(--text-tertiary)', marginBottom: 4 }}>Chat API</div>
211
+ <code>{baseUrl}/v1/chat/completions</code>
212
+ </div>
213
+ <div>
214
+ <div style={{ color: 'var(--text-tertiary)', marginBottom: 4 }}>Model</div>
215
+ <code>total-recall/gemma4</code>
216
+ </div>
217
+ </div>
218
+
219
+ <div style={{
220
+ display: 'grid',
221
+ gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
222
+ gap: 16,
223
+ }}>
224
+ {filteredPresets.map(preset => {
225
+ const snippet = preset.snippet(baseUrl)
226
+
227
+ // Match keys by name (e.g. "Cursor Link", "Claude Code Link", "Codex Link")
228
+ const matchingKey = keys.find(k => {
229
+ if (preset.id === 'cursor') return k.name.toLowerCase().includes('cursor') && !k.revoked;
230
+ if (preset.id === 'claude-code') return k.name.toLowerCase().includes('claude') && !k.revoked;
231
+ if (preset.id === 'codex') return (k.name.toLowerCase().includes('codex') || k.name.toLowerCase().includes('agents')) && !k.revoked;
232
+ if (preset.id === 'antigravity') return k.name.toLowerCase().includes('antigravity') && !k.revoked;
233
+ if (preset.id === 'ultrachat') return k.name.toLowerCase().includes('ultrachat') && !k.revoked;
234
+ return false;
235
+ })
236
+
237
+ let statusBadge = (
238
+ <span style={{
239
+ display: 'inline-flex',
240
+ alignItems: 'center',
241
+ gap: 6,
242
+ padding: '4px 8px',
243
+ borderRadius: 6,
244
+ background: 'rgba(255, 255, 255, 0.05)',
245
+ border: '1px solid rgba(255, 255, 255, 0.1)',
246
+ fontSize: 12,
247
+ color: 'var(--text-tertiary)'
248
+ }}>
249
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#94a3b8' }} />
250
+ Not Connected
251
+ </span>
252
+ )
253
+
254
+ if (matchingKey) {
255
+ if (matchingKey.hit_count > 0) {
256
+ const lastUsedStr = matchingKey.last_used_at
257
+ ? ` (Active ${new Date(matchingKey.last_used_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`
258
+ : ''
259
+ statusBadge = (
260
+ <span style={{
261
+ display: 'inline-flex',
262
+ alignItems: 'center',
263
+ gap: 6,
264
+ padding: '4px 8px',
265
+ borderRadius: 6,
266
+ background: 'rgba(16, 185, 129, 0.1)',
267
+ border: '1px solid rgba(16, 185, 129, 0.3)',
268
+ fontSize: 12,
269
+ color: '#34d399',
270
+ fontWeight: 500,
271
+ }}>
272
+ <span style={{
273
+ width: 8,
274
+ height: 8,
275
+ borderRadius: '50%',
276
+ background: '#10b981',
277
+ boxShadow: '0 0 8px #10b981',
278
+ }} />
279
+ Connected{lastUsedStr}
280
+ </span>
281
+ )
282
+ } else {
283
+ statusBadge = (
284
+ <span style={{
285
+ display: 'inline-flex',
286
+ alignItems: 'center',
287
+ gap: 6,
288
+ padding: '4px 8px',
289
+ borderRadius: 6,
290
+ background: 'rgba(245, 158, 11, 0.1)',
291
+ border: '1px solid rgba(245, 158, 11, 0.3)',
292
+ fontSize: 12,
293
+ color: '#fbbf24',
294
+ fontWeight: 500,
295
+ }}>
296
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f59e0b' }} />
297
+ Registered (Pending Activity)
298
+ </span>
299
+ )
300
+ }
301
+ }
302
+
303
+ return (
304
+ <div key={preset.id} style={{
305
+ border: '1px solid var(--border)',
306
+ background: 'var(--bg-secondary)',
307
+ borderRadius: 12,
308
+ padding: 18,
309
+ display: 'flex',
310
+ flexDirection: 'column',
311
+ gap: 12,
312
+ position: 'relative',
313
+ }}>
314
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
315
+ <div>
316
+ <h3 style={{ margin: '0 0 4px' }}>{preset.name}</h3>
317
+ <div style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>{preset.surface}</div>
318
+ </div>
319
+ {statusBadge}
320
+ </div>
321
+
322
+ <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
323
+ Recommended scopes: <code>{preset.scopes}</code>
324
+ </div>
325
+
326
+ <div style={{
327
+ background: 'var(--bg-primary)',
328
+ border: '1px solid var(--border)',
329
+ borderRadius: 8,
330
+ padding: 10,
331
+ fontFamily: 'monospace',
332
+ fontSize: 12,
333
+ whiteSpace: 'pre-wrap',
334
+ color: 'var(--text-secondary)',
335
+ }}>{preset.command}</div>
336
+
337
+ <div style={{
338
+ background: 'var(--bg-primary)',
339
+ border: '1px solid var(--border)',
340
+ borderRadius: 8,
341
+ padding: 10,
342
+ fontFamily: 'monospace',
343
+ fontSize: 12,
344
+ whiteSpace: 'pre-wrap',
345
+ color: 'var(--text-secondary)',
346
+ }}>{snippet}</div>
347
+
348
+ <div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
349
+ <button
350
+ className="btn btn-sm btn-primary"
351
+ onClick={() => handleConnect(preset.id)}
352
+ disabled={connecting !== null}
353
+ style={{
354
+ background: 'linear-gradient(135deg, #10b981 0%, #047857 100%)',
355
+ border: 'none',
356
+ boxShadow: '0 2px 4px rgba(16, 185, 129, 0.3)',
357
+ fontWeight: 600,
358
+ cursor: 'pointer',
359
+ }}
360
+ >
361
+ {connecting === preset.id ? 'Connecting...' : 'Connect'}
362
+ </button>
363
+ <button
364
+ className="btn btn-sm"
365
+ onClick={() => copy(preset.id, `${preset.command}\n\n${snippet}`)}
366
+ style={{
367
+ background: 'rgba(255, 255, 255, 0.05)',
368
+ border: '1px solid var(--border)',
369
+ color: 'var(--text-secondary)',
370
+ cursor: 'pointer',
371
+ }}
372
+ >
373
+ {copied === preset.id ? 'Copied!' : 'Copy Config'}
374
+ </button>
375
+ </div>
376
+ </div>
377
+ )
378
+ })}
379
+ </div>
380
+ </div>
381
+ )
382
+ }