total-recall-brain 3.13.0 → 3.13.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 (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -0,0 +1,690 @@
1
+ import { useEffect, useState, type CSSProperties } from 'react'
2
+ import { Link, useNavigate } from 'react-router-dom'
3
+ import {
4
+ listMemory,
5
+ createMemory,
6
+ triggerRecompile,
7
+ scanEnvSecrets,
8
+ parseEnvPaste,
9
+ importEnvSecrets,
10
+ type EnvSecretCandidate,
11
+ } from '../api'
12
+ import BrandMark from '../components/brand/BrandMark'
13
+
14
+ const STORAGE_KEY = 'tr-onboarding-v1'
15
+
16
+ type StepId = 'welcome' | 'import-env' | 'remember' | 'connect' | 'dream' | 'done'
17
+
18
+ interface Step {
19
+ id: StepId
20
+ title: string
21
+ body: string
22
+ }
23
+
24
+ const STEPS: Step[] = [
25
+ {
26
+ id: 'welcome',
27
+ title: 'Welcome to Total Recall',
28
+ body: 'Portable personal memory for any IDE. Your rules and facts live as Markdown files on disk — not a locked vendor database.',
29
+ },
30
+ {
31
+ id: 'import-env',
32
+ title: 'Import your API keys',
33
+ body: 'Scan the machine for keys already in .env files or the shell, or paste a .env block. Nothing is written until you choose what to import.',
34
+ },
35
+ {
36
+ id: 'remember',
37
+ title: 'Save your first memory',
38
+ body: 'Write a fact or preference into the vault. This is the core write path: remember → compile → IDEs see it.',
39
+ },
40
+ {
41
+ id: 'connect',
42
+ title: 'Connect an IDE',
43
+ body: 'Wire Claude Code, Cursor, Codex, Gemini, Aider, Obsidian, or any HTTP host. Injected blocks never overwrite your local rules outside TR markers.',
44
+ },
45
+ {
46
+ id: 'dream',
47
+ title: 'Dream consolidates memory',
48
+ body: 'Dream is sleep for your vault: ingest sessions, conflict-check, recompile surfaces, prune. Run it anytime; no always-on LLM required.',
49
+ },
50
+ {
51
+ id: 'done',
52
+ title: "You're set",
53
+ body: 'Default loop: init → connect → remember/recall → dream. Optional: task queue, skill track any repo, daemon in the background.',
54
+ },
55
+ ]
56
+
57
+ function loadProgress(): { step: number; fact?: string } {
58
+ try {
59
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
60
+ } catch {
61
+ return { step: 0 }
62
+ }
63
+ }
64
+
65
+ function saveProgress(p: { step: number; fact?: string; complete?: boolean }) {
66
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(p))
67
+ if (p.complete) localStorage.setItem('tr-onboarding-complete', '1')
68
+ }
69
+
70
+ export function isOnboardingComplete(): boolean {
71
+ return localStorage.getItem('tr-onboarding-complete') === '1'
72
+ }
73
+
74
+ export function resetOnboarding() {
75
+ localStorage.removeItem(STORAGE_KEY)
76
+ localStorage.removeItem('tr-onboarding-complete')
77
+ }
78
+
79
+ /** Client-side .env parse so we can re-send pairs on import without server holding paste. */
80
+ function parseEnvClient(text: string): Record<string, string> {
81
+ const out: Record<string, string> = {}
82
+ for (const raw of text.split(/\r?\n/)) {
83
+ const line = raw.trim()
84
+ if (!line || line.startsWith('#')) continue
85
+ const body = line.startsWith('export ') ? line.slice(7).trim() : line
86
+ const eq = body.indexOf('=')
87
+ if (eq <= 0) continue
88
+ const key = body.slice(0, eq).trim()
89
+ let val = body.slice(eq + 1).trim()
90
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
91
+ val = val.slice(1, -1)
92
+ } else {
93
+ val = val.replace(/\s+#.*$/, '').trim()
94
+ }
95
+ if (key && val) out[key] = val
96
+ }
97
+ return out
98
+ }
99
+
100
+ export default function OnboardingPage() {
101
+ const navigate = useNavigate()
102
+ const initial = loadProgress()
103
+ const [stepIdx, setStepIdx] = useState(initial.step || 0)
104
+ const [fact, setFact] = useState(initial.fact || '')
105
+ const [status, setStatus] = useState('')
106
+ const [busy, setBusy] = useState(false)
107
+ const [hasProfile, setHasProfile] = useState(false)
108
+
109
+ // Env import state
110
+ const [candidates, setCandidates] = useState<EnvSecretCandidate[]>([])
111
+ const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set())
112
+ const [pasteText, setPasteText] = useState('')
113
+ const [pastePairs, setPastePairs] = useState<Record<string, string>>({})
114
+ const [scanError, setScanError] = useState('')
115
+ const [scanLoading, setScanLoading] = useState(false)
116
+ const [importMode, setImportMode] = useState<'scan' | 'paste'>('scan')
117
+
118
+ const step = STEPS[Math.min(stepIdx, STEPS.length - 1)]
119
+
120
+ useEffect(() => {
121
+ listMemory()
122
+ .then((nodes) => {
123
+ const list = nodes || []
124
+ const profile = list.some(
125
+ (n) =>
126
+ n.slug === 'user-profile' ||
127
+ n.slug === 'onboarding-first-memory' ||
128
+ (n.tags || []).includes('onboarding'),
129
+ )
130
+ const established = profile || list.length >= 5
131
+ setHasProfile(profile || established)
132
+ if (established && !isOnboardingComplete()) {
133
+ localStorage.setItem('tr-onboarding-complete', '1')
134
+ }
135
+ })
136
+ .catch(() => {})
137
+ }, [])
138
+
139
+ // Auto-scan when entering import-env step
140
+ useEffect(() => {
141
+ if (step.id !== 'import-env' || importMode !== 'scan') return
142
+ let cancelled = false
143
+ setScanLoading(true)
144
+ setScanError('')
145
+ scanEnvSecrets()
146
+ .then((data) => {
147
+ if (cancelled) return
148
+ setCandidates(data.candidates || [])
149
+ const pick = new Set(
150
+ (data.candidates || [])
151
+ .filter((c) => !c.already_set)
152
+ .map((c) => c.key),
153
+ )
154
+ setSelectedKeys(pick)
155
+ })
156
+ .catch((e) => {
157
+ if (!cancelled) setScanError(e instanceof Error ? e.message : 'Scan failed')
158
+ })
159
+ .finally(() => {
160
+ if (!cancelled) setScanLoading(false)
161
+ })
162
+ return () => {
163
+ cancelled = true
164
+ }
165
+ }, [step.id, importMode])
166
+
167
+ function next() {
168
+ const n = Math.min(stepIdx + 1, STEPS.length - 1)
169
+ setStepIdx(n)
170
+ saveProgress({ step: n, fact })
171
+ setStatus('')
172
+ }
173
+
174
+ function back() {
175
+ const n = Math.max(stepIdx - 1, 0)
176
+ setStepIdx(n)
177
+ saveProgress({ step: n, fact })
178
+ setStatus('')
179
+ }
180
+
181
+ function toggleKey(key: string) {
182
+ setSelectedKeys((prev) => {
183
+ const next = new Set(prev)
184
+ if (next.has(key)) next.delete(key)
185
+ else next.add(key)
186
+ return next
187
+ })
188
+ }
189
+
190
+ function selectAllNew() {
191
+ setSelectedKeys(
192
+ new Set(candidates.filter((c) => !c.already_set).map((c) => c.key)),
193
+ )
194
+ }
195
+
196
+ function selectNone() {
197
+ setSelectedKeys(new Set())
198
+ }
199
+
200
+ async function handlePastePreview() {
201
+ if (!pasteText.trim()) {
202
+ setStatus('Paste a .env block first.')
203
+ return
204
+ }
205
+ setBusy(true)
206
+ setScanError('')
207
+ try {
208
+ const pairs = parseEnvClient(pasteText)
209
+ setPastePairs(pairs)
210
+ const data = await parseEnvPaste(pasteText)
211
+ setCandidates(data.candidates || [])
212
+ setSelectedKeys(
213
+ new Set((data.candidates || []).filter((c) => !c.already_set).map((c) => c.key)),
214
+ )
215
+ setStatus(
216
+ data.count
217
+ ? `Found ${data.count} importable key(s) in paste.`
218
+ : 'No known API key names in paste.',
219
+ )
220
+ } catch (e) {
221
+ setScanError(e instanceof Error ? e.message : 'Parse failed')
222
+ } finally {
223
+ setBusy(false)
224
+ }
225
+ }
226
+
227
+ async function runImport() {
228
+ const keys = [...selectedKeys]
229
+ if (!keys.length) {
230
+ setStatus('Select at least one key, or skip this step.')
231
+ return
232
+ }
233
+ setBusy(true)
234
+ setStatus('')
235
+ try {
236
+ const result =
237
+ importMode === 'paste' && Object.keys(pastePairs).length
238
+ ? await importEnvSecrets({ keys, pairs: pastePairs, overwrite: false })
239
+ : await importEnvSecrets({ keys, overwrite: false })
240
+ setStatus(
241
+ `Imported ${result.imported_count} secret(s)` +
242
+ (result.skipped_count ? `, skipped ${result.skipped_count} already set` : '') +
243
+ '.',
244
+ )
245
+ // refresh scan marks
246
+ if (importMode === 'scan') {
247
+ const data = await scanEnvSecrets()
248
+ setCandidates(data.candidates || [])
249
+ setSelectedKeys(new Set())
250
+ } else {
251
+ setCandidates((prev) =>
252
+ prev.map((c) =>
253
+ result.imported.some((i) => i.key === c.key)
254
+ ? { ...c, already_set: true }
255
+ : c,
256
+ ),
257
+ )
258
+ setSelectedKeys(new Set())
259
+ }
260
+ } catch (e) {
261
+ setStatus(e instanceof Error ? e.message : 'Import failed')
262
+ } finally {
263
+ setBusy(false)
264
+ }
265
+ }
266
+
267
+ async function saveFirstMemory() {
268
+ if (!fact.trim()) {
269
+ setStatus('Write a short fact or preference first.')
270
+ return
271
+ }
272
+ setBusy(true)
273
+ setStatus('')
274
+ try {
275
+ await createMemory({
276
+ slug: 'onboarding-first-memory',
277
+ category: 'preferences',
278
+ title: 'Onboarding first memory',
279
+ content: fact.trim(),
280
+ tags: ['onboarding'],
281
+ importance: 4,
282
+ } as Parameters<typeof createMemory>[0])
283
+ try {
284
+ await triggerRecompile()
285
+ } catch {
286
+ // optional
287
+ }
288
+ setStatus('Saved and queued surface compile.')
289
+ saveProgress({ step: stepIdx, fact })
290
+ next()
291
+ } catch (e) {
292
+ setStatus(e instanceof Error ? e.message : 'Failed to save memory')
293
+ } finally {
294
+ setBusy(false)
295
+ }
296
+ }
297
+
298
+ function finish() {
299
+ saveProgress({ step: STEPS.length - 1, fact, complete: true })
300
+ navigate('/memory')
301
+ }
302
+
303
+ const pct = Math.round(((stepIdx + 1) / STEPS.length) * 100)
304
+
305
+ return (
306
+ <div className="onboarding-shell">
307
+ {step.id === 'welcome' && (
308
+ <div
309
+ style={{
310
+ marginBottom: 28,
311
+ borderRadius: 16,
312
+ padding: '16px 20px',
313
+ background: 'linear-gradient(180deg, #f8fafc 0%, #e8eef9 100%)',
314
+ boxShadow:
315
+ '0 1px 0 rgba(255,255,255,0.7) inset, 0 16px 40px rgba(15, 23, 42, 0.35), 0 0 0 1px rgba(148,163,184,0.2)',
316
+ display: 'flex',
317
+ justifyContent: 'center',
318
+ }}
319
+ >
320
+ <BrandMark variant="lockup" height={64} alt="Total Recall" />
321
+ </div>
322
+ )}
323
+
324
+ <div style={{ marginBottom: 28 }}>
325
+ <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 10, letterSpacing: '0.08em', textTransform: 'uppercase', fontWeight: 600 }}>
326
+ Onboarding · {stepIdx + 1}/{STEPS.length}
327
+ </div>
328
+ <div className="onboarding-progress">
329
+ <div style={{ width: `${pct}%` }} />
330
+ </div>
331
+ </div>
332
+
333
+ <h1
334
+ style={{
335
+ fontSize: 28,
336
+ fontWeight: 700,
337
+ margin: '0 0 12px',
338
+ letterSpacing: '-0.03em',
339
+ background: 'linear-gradient(135deg, #f8fafc 0%, #93c5fd 100%)',
340
+ WebkitBackgroundClip: 'text',
341
+ WebkitTextFillColor: 'transparent',
342
+ backgroundClip: 'text',
343
+ }}
344
+ >
345
+ {step.title}
346
+ </h1>
347
+ <p style={{ fontSize: 15.5, lineHeight: 1.6, color: 'var(--text-secondary)', margin: '0 0 28px' }}>{step.body}</p>
348
+
349
+ {step.id === 'welcome' && (
350
+ <div className="loop-card" style={{ marginBottom: 24 }}>
351
+ write&nbsp;&nbsp;→ remember / session ingest<br />
352
+ sleep&nbsp;&nbsp;→ dream (consolidate, conflict, compile)<br />
353
+ read&nbsp;&nbsp;&nbsp;→ recall + compiled surfaces<br />
354
+ async&nbsp;&nbsp;→ daemon tasks (any intent under policy)
355
+ </div>
356
+ )}
357
+
358
+ {step.id === 'import-env' && (
359
+ <div style={{ marginBottom: 24 }}>
360
+ <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
361
+ <button
362
+ type="button"
363
+ onClick={() => setImportMode('scan')}
364
+ style={importMode === 'scan' ? tabActive : tabIdle}
365
+ >
366
+ Scan machine
367
+ </button>
368
+ <button
369
+ type="button"
370
+ onClick={() => {
371
+ setImportMode('paste')
372
+ setCandidates([])
373
+ setSelectedKeys(new Set())
374
+ }}
375
+ style={importMode === 'paste' ? tabActive : tabIdle}
376
+ >
377
+ Paste .env
378
+ </button>
379
+ </div>
380
+
381
+ {importMode === 'paste' && (
382
+ <div style={{ marginBottom: 14 }}>
383
+ <textarea
384
+ value={pasteText}
385
+ onChange={(e) => setPasteText(e.target.value)}
386
+ placeholder={'OPENAI_API_KEY=sk-...\nANTHROPIC_API_KEY=sk-ant-...\nGOOGLE_API_KEY=...'}
387
+ rows={6}
388
+ style={{
389
+ width: '100%',
390
+ boxSizing: 'border-box',
391
+ background: 'var(--bg-primary)',
392
+ border: '1px solid var(--border)',
393
+ borderRadius: 10,
394
+ padding: 12,
395
+ color: 'var(--text-primary)',
396
+ fontSize: 12,
397
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
398
+ resize: 'vertical',
399
+ }}
400
+ />
401
+ <button
402
+ type="button"
403
+ disabled={busy}
404
+ onClick={handlePastePreview}
405
+ style={{ ...btnGhost, marginTop: 8 }}
406
+ >
407
+ Preview keys in paste
408
+ </button>
409
+ </div>
410
+ )}
411
+
412
+ {scanLoading && (
413
+ <p style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Scanning env sources…</p>
414
+ )}
415
+ {scanError && (
416
+ <p style={{ fontSize: 13, color: '#f87171', marginBottom: 12 }}>{scanError}</p>
417
+ )}
418
+
419
+ {!scanLoading && candidates.length === 0 && !scanError && (
420
+ <div
421
+ style={{
422
+ padding: 14,
423
+ borderRadius: 10,
424
+ border: '1px solid var(--border)',
425
+ background: 'rgba(148,163,184,0.06)',
426
+ fontSize: 13,
427
+ color: 'var(--text-secondary)',
428
+ marginBottom: 12,
429
+ }}
430
+ >
431
+ {importMode === 'scan'
432
+ ? 'No known API keys found in process.env or common .env paths (~/.agent/.env, project .env, …). Paste a .env or continue and set keys later with secret set.'
433
+ : 'Paste your .env and click Preview.'}
434
+ </div>
435
+ )}
436
+
437
+ {candidates.length > 0 && (
438
+ <>
439
+ <div style={{ display: 'flex', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
440
+ <button type="button" onClick={selectAllNew} style={btnTiny}>Select new</button>
441
+ <button type="button" onClick={selectNone} style={btnTiny}>Clear</button>
442
+ <span style={{ fontSize: 12, color: 'var(--text-tertiary)', alignSelf: 'center' }}>
443
+ {selectedKeys.size} selected · values never shown raw
444
+ </span>
445
+ </div>
446
+ <div
447
+ style={{
448
+ maxHeight: 280,
449
+ overflowY: 'auto',
450
+ border: '1px solid var(--border)',
451
+ borderRadius: 12,
452
+ marginBottom: 12,
453
+ }}
454
+ >
455
+ {candidates.map((c) => (
456
+ <label
457
+ key={c.key + c.source_label}
458
+ style={{
459
+ display: 'flex',
460
+ alignItems: 'center',
461
+ gap: 10,
462
+ padding: '10px 12px',
463
+ borderBottom: '1px solid var(--border)',
464
+ cursor: c.already_set ? 'default' : 'pointer',
465
+ opacity: c.already_set ? 0.55 : 1,
466
+ background: selectedKeys.has(c.key) ? 'rgba(59,130,246,0.08)' : 'transparent',
467
+ }}
468
+ >
469
+ <input
470
+ type="checkbox"
471
+ checked={selectedKeys.has(c.key)}
472
+ disabled={!!c.already_set}
473
+ onChange={() => toggleKey(c.key)}
474
+ style={{ accentColor: '#3b82f6' }}
475
+ />
476
+ <div style={{ flex: 1, minWidth: 0 }}>
477
+ <div style={{ fontSize: 12, fontWeight: 600, fontFamily: 'ui-monospace, monospace' }}>
478
+ {c.key}
479
+ {c.provider && (
480
+ <span style={{ marginLeft: 8, fontSize: 10, color: '#93c5fd', fontWeight: 500 }}>
481
+ {c.provider}
482
+ </span>
483
+ )}
484
+ {c.already_set && (
485
+ <span style={{ marginLeft: 8, fontSize: 10, color: '#34d399' }}>already set</span>
486
+ )}
487
+ </div>
488
+ <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}>
489
+ {c.masked} · {c.source_label}
490
+ </div>
491
+ </div>
492
+ </label>
493
+ ))}
494
+ </div>
495
+ <button
496
+ type="button"
497
+ disabled={busy || selectedKeys.size === 0}
498
+ onClick={runImport}
499
+ style={{
500
+ ...btnPrimary,
501
+ opacity: busy || selectedKeys.size === 0 ? 0.5 : 1,
502
+ cursor: busy || selectedKeys.size === 0 ? 'not-allowed' : 'pointer',
503
+ }}
504
+ >
505
+ {busy ? 'Importing…' : `Import ${selectedKeys.size} secret(s)`}
506
+ </button>
507
+ </>
508
+ )}
509
+
510
+ <p style={{ marginTop: 14, fontSize: 12, color: 'var(--text-tertiary)' }}>
511
+ CLI: <code>npx total-recall secret import-env --all</code>
512
+ {' · '}
513
+ <Link to="/keys" style={{ color: 'var(--accent)' }}>Secrets page</Link>
514
+ </p>
515
+ </div>
516
+ )}
517
+
518
+ {step.id === 'remember' && (
519
+ <div style={{ marginBottom: 24 }}>
520
+ <label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-tertiary)', display: 'block', marginBottom: 8 }}>
521
+ First memory (fact or preference)
522
+ </label>
523
+ <textarea
524
+ value={fact}
525
+ onChange={(e) => setFact(e.target.value)}
526
+ placeholder='e.g. "I prefer short answers and single quotes in JS."'
527
+ rows={4}
528
+ style={{
529
+ width: '100%',
530
+ boxSizing: 'border-box',
531
+ background: 'var(--bg-primary)',
532
+ border: '1px solid var(--border)',
533
+ borderRadius: 10,
534
+ padding: 12,
535
+ color: 'var(--text-primary)',
536
+ fontSize: 14,
537
+ resize: 'vertical',
538
+ }}
539
+ />
540
+ <button type="button" disabled={busy} onClick={saveFirstMemory} style={{ ...btnPrimary, marginTop: 12 }}>
541
+ {busy ? 'Saving…' : 'Save to vault & continue'}
542
+ </button>
543
+ </div>
544
+ )}
545
+
546
+ {step.id === 'connect' && (
547
+ <div style={{ marginBottom: 24, display: 'flex', flexDirection: 'column', gap: 10 }}>
548
+ <code style={codeBlock}>npx total-recall connect claude-code</code>
549
+ <code style={codeBlock}>npx total-recall connect cursor</code>
550
+ <code style={codeBlock}>npx total-recall connect http-api --brain http://localhost:3000</code>
551
+ <Link to="/integrations" style={{ fontSize: 13, color: 'var(--accent)' }}>Open Integrations page →</Link>
552
+ </div>
553
+ )}
554
+
555
+ {step.id === 'dream' && (
556
+ <div style={{ marginBottom: 24, display: 'flex', flexDirection: 'column', gap: 10 }}>
557
+ <code style={codeBlock}>npx total-recall dream</code>
558
+ <code style={codeBlock}>npx total-recall daemon start # optional background worker</code>
559
+ <code style={codeBlock}>npx total-recall task add "Extract decisions from last session" --cap vault:write</code>
560
+ <Link to="/tasks" style={{ fontSize: 13, color: 'var(--accent)' }}>Open Tasks page →</Link>
561
+ </div>
562
+ )}
563
+
564
+ {step.id === 'done' && (
565
+ <div
566
+ style={{
567
+ marginBottom: 24,
568
+ padding: 16,
569
+ borderRadius: 12,
570
+ background: 'rgba(34,197,94,0.08)',
571
+ border: '1px solid rgba(34,197,94,0.25)',
572
+ fontSize: 14,
573
+ color: 'var(--text-secondary)',
574
+ }}
575
+ >
576
+ {hasProfile
577
+ ? 'Vault already has onboarding profile data — you can jump to Memory anytime.'
578
+ : 'Optional: chat with the assistant to run the full preference interview (user-profile.md).'}
579
+ <div style={{ marginTop: 12, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
580
+ <Link to="/" style={btnSecondary}>Open Chat</Link>
581
+ <Link to="/memory" style={btnSecondary}>Open Vault</Link>
582
+ <Link to="/integrations" style={btnSecondary}>Connect IDE</Link>
583
+ </div>
584
+ </div>
585
+ )}
586
+
587
+ {status && (
588
+ <p
589
+ style={{
590
+ fontSize: 13,
591
+ color: status.startsWith('Saved') || status.startsWith('Imported') || status.startsWith('Found')
592
+ ? '#22c55e'
593
+ : '#f59e0b',
594
+ marginBottom: 16,
595
+ }}
596
+ >
597
+ {status}
598
+ </p>
599
+ )}
600
+
601
+ <div style={{ display: 'flex', gap: 10, justifyContent: 'space-between' }}>
602
+ <button type="button" onClick={back} disabled={stepIdx === 0} style={btnGhost}>Back</button>
603
+ <div style={{ display: 'flex', gap: 10 }}>
604
+ {step.id !== 'done' && step.id !== 'remember' && (
605
+ <button type="button" onClick={next} style={btnPrimary}>
606
+ {step.id === 'import-env' ? 'Continue' : 'Continue'}
607
+ </button>
608
+ )}
609
+ {step.id === 'import-env' && (
610
+ <button type="button" onClick={next} style={btnGhost}>Skip for now</button>
611
+ )}
612
+ {step.id === 'remember' && (
613
+ <button type="button" onClick={next} style={btnGhost}>Skip for now</button>
614
+ )}
615
+ {step.id === 'done' && (
616
+ <button type="button" onClick={finish} style={btnPrimary}>Go to Memory</button>
617
+ )}
618
+ </div>
619
+ </div>
620
+
621
+ <p style={{ marginTop: 32, fontSize: 12, color: 'var(--text-tertiary)' }}>
622
+ CLI-first: <code>init → secret import-env --all → connect → remember → dream</code>
623
+ </p>
624
+
625
+ {stepIdx < STEPS.length - 1 && (
626
+ <button type="button" onClick={finish} style={{ ...btnGhost, marginTop: 16, width: '100%' }}>
627
+ Skip onboarding → Memory
628
+ </button>
629
+ )}
630
+ </div>
631
+ )
632
+ }
633
+
634
+ const codeBlock: CSSProperties = {
635
+ display: 'block',
636
+ background: 'var(--bg-primary)',
637
+ border: '1px solid var(--border)',
638
+ borderRadius: 8,
639
+ padding: '10px 12px',
640
+ fontSize: 12,
641
+ color: 'var(--text-secondary)',
642
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
643
+ overflowX: 'auto',
644
+ }
645
+
646
+ const btnPrimary: CSSProperties = {
647
+ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 55%, #1d4ed8 100%)',
648
+ color: '#fff',
649
+ border: 'none',
650
+ borderRadius: 12,
651
+ padding: '11px 20px',
652
+ fontWeight: 600,
653
+ cursor: 'pointer',
654
+ fontSize: 14,
655
+ boxShadow: '0 1px 0 rgba(255,255,255,0.15) inset, 0 8px 20px rgba(37, 99, 235, 0.28)',
656
+ }
657
+
658
+ const btnGhost: CSSProperties = {
659
+ background: 'rgba(148, 163, 184, 0.06)',
660
+ color: 'var(--text-secondary)',
661
+ border: '1px solid var(--border)',
662
+ borderRadius: 12,
663
+ padding: '11px 20px',
664
+ cursor: 'pointer',
665
+ fontSize: 14,
666
+ }
667
+
668
+ const btnSecondary: CSSProperties = {
669
+ ...btnGhost,
670
+ textDecoration: 'none',
671
+ display: 'inline-block',
672
+ }
673
+
674
+ const btnTiny: CSSProperties = {
675
+ ...btnGhost,
676
+ padding: '5px 10px',
677
+ fontSize: 11,
678
+ }
679
+
680
+ const tabActive: CSSProperties = {
681
+ ...btnPrimary,
682
+ padding: '8px 14px',
683
+ fontSize: 12,
684
+ }
685
+
686
+ const tabIdle: CSSProperties = {
687
+ ...btnGhost,
688
+ padding: '8px 14px',
689
+ fontSize: 12,
690
+ }