total-recall-brain 3.14.4 → 3.14.7

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 (142) hide show
  1. package/frontend/dist/assets/index-D9-jAOr9.js +502 -0
  2. package/frontend/dist/assets/index-L0eEIoBf.css +1 -0
  3. package/frontend/dist/brand/total-recall-badge.jpg +0 -0
  4. package/frontend/dist/brand/total-recall-icon.svg +6 -0
  5. package/frontend/dist/brand/total-recall-lockup.png +0 -0
  6. package/frontend/dist/brand/total-recall-mark.svg +9 -0
  7. package/frontend/dist/favicon.svg +6 -0
  8. package/frontend/dist/icons.svg +24 -0
  9. package/frontend/dist/index.html +27 -0
  10. package/frontend/dist/manifest.json +25 -0
  11. package/frontend/dist/sw.js +272 -0
  12. package/frontend/index.html +7 -2
  13. package/frontend/package.json +2 -0
  14. package/frontend/src/App.spec.tsx +82 -0
  15. package/frontend/src/api/_base.ts +35 -0
  16. package/frontend/src/api/api.spec.ts +297 -0
  17. package/frontend/src/api/auth.ts +73 -0
  18. package/frontend/src/api/chat.ts +93 -0
  19. package/frontend/src/api/config.ts +35 -0
  20. package/frontend/src/api/docs.ts +146 -0
  21. package/frontend/src/api/extension.ts +9 -0
  22. package/frontend/src/api/index.ts +20 -0
  23. package/frontend/src/api/integrations.ts +22 -0
  24. package/frontend/src/api/keys.ts +384 -0
  25. package/frontend/src/api/memory.ts +234 -0
  26. package/frontend/src/api/models.ts +36 -0
  27. package/frontend/src/api/research.ts +43 -0
  28. package/frontend/src/api/sandbox.ts +19 -0
  29. package/frontend/src/api/sessions.ts +27 -0
  30. package/frontend/src/api/skills.ts +127 -0
  31. package/frontend/src/api/system.ts +165 -0
  32. package/frontend/src/api/update.ts +21 -0
  33. package/frontend/src/components/BrainSelector.spec.tsx +72 -0
  34. package/frontend/src/components/DaemonLogsTab.spec.tsx +21 -0
  35. package/frontend/src/components/DocumentEditorModal.spec.tsx +80 -0
  36. package/frontend/src/components/DocumentTable.spec.tsx +45 -0
  37. package/frontend/src/components/Graph3D.spec.tsx +75 -0
  38. package/frontend/src/components/MarkdownUtils.spec.tsx +82 -0
  39. package/frontend/src/components/MobilePairing.spec.tsx +24 -0
  40. package/frontend/src/components/MobilePairing.tsx +37 -0
  41. package/frontend/src/components/ResearchAgendaTab.spec.tsx +127 -0
  42. package/frontend/src/components/TaskQueueTab.spec.tsx +70 -0
  43. package/frontend/src/components/TaskQueueTab.tsx +9 -1
  44. package/frontend/src/components/UsageChart.spec.tsx +89 -0
  45. package/frontend/src/components/VoiceInput.spec.tsx +38 -0
  46. package/frontend/src/components/VoiceInput.tsx +68 -0
  47. package/frontend/src/index.css +30 -0
  48. package/frontend/src/main.tsx +13 -0
  49. package/frontend/src/pages/ApiKeysPage.spec.tsx +32 -0
  50. package/frontend/src/pages/AutomationsPage.spec.tsx +60 -0
  51. package/frontend/src/pages/ChatPage.spec.tsx +37 -0
  52. package/frontend/src/pages/ChatPage.tsx +14 -0
  53. package/frontend/src/pages/CollabPage.spec.tsx +64 -0
  54. package/frontend/src/pages/DesignDocsPage.spec.tsx +43 -0
  55. package/frontend/src/pages/FilesPage.spec.tsx +46 -0
  56. package/frontend/src/pages/GraphPage.spec.tsx +24 -0
  57. package/frontend/src/pages/HealthPage.spec.tsx +31 -0
  58. package/frontend/src/pages/HelpPage.spec.tsx +63 -0
  59. package/frontend/src/pages/InboxPage.spec.tsx +55 -0
  60. package/frontend/src/pages/InstructionsPage.spec.tsx +52 -0
  61. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  62. package/frontend/src/pages/IntegrationsPage.spec.tsx +55 -0
  63. package/frontend/src/pages/LoginPage.spec.tsx +73 -0
  64. package/frontend/src/pages/LoginPage.tsx +20 -0
  65. package/frontend/src/pages/MemoryPage.spec.tsx +51 -0
  66. package/frontend/src/pages/ModelsPage.spec.tsx +64 -0
  67. package/frontend/src/pages/ModelsPage.tsx +18 -11
  68. package/frontend/src/pages/OkfPage.spec.tsx +46 -0
  69. package/frontend/src/pages/OnboardingPage.spec.tsx +58 -0
  70. package/frontend/src/pages/OpenWikiPage.spec.tsx +32 -0
  71. package/frontend/src/pages/SandboxPage.spec.tsx +33 -0
  72. package/frontend/src/pages/SettingsPage.spec.tsx +33 -0
  73. package/frontend/src/pages/SettingsPage.tsx +6 -1
  74. package/frontend/src/pages/SkillsPage.spec.tsx +20 -0
  75. package/frontend/src/pages/TasksPage.spec.tsx +17 -0
  76. package/frontend/src/pages/UsagePage.spec.tsx +35 -0
  77. package/frontend/src/pages/VaultPage.spec.tsx +26 -0
  78. package/frontend/src/setupTests.ts +23 -0
  79. package/frontend/vite.config.ts +6 -0
  80. package/package.json +16 -4
  81. package/scaffold/.agent/skills/total-recall/SKILL.md +6 -2
  82. package/scaffold/.agent/skills/total-recall/skills-registry/index.yaml +2575 -0
  83. package/src/cli/connect.mjs +7 -1
  84. package/src/cli/deploy.mjs +8 -8
  85. package/src/cli/init.mjs +14 -4
  86. package/src/cli/recall.mjs +3 -1
  87. package/src/cli/remember.mjs +2 -1
  88. package/src/cli/restore.mjs +2 -2
  89. package/src/cli/upgrade.mjs +2 -2
  90. package/src/core/clarity-rewriter.mjs +2 -2
  91. package/src/core/crons.mjs +23 -55
  92. package/src/core/crypto.mjs +4 -8
  93. package/src/core/daemon-control.mjs +5 -0
  94. package/src/core/embeddings.mjs +207 -374
  95. package/src/core/fact-seeker.mjs +114 -0
  96. package/src/core/github-sync.mjs +319 -0
  97. package/src/core/obsidian-sync.mjs +129 -0
  98. package/src/core/project-brain.mjs +2 -1
  99. package/src/core/remote-vault-sync.mjs +6 -6
  100. package/src/core/runtime.mjs +1 -1
  101. package/src/core/skills-registry.mjs +11 -0
  102. package/src/core/snapshot.mjs +1 -1
  103. package/src/core/vault-watcher.mjs +6 -3
  104. package/src/server/api.mjs +1 -1
  105. package/src/server/rest.mjs +50 -2163
  106. package/src/server/route-manifest.json +57 -5
  107. package/src/server/routes/brains.mjs +212 -0
  108. package/src/server/routes/capture.mjs +33 -0
  109. package/src/server/routes/collab.mjs +1 -1
  110. package/src/server/routes/config.mjs +206 -0
  111. package/src/server/routes/context.mjs +68 -0
  112. package/src/server/routes/dashboard.mjs +53 -0
  113. package/src/server/routes/dream.mjs +30 -0
  114. package/src/server/routes/export.mjs +40 -0
  115. package/src/server/routes/extension.mjs +83 -0
  116. package/src/server/routes/field.mjs +48 -0
  117. package/src/server/routes/files.mjs +34 -0
  118. package/src/server/routes/graph.mjs +323 -0
  119. package/src/server/routes/help.mjs +184 -0
  120. package/src/server/routes/import.mjs +39 -0
  121. package/src/server/routes/instructions.mjs +29 -0
  122. package/src/server/routes/integrations.mjs +123 -0
  123. package/src/server/routes/memory.mjs +19 -1
  124. package/src/server/routes/models.mjs +388 -0
  125. package/src/server/routes/scripts.mjs +110 -0
  126. package/src/server/routes/sessions.mjs +1 -1
  127. package/src/server/routes/ssss.mjs +173 -0
  128. package/src/server/routes/system.mjs +167 -0
  129. package/src/server/routes/tasks.mjs +96 -0
  130. package/src/server/routes/tts.mjs +39 -0
  131. package/src/server/routes/update.mjs +42 -0
  132. package/src/server/routes/vault.mjs +99 -0
  133. package/frontend/src/api.ts +0 -1441
  134. package/scaffold/.agent/skills/ssss/SKILL.md +0 -235
  135. package/scaffold/.agent/skills/ssss/evals/evals.json +0 -37
  136. package/scaffold/.agent/skills/ssss/references/contract-reference.md +0 -95
  137. package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +0 -215
  138. package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +0 -22
  139. package/src/core/pattern_detector.mjs +0 -113
  140. package/src/core/promotion-pipeline.mjs +0 -54
  141. package/src/core/semantic-index.mjs +0 -276
  142. package/src/core/task_runner.mjs +0 -131
@@ -0,0 +1 @@
1
+ :root{--bg-primary:#070b14;--bg-secondary:#0c1220;--bg-tertiary:#121a2b;--bg-elevated:#172033;--bg-hover:#1c2740;--bg-active:#243352;--accent:#3b82f6;--accent-deep:#1e3a8a;--accent-glow:#3b82f647;--accent-hover:#60a5fa;--accent-muted:#3b82f624;--accent-soft:#93c5fd1f;--purple-primary:var(--accent);--purple-border:var(--border-accent);--success:#34d399;--success-muted:#34d3991f;--warning:#fbbf24;--warning-muted:#fbbf241f;--error:#f87171;--error-muted:#f871711f;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--text-tertiary:#64748b;--text-inverse:#070b14;--border:#94a3b81a;--border-hover:#94a3b838;--border-accent:#3b82f659;--glass:#0c1220b8;--glass-border:#94a3b81f;--mesh-1:#1e3a8a59;--mesh-2:#3b82f61f;--mesh-3:#0f172ae6;--font-sans:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", "SF Mono", ui-monospace, monospace;--sidebar-width:252px;--radius-sm:8px;--radius-md:12px;--radius-lg:18px;--radius-xl:24px;--shadow-sm:0 1px 2px #00000059;--shadow-md:0 8px 24px #00000059;--shadow-lg:0 24px 64px #00000073;--shadow-glow:0 0 40px #3b82f62e;--transition-fast:.14s cubic-bezier(.23, 1, .32, 1);--transition-normal:.22s cubic-bezier(.23, 1, .32, 1)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{background:var(--bg-primary);height:100%;color:var(--text-primary);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;font-size:14px;line-height:1.6}body{background:radial-gradient(1200px 600px at 10% -10%, var(--mesh-1), transparent 55%), radial-gradient(900px 500px at 100% 0%, var(--mesh-2), transparent 50%), radial-gradient(800px 400px at 50% 100%, #1e3a8a33, transparent 60%), var(--bg-primary)}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover)}button{font-family:var(--font-sans);cursor:pointer;border:none;outline:none}code,pre{font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-tertiary)}.app-layout{height:100vh;display:flex;position:relative;overflow:hidden}.sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);background:var(--glass);-webkit-backdrop-filter:blur(20px)saturate(1.4);border-right:1px solid var(--glass-border);transition:transform var(--transition-normal);z-index:100;flex-direction:column;padding:14px 0 12px;display:flex;box-shadow:4px 0 40px #00000040}.sidebar-brand{border-bottom:1px solid var(--border);flex-direction:column;gap:10px;margin-bottom:10px;padding:4px 16px 18px;display:flex}.sidebar-brand-row{align-items:center;gap:12px;display:flex}.sidebar-brand h1{color:var(--text-primary);letter-spacing:-.03em;font-size:15px;font-weight:700;line-height:1.2}.sidebar-brand span{color:var(--text-tertiary);letter-spacing:.02em;font-size:11px;font-weight:500}.sidebar-brand-lockup{background:linear-gradient(#f8fafc 0%,#e8eef9 100%);border-radius:12px;width:100%;padding:10px 12px;overflow:hidden;box-shadow:inset 0 1px #ffffffb3,0 8px 20px #0f172a59,0 0 0 1px #94a3b833}.sidebar-brand-lockup img{object-fit:contain;width:100%;height:auto;display:block}.sidebar-nav{scrollbar-width:thin;flex-direction:column;flex:1;gap:2px;padding:0 10px;display:flex;overflow-y:auto}.nav-section-label{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);opacity:.85;padding:12px 12px 6px;font-size:10px;font-weight:600}.nav-link{color:var(--text-secondary);transition:all var(--transition-fast);border:1px solid #0000;border-radius:10px;align-items:center;gap:10px;padding:9px 12px;font-size:13px;font-weight:500;text-decoration:none;display:flex;position:relative}.nav-link:hover{color:var(--text-primary);background:#94a3b814}.nav-link.active{color:#dbeafe;border-color:var(--border-accent);background:linear-gradient(135deg,#3b82f62e,#1e3a8a38);box-shadow:0 0 0 1px #3b82f614,inset 0 1px #ffffff0a}.nav-link.active:before{content:"";background:linear-gradient(#60a5fa,#2563eb);border-radius:0 3px 3px 0;width:3px;position:absolute;top:20%;bottom:20%;left:0;box-shadow:0 0 12px #3b82f699}.nav-link svg{opacity:.72;flex-shrink:0;width:18px;height:18px}.nav-link.active svg{opacity:1;color:#93c5fd}.nav-link:hover svg{opacity:.95}.sidebar-footer{border-top:1px solid var(--border);background:linear-gradient(#0000,#0f172a66);margin-top:8px;padding:12px 14px}.main-content{background:0 0;flex-direction:column;flex:1;min-width:0;display:flex;overflow:hidden}.page{flex:1;padding:28px 32px 40px;overflow-y:auto}.page-header{margin-bottom:28px}.page-header h1{letter-spacing:-.03em;background:linear-gradient(135deg,#f8fafc 0%,#93c5fd 100%);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;font-size:24px;font-weight:700}.page-header p{color:var(--text-secondary);max-width:56ch;margin-top:6px;font-size:13.5px}.card{border:1px solid var(--border);border-radius:var(--radius-md);transition:border-color var(--transition-normal), box-shadow var(--transition-normal), transform var(--transition-normal);box-shadow:var(--shadow-sm);background:linear-gradient(165deg,#172033e6,#0c1220f2);padding:20px}.card:hover{border-color:var(--border-hover);box-shadow:var(--shadow-md)}.card-grid{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;display:grid}.btn{transition:all var(--transition-fast);letter-spacing:-.01em;border-radius:10px;align-items:center;gap:6px;padding:9px 16px;font-size:13px;font-weight:600;display:inline-flex}.btn-primary{color:#fff;background:linear-gradient(135deg,#3b82f6 0%,#2563eb 55%,#1d4ed8 100%);box-shadow:inset 0 1px #ffffff26,0 8px 20px #2563eb47}.btn-primary:hover{box-shadow:0 0 28px var(--accent-glow), 0 8px 24px #2563eb59;background:linear-gradient(135deg,#60a5fa 0%,#3b82f6 50%,#2563eb 100%);transform:translateY(-1px)}.btn-ghost{color:var(--text-secondary);border:1px solid var(--border);background:#94a3b80f}.btn-ghost:hover{color:var(--text-primary);border-color:var(--border-hover);background:#94a3b81f}.btn-sm{padding:6px 11px;font-size:12px}.surface-premium{border:1px solid var(--glass-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-md), inset 0 1px 0 #ffffff0a;background:linear-gradient(165deg,#172033eb,#0c1220f5)}.login-shell{justify-content:center;align-items:center;min-height:100vh;padding:24px;display:flex;position:relative;overflow:hidden}.login-shell:before{content:"";pointer-events:none;background:radial-gradient(50% 40% at 20% 20%,#2563eb47,#0000 55%),radial-gradient(45% 35% at 80% 70%,#1e3a8a59,#0000 50%),radial-gradient(30% 25%,#60a5fa14,#0000 60%);animation:18s ease-in-out infinite alternate meshDrift;position:absolute;inset:-20%}@keyframes meshDrift{0%{transform:translate(0,0)scale(1)}to{transform:translate(-2%,2%)scale(1.05)}}.login-card{z-index:1;border-radius:var(--radius-xl);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);background:linear-gradient(165deg,#121a2beb,#080c16f5);border:1px solid #94a3b824;flex-direction:column;gap:28px;width:min(400px,100%);padding:40px 36px;display:flex;position:relative;box-shadow:0 0 0 1px #3b82f614,0 32px 80px #0000008c,0 0 80px #2563eb1f}.onboarding-shell{max-width:680px;margin:0 auto;padding:36px 22px 72px}.onboarding-progress{background:#94a3b81a;border-radius:99px;height:6px;overflow:hidden;box-shadow:inset 0 1px 2px #0000004d}.onboarding-progress>div{background:linear-gradient(90deg,#1e40af,#3b82f6,#93c5fd);border-radius:99px;height:100%;transition:width .3s cubic-bezier(.23,1,.32,1);box-shadow:0 0 16px #3b82f680}.loop-card{border:1px solid var(--border-accent);border-radius:var(--radius-md);font-family:var(--font-mono);color:#cbd5e1;background:linear-gradient(145deg,#1e3a8a40,#0c1220cc);padding:18px 20px;font-size:12.5px;line-height:1.85;box-shadow:inset 0 1px #ffffff0a}.badge{text-transform:uppercase;letter-spacing:.04em;border-radius:99px;align-items:center;gap:4px;padding:3px 8px;font-size:11px;font-weight:600;display:inline-flex}.badge-success{background:var(--success-muted);color:var(--success)}.badge-warning{background:var(--warning-muted);color:var(--warning)}.badge-error{background:var(--error-muted);color:var(--error)}.badge-accent{background:var(--accent-muted);color:var(--accent)}.chat-container{flex-direction:column;flex:1;height:calc(100vh - 1px);display:flex;overflow:hidden}.chat-messages{flex-direction:column;flex:1;gap:16px;padding:24px 32px;display:flex;overflow-y:auto}.chat-empty{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;display:flex}.chat-empty svg{opacity:.3;width:48px;height:48px}.chat-empty h2{color:var(--text-secondary);font-size:18px;font-weight:600}.chat-empty p{font-size:13px}.message{border-radius:var(--radius-md);white-space:pre-wrap;word-break:break-word;max-width:720px;padding:14px 18px;font-size:14px;line-height:1.7;animation:.2s msgIn}@keyframes msgIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.message-user{color:#fff;background:linear-gradient(135deg,#3b82f6 0%,#2563eb 100%);border-bottom-right-radius:4px;align-self:flex-end;box-shadow:0 8px 24px #2563eb38}.message-assistant{border:1px solid var(--border);box-shadow:var(--shadow-sm);background:linear-gradient(165deg,#172033f2,#0c1220fa);border-bottom-left-radius:4px;align-self:flex-start}.message-assistant code{background:var(--bg-primary);border-radius:4px;padding:1px 5px;font-size:12.5px}.chat-input-bar{border-top:1px solid var(--border);background:var(--bg-secondary);padding:16px 32px 20px}.chat-input-wrapper{align-items:flex-end;gap:8px;max-width:800px;margin:0 auto;display:flex}.chat-input-wrapper textarea{resize:none;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);font-family:var(--font-sans);min-height:44px;max-height:160px;transition:border-color var(--transition-fast);flex:1;padding:12px 16px;font-size:14px;line-height:1.5}.chat-input-wrapper textarea:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.chat-input-wrapper textarea::placeholder{color:var(--text-tertiary)}.chat-send-btn{border-radius:var(--radius-md);color:#fff;width:44px;height:44px;transition:all var(--transition-fast);background:linear-gradient(135deg,#3b82f6,#1d4ed8);flex-shrink:0;justify-content:center;align-items:center;display:flex;box-shadow:0 4px 14px #2563eb59}.chat-send-btn:hover{box-shadow:0 0 24px var(--accent-glow);background:linear-gradient(135deg,#60a5fa,#2563eb);transform:translateY(-1px)}.chat-send-btn:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}.chat-send-btn svg{width:18px;height:18px}.memory-layout{grid-template-columns:220px 1fr;gap:20px;height:calc(100vh - 120px);display:grid}.memory-sidebar{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;overflow-y:auto}.memory-sidebar h3{text-transform:uppercase;letter-spacing:.06em;color:var(--text-tertiary);padding:8px 8px 6px;font-size:11px}.category-btn{border-radius:var(--radius-sm);width:100%;color:var(--text-secondary);text-align:left;transition:all var(--transition-fast);background:0 0;justify-content:space-between;align-items:center;padding:8px 10px;font-size:13px;display:flex}.category-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.category-btn.active{background:var(--accent-muted);color:var(--accent)}.category-count{background:var(--bg-tertiary);color:var(--text-tertiary);border-radius:99px;padding:1px 6px;font-size:11px}.memory-main{flex-direction:column;gap:12px;display:flex;overflow:hidden}.memory-search{gap:8px;display:flex}.memory-search input{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);font-size:13px;font-family:var(--font-sans);transition:border-color var(--transition-fast);flex:1;padding:10px 14px}.memory-search input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.memory-search input::placeholder{color:var(--text-tertiary)}.memory-list{flex-direction:column;flex:1;gap:8px;display:flex;overflow-y:auto}.memory-node-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;transition:all var(--transition-fast);padding:14px 16px}.memory-node-card:hover{border-color:var(--accent);background:var(--bg-tertiary)}.memory-node-card.selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent-glow)}.memory-node-card h4{margin-bottom:4px;font-size:14px;font-weight:600}.memory-node-card .meta{color:var(--text-tertiary);gap:12px;font-size:12px;display:flex}.memory-detail{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:24px;overflow-y:auto}.memory-detail h2{margin-bottom:12px;font-size:18px}.memory-detail pre{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:pre-wrap;color:var(--text-secondary);padding:16px;font-size:12px;line-height:1.7;overflow-x:auto}.memory-detail .frontmatter{background:var(--bg-tertiary);border-radius:var(--radius-sm);grid-template-columns:auto 1fr;gap:4px 16px;margin-bottom:16px;padding:12px 16px;font-size:13px;display:grid}.frontmatter dt{color:var(--text-tertiary);font-weight:500}.frontmatter dd{color:var(--text-primary)}.health-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:24px;display:grid}.stat-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);text-align:center;transition:border-color var(--transition-normal);padding:20px}.stat-card:hover{border-color:var(--border-hover)}.stat-card .value{letter-spacing:-.02em;font-size:28px;font-weight:700}.stat-card .label{color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;margin-top:4px;font-size:12px}.stat-card.healthy .value{color:var(--success)}.stat-card.warning .value{color:var(--warning)}.stat-card.error .value{color:var(--error)}.health-log{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:20px}.health-log h3{margin-bottom:12px;font-size:14px;font-weight:600}.health-log table{border-collapse:collapse;width:100%;font-size:13px}.health-log th{text-align:left;color:var(--text-tertiary);border-bottom:1px solid var(--border);padding:8px 12px;font-weight:500}.health-log td{border-bottom:1px solid var(--border);color:var(--text-secondary);padding:8px 12px}.sandbox-layout{grid-template-rows:1fr auto 1fr;gap:0;height:calc(100vh - 120px);display:grid}.sandbox-editor{flex-direction:column;display:flex;overflow:hidden}.sandbox-editor-header{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-md) var(--radius-md) 0 0;color:var(--text-tertiary);justify-content:space-between;align-items:center;padding:8px 12px;font-size:12px;display:flex}.sandbox-editor textarea{resize:none;background:var(--bg-secondary);border:1px solid var(--border);border-radius:0 0 var(--radius-md) var(--radius-md);width:100%;color:var(--text-primary);font-family:var(--font-mono);tab-size:2;border-top:none;flex:1;padding:16px;font-size:13px;line-height:1.6}.sandbox-editor textarea:focus{border-color:var(--accent);outline:none}.sandbox-toolbar{justify-content:center;align-items:center;padding:8px;display:flex}.sandbox-output{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-md);font-family:var(--font-mono);color:var(--success);white-space:pre-wrap;padding:16px;font-size:13px;line-height:1.6;overflow-y:auto}.sandbox-output.error{color:var(--error)}.pulse{background:var(--success);border-radius:50%;width:8px;height:8px;animation:2s infinite pulseAnim;display:inline-block}@keyframes pulseAnim{0%,to{box-shadow:0 0 #00cec966}50%{box-shadow:0 0 0 6px #00cec900}}.skeleton{background:linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-hover) 50%, var(--bg-tertiary) 75%);border-radius:var(--radius-sm);background-size:200% 100%;animation:1.5s infinite shimmer}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (width<=768px){.sidebar{position:fixed;top:0;bottom:0;left:0;transform:translate(-100%);box-shadow:0 0 40px #00000080}.sidebar.open{transform:translate(0)}.memory-layout{grid-template-columns:1fr;height:auto}.page{padding:16px}.btn,.nav-link,.category-btn{touch-action:manipulation;min-height:44px}.chat-input-wrapper textarea,.memory-search input,.sandbox-editor textarea{font-size:16px}.chat-input-bar{padding:12px 16px 16px}}.chat-layout{width:100%;height:100%;display:flex;overflow:hidden}.chat-threads-sidebar{background:var(--bg-secondary);border-right:1px solid var(--border);flex-direction:column;flex-shrink:0;width:260px;height:100%;display:flex;overflow:hidden}.sidebar-header{border-bottom:1px solid var(--border);padding:16px}.new-chat-btn{background:var(--accent);color:#fff;border-radius:var(--radius-sm);width:100%;transition:all var(--transition-fast);cursor:pointer;border:none;justify-content:center;align-items:center;gap:8px;padding:10px 16px;font-size:13px;font-weight:500;display:flex}.new-chat-btn:hover{background:var(--accent-hover);box-shadow:0 0 16px var(--accent-glow)}.thread-list{flex-direction:column;flex:1;gap:6px;padding:16px 12px;display:flex;overflow-y:auto}.thread-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);border:1px solid #0000;justify-content:space-between;align-items:center;padding:10px 12px;display:flex;position:relative}.thread-item:hover{background:var(--bg-hover)}.thread-item.active{border-color:var(--border);background:#ffffff0d}.thread-info{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.thread-title{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}.thread-meta{color:var(--text-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}.thread-delete-btn{color:var(--text-tertiary);cursor:pointer;opacity:0;transition:all var(--transition-fast);background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;padding:4px;display:flex}.thread-item:hover .thread-delete-btn{opacity:1}.thread-delete-btn:hover{color:var(--error);background:var(--error-muted)}.chat-suggestions-container{width:100%;max-width:900px;margin-top:32px;animation:.4s ease-out fadeIn}.chat-suggestions-container h3{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);text-align:center;margin-bottom:16px;font-size:12px}.chat-suggestions-deck{grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;width:100%;display:grid}.suggestion-card{-webkit-backdrop-filter:blur(12px);border-radius:var(--radius-md);cursor:pointer;text-align:left;transition:all var(--transition-normal);background:#12121a73;border:1px solid #ffffff0d;gap:14px;padding:16px;display:flex;position:relative;overflow:hidden}.suggestion-card:before{content:"";opacity:.8;width:4px;height:100%;transition:width var(--transition-fast) ease;position:absolute;top:0;left:0}.suggestion-card.card-fact:before{background:var(--success)}.suggestion-card.card-concept:before{background:var(--accent)}.suggestion-card.card-question:before{background:var(--warning)}.suggestion-card:hover{background:#1a1a2699;border-color:#ffffff1f;transform:translateY(-2px);box-shadow:0 8px 24px #0003}.suggestion-card:hover:before{width:6px}.suggestion-card.card-fact:hover{border-color:#00cec940;box-shadow:0 8px 24px #00cec90f}.suggestion-card.card-concept:hover{border-color:#6c5ce740;box-shadow:0 8px 24px #6c5ce70f}.suggestion-card.card-question:hover{border-color:#fdcb6e40;box-shadow:0 8px 24px #fdcb6e0f}.suggestion-card-icon{-webkit-user-select:none;user-select:none;align-items:flex-start;padding-top:2px;font-size:22px;display:flex}.suggestion-card-content{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.suggestion-card-content h4{color:var(--text-primary);font-size:13.5px;font-weight:600;line-height:1.4}.suggestion-card-content p{color:var(--text-secondary);font-size:12.5px;line-height:1.5}.suggestion-card-nodes{flex-wrap:wrap;gap:6px;margin-top:8px;display:flex}.suggestion-node-pill{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);white-space:nowrap;text-overflow:ellipsis;border-radius:4px;max-width:140px;padding:2px 6px;font-size:10px;font-weight:500;overflow:hidden}.grounding-pills{border:1px solid var(--border);border-radius:var(--radius-md) var(--radius-md) 0 0;background:#12121a66;border-bottom:none;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 12px;display:flex}.grounding-pill{background:var(--accent-muted);color:var(--text-primary);transition:all var(--transition-fast);border:1px solid #6c5ce74d;border-radius:99px;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:500;display:inline-flex}.grounding-pill:hover{background:#6c5ce733;border-color:#6c5ce780}.grounding-pill svg{color:var(--accent);flex-shrink:0}.grounding-pill-title{text-overflow:ellipsis;white-space:nowrap;max-width:150px;overflow:hidden}.grounding-pill-remove{color:var(--text-tertiary);cursor:pointer;transition:color var(--transition-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0 2px;font-size:14px;line-height:1;display:inline-flex}.grounding-pill-remove:hover{color:var(--error)}.grounding-count-badge{background:var(--accent);color:#fff;border:1.5px solid var(--bg-secondary);min-width:16px;height:16px;box-shadow:0 0 8px var(--accent-glow);border-radius:50%;justify-content:center;align-items:center;font-size:9px;font-weight:700;animation:.2s cubic-bezier(.175,.885,.32,1.275) popIn;display:flex;position:absolute;top:-4px;right:-4px}.node-selector-popover{-webkit-backdrop-filter:blur(20px);border-radius:var(--radius-lg);z-index:50;background:#12121ad9;border:1px solid #ffffff14;flex-direction:column;width:320px;max-height:380px;animation:.25s cubic-bezier(.16,1,.3,1) slideUp;display:flex;position:absolute;bottom:calc(100% + 12px);left:0;overflow:hidden;box-shadow:0 12px 32px #0006}.node-selector-header{border-bottom:1px solid var(--border);flex-direction:column;gap:8px;padding:14px 16px 10px;display:flex}.node-selector-header h4{color:var(--text-primary);letter-spacing:.02em;font-size:13px;font-weight:600}.node-search-input{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-sm);width:100%;color:var(--text-primary);font-size:12px;font-family:var(--font-sans);transition:all var(--transition-fast);padding:6px 10px}.node-search-input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.node-selector-list{flex-direction:column;flex:1;gap:2px;padding:8px;display:flex;overflow-y:auto}.node-selector-empty{text-align:center;color:var(--text-tertiary);padding:24px;font-size:12px}.node-selector-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);align-items:center;gap:10px;padding:8px 12px;display:flex}.node-selector-item:hover{background:var(--bg-hover)}.node-selector-item.selected{background:var(--accent-muted)}.node-selector-item-checkbox{border:1px solid var(--text-tertiary);width:16px;height:16px;color:var(--accent);transition:all var(--transition-fast);background:var(--bg-tertiary);border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;font-size:11px;font-weight:700;display:flex}.node-selector-item.selected .node-selector-item-checkbox{background:var(--accent);border-color:var(--accent);color:#fff}.node-selector-item-info{flex-direction:column;flex:1;min-width:0;display:flex}.node-selector-item-title{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}.node-selector-item-meta{color:var(--text-tertiary);text-transform:capitalize;font-size:11px}.animate-fade-in{animation:.2s ease-out fadeIn}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes popIn{0%{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}}.graph-3d-wrapper{-webkit-user-select:none;user-select:none;background:radial-gradient(circle,#0a0d16 0%,#020408 100%);width:100%;height:100%;position:relative;overflow:hidden}.node-detail-drawer{-webkit-backdrop-filter:blur(25px);border-radius:var(--radius-lg);z-index:100;width:350px;color:var(--text-primary);background:#0a0f1ebf;border:1px solid #ffffff14;flex-direction:column;display:flex;position:absolute;top:20px;bottom:20px;right:20px;overflow:hidden;box-shadow:0 16px 48px #00000080,inset 0 0 0 1px #ffffff0d}.animate-slide-in{animation:.4s cubic-bezier(.16,1,.3,1) forwards slideInRight}@keyframes slideInRight{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.drawer-header{border-bottom:1px solid #ffffff0f;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.drawer-badge{text-transform:uppercase;letter-spacing:.05em;border-radius:4px;padding:4px 8px;font-size:10px;font-weight:700}.drawer-badge.badge-memory{color:#c084fc;background:#8b5cf626;border:1px solid #8b5cf64d}.drawer-badge.badge-thread{color:#22d3ee;background:#06b6d426;border:1px solid #06b6d44d}.drawer-badge.badge-research{color:#fbbf24;background:#f59e0b26;border:1px solid #f59e0b4d}.drawer-close-btn{color:var(--text-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:4px;font-size:20px;transition:color .15s;display:flex}.drawer-close-btn:hover{color:#fff}.drawer-body{flex-direction:column;flex:1;gap:12px;padding:20px;display:flex;overflow-y:auto}.drawer-title{color:#fff;margin:0;font-size:18px;font-weight:700;line-height:1.35}.drawer-subtitle{color:var(--text-tertiary);margin-top:-4px;font-size:11px}.drawer-status{background:#ffffff08;border:1px solid #ffffff0a;border-radius:6px;align-items:center;gap:8px;width:fit-content;padding:6px 12px;font-size:12px;display:flex}.drawer-status .label{color:var(--text-tertiary)}.drawer-status .value{text-transform:capitalize;font-weight:600}.drawer-status .value.status-completed,.drawer-status .value.status-done,.drawer-status .value.status-active{color:#10b981}.drawer-status .value.status-in-progress{color:#f59e0b}.drawer-status .value.status-pending{color:#3b82f6}.drawer-excerpt{color:var(--text-secondary);white-space:pre-wrap;margin-top:8px;font-size:13.5px;line-height:1.6}.drawer-footer{background:#00000026;border-top:1px solid #ffffff0f;padding:16px 20px}.drawer-action-btn{border-radius:var(--radius-sm);cursor:pointer;width:100%;transition:all var(--transition-fast);border:none;justify-content:center;align-items:center;padding:10px 16px;font-size:13px;font-weight:600;display:flex}.drawer-action-btn.btn-cyan{color:#fff;background:#06b6d4}.drawer-action-btn.btn-cyan:hover{background:#0891b2;box-shadow:0 0 16px #06b6d466}.drawer-action-btn.btn-purple{color:#fff;background:#8b5cf6}.drawer-action-btn.btn-purple:hover{background:#7c3aed;box-shadow:0 0 16px #8b5cf666}@media (width<=768px){.node-detail-drawer{width:auto;height:300px;bottom:20px;left:20px}}.chat-messages:has(.graph-3d-wrapper){padding:0;overflow:hidden}.chat-header{-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);z-index:40;background:#12121aeb;flex-shrink:0;justify-content:space-between;align-items:center;padding:12px 24px;display:flex;position:relative;overflow:visible}.chat-header-title{align-items:center;gap:10px;display:flex}.chat-header-title h2{color:var(--text-primary);letter-spacing:-.01em;font-size:15px;font-weight:600}.chat-header-model-selector{align-items:center;gap:8px;display:flex}.chat-header-model-selector span.selector-label{color:var(--text-tertiary);font-size:12px;font-weight:500}.model-selector-dropdown-wrapper{position:relative}.model-selector-dropdown-btn{border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-secondary);transition:all var(--transition-fast);cursor:pointer;background:#ffffff0a;align-items:center;gap:8px;padding:6px 12px;font-size:13px;font-weight:500;display:flex}.model-selector-dropdown-btn:hover{border-color:var(--border-hover);color:var(--text-primary);background:#ffffff14}.model-selector-dropdown-btn.active{border-color:var(--accent);color:var(--text-primary);background:var(--accent-muted)}.model-selector-menu{-webkit-backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--radius-md);z-index:200;pointer-events:auto;background:#12121afa;flex-direction:column;gap:2px;width:240px;margin-top:6px;padding:6px;animation:.15s ease-out popoverIn;display:flex;position:absolute;top:100%;right:0;box-shadow:0 12px 32px #0000008c}@keyframes popoverIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.model-selector-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);text-align:left;justify-content:space-between;align-items:center;padding:8px 12px;display:flex}.model-selector-item:hover{background:var(--bg-hover)}.model-selector-item.selected{background:var(--accent-muted)}.model-selector-item-info{flex-direction:column;gap:2px;display:flex}.model-selector-item-name{color:var(--text-primary);font-size:13px;font-weight:500}.model-selector-item.selected .model-selector-item-name{color:var(--accent)}.model-selector-item-desc{color:var(--text-tertiary);font-size:11px}.model-selector-item-badge{background:var(--bg-tertiary);color:var(--text-tertiary);text-transform:uppercase;border-radius:99px;padding:2px 6px;font-size:9px;font-weight:600}.model-selector-item.selected .model-selector-item-badge{background:var(--accent-muted);color:var(--accent)}
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" fill="none">
2
+ <!-- Transparent vault arch mark — dark UI friendly -->
3
+ <path d="M 120 300 L 120 180 A 80 80 0 0 1 280 180 L 280 300 Z" fill="#93c5fd"/>
4
+ <path d="M 280 300 L 280 180 A 80 80 0 0 0 200 100 L 350 250 L 350 300 Z" fill="#1e40af" opacity="0.55"/>
5
+ <path d="M 150 300 L 150 180 A 50 50 0 0 1 250 180 L 250 300 Z" fill="#0b1220"/>
6
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
2
+ <rect width="400" height="400" fill="#f4f1ea" />
3
+ <!-- Minimalist Archway representing a permanent memory vault -->
4
+ <path d="M 120 300 L 120 180 A 80 80 0 0 1 280 180 L 280 300 Z" fill="#2b2b2b" />
5
+ <!-- Elegant long shadow -->
6
+ <path d="M 280 300 L 280 180 A 80 80 0 0 0 200 100 L 350 250 L 350 300 Z" fill="#d9d4c7" />
7
+ <!-- Inner void -->
8
+ <path d="M 150 300 L 150 180 A 50 50 0 0 1 250 180 L 250 300 Z" fill="#f4f1ea" />
9
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <rect width="64" height="64" rx="14" fill="#0b1220"/>
3
+ <path d="M18 42V28a14 14 0 0 1 28 0v14" stroke="#60a5fa" stroke-width="5" stroke-linecap="round"/>
4
+ <path d="M24 42V28a8 8 0 0 1 16 0v14" stroke="#1e3a8a" stroke-width="4" stroke-linecap="round"/>
5
+ <rect x="20" y="40" width="24" height="6" rx="2" fill="#93c5fd"/>
6
+ </svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Total Recall — portable personal memory for any IDE. Memory explorer, chat, dream consolidation, and system health." />
7
+ <meta name="theme-color" content="#070b14" />
8
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9
+ <link rel="manifest" href="/manifest.json" />
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
13
+ <title>Total Recall — Portable Memory</title>
14
+ <script type="module" crossorigin src="/assets/index-D9-jAOr9.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-L0eEIoBf.css">
16
+ </head>
17
+ <body>
18
+ <div id="root"></div>
19
+ <script>
20
+ if ('serviceWorker' in navigator) {
21
+ window.addEventListener('load', () => {
22
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
23
+ });
24
+ }
25
+ </script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "Total Recall — Portable Memory",
3
+ "short_name": "Total Recall",
4
+ "description": "Portable personal memory for any IDE. Remember everything, recall anywhere.",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#070b14",
8
+ "theme_color": "#070b14",
9
+ "orientation": "any",
10
+ "categories": ["productivity", "utilities"],
11
+ "icons": [
12
+ {
13
+ "src": "/favicon.svg",
14
+ "sizes": "any",
15
+ "type": "image/svg+xml",
16
+ "purpose": "any maskable"
17
+ },
18
+ {
19
+ "src": "/brand/total-recall-icon.svg",
20
+ "sizes": "any",
21
+ "type": "image/svg+xml",
22
+ "purpose": "any"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Total Recall — Service Worker
3
+ *
4
+ * Caching strategies:
5
+ * • App shell: Pre-cached on install
6
+ * • /api/* GETs: Stale-While-Revalidate
7
+ * • /api/memory writes (POST/PUT/PATCH/DELETE): Network-first + offline queue with Background Sync
8
+ * • Static assets: Cache-first
9
+ * • Everything else: Network-first
10
+ *
11
+ * Vault-hash optimization:
12
+ * Stores the vault_hash from /api/vault/status responses. Before fetching
13
+ * memory data, the app can check if the cached hash still matches — if so,
14
+ * skip the fetch entirely (handled client-side via the hash endpoint).
15
+ */
16
+
17
+ const CACHE_NAME = 'total-recall-v1';
18
+ const API_CACHE = 'total-recall-api-v1';
19
+
20
+ // Pre-cache app shell
21
+ const PRECACHE_URLS = [
22
+ '/',
23
+ ];
24
+
25
+ // ─── Install ──────────────────────────────────────────────────────────────────
26
+
27
+ self.addEventListener('install', (event) => {
28
+ event.waitUntil(
29
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
30
+ );
31
+ self.skipWaiting();
32
+ });
33
+
34
+ // ─── Activate ─────────────────────────────────────────────────────────────────
35
+
36
+ self.addEventListener('activate', (event) => {
37
+ event.waitUntil(
38
+ caches.keys().then((keys) =>
39
+ Promise.all(
40
+ keys
41
+ .filter((k) => k !== CACHE_NAME && k !== API_CACHE)
42
+ .map((k) => caches.delete(k))
43
+ )
44
+ )
45
+ );
46
+ self.clients.claim();
47
+ });
48
+
49
+ // ─── Fetch ────────────────────────────────────────────────────────────────────
50
+
51
+ self.addEventListener('fetch', (event) => {
52
+ const url = new URL(event.request.url);
53
+
54
+ // Skip non-GET for caching (but handle write queueing for /api/memory)
55
+ if (event.request.method !== 'GET') {
56
+ if (url.pathname.startsWith('/api/memory')) {
57
+ event.respondWith(networkWithOfflineQueue(event.request));
58
+ }
59
+ return;
60
+ }
61
+
62
+ // API routes: stale-while-revalidate (with vault-hash tracking)
63
+ if (url.pathname.startsWith('/api/')) {
64
+ event.respondWith(staleWhileRevalidate(event.request));
65
+ return;
66
+ }
67
+
68
+ // Static assets: cache-first
69
+ if (url.pathname.match(/\.(js|css|svg|png|woff2?)$/)) {
70
+ event.respondWith(cacheFirst(event.request));
71
+ return;
72
+ }
73
+
74
+ // Everything else: network-first
75
+ event.respondWith(networkFirst(event.request));
76
+ });
77
+
78
+ // ─── Stale-While-Revalidate ──────────────────────────────────────────────────
79
+
80
+ async function staleWhileRevalidate(request) {
81
+ const cache = await caches.open(API_CACHE);
82
+ const cached = await cache.match(request);
83
+
84
+ const fetchPromise = fetch(request)
85
+ .then((response) => {
86
+ if (response.ok) {
87
+ cache.put(request, response.clone());
88
+
89
+ // Track vault_hash from /api/vault/status responses
90
+ const url = new URL(request.url);
91
+ if (
92
+ url.pathname === '/api/vault/status' ||
93
+ url.pathname === '/api/vault/hash'
94
+ ) {
95
+ response
96
+ .clone()
97
+ .json()
98
+ .then((data) => {
99
+ if (data.vault_hash !== undefined) {
100
+ saveVaultHash(data.vault_hash);
101
+ }
102
+ })
103
+ .catch(() => {});
104
+ }
105
+ }
106
+ return response;
107
+ })
108
+ .catch(() => cached);
109
+
110
+ return cached || fetchPromise;
111
+ }
112
+
113
+ // ─── Cache-First ──────────────────────────────────────────────────────────────
114
+
115
+ async function cacheFirst(request) {
116
+ const cached = await caches.match(request);
117
+ if (cached) return cached;
118
+ const response = await fetch(request);
119
+ if (response.ok) {
120
+ const cache = await caches.open(CACHE_NAME);
121
+ cache.put(request, response.clone());
122
+ }
123
+ return response;
124
+ }
125
+
126
+ // ─── Network-First ────────────────────────────────────────────────────────────
127
+
128
+ async function networkFirst(request) {
129
+ try {
130
+ const response = await fetch(request);
131
+ if (response.ok) {
132
+ const cache = await caches.open(CACHE_NAME);
133
+ cache.put(request, response.clone());
134
+ }
135
+ return response;
136
+ } catch {
137
+ const cached = await caches.match(request);
138
+ return cached || new Response('Offline', { status: 503 });
139
+ }
140
+ }
141
+
142
+ // ─── Network with Offline Queue ───────────────────────────────────────────────
143
+
144
+ async function networkWithOfflineQueue(request) {
145
+ try {
146
+ return await fetch(request);
147
+ } catch {
148
+ // Queue for background sync when back online
149
+ const body = await request.clone().text();
150
+ const queue = await getQueue();
151
+ queue.push({
152
+ url: request.url,
153
+ method: request.method,
154
+ headers: Object.fromEntries(request.headers),
155
+ body,
156
+ timestamp: Date.now(),
157
+ });
158
+ await saveQueue(queue);
159
+
160
+ // Register for background sync
161
+ if (self.registration.sync) {
162
+ await self.registration.sync.register('sync-memory-writes');
163
+ }
164
+
165
+ return new Response(JSON.stringify({ queued: true, offline: true }), {
166
+ status: 202,
167
+ headers: { 'Content-Type': 'application/json' },
168
+ });
169
+ }
170
+ }
171
+
172
+ // ─── Background Sync ──────────────────────────────────────────────────────────
173
+
174
+ self.addEventListener('sync', (event) => {
175
+ if (event.tag === 'sync-memory-writes') {
176
+ event.waitUntil(replayQueue());
177
+ }
178
+ });
179
+
180
+ // ─── Vault Hash Tracking ──────────────────────────────────────────────────────
181
+
182
+ let _cachedVaultHash = null;
183
+
184
+ function saveVaultHash(hash) {
185
+ _cachedVaultHash = hash;
186
+ // Also persist to IndexedDB for cross-restart durability
187
+ openDB()
188
+ .then((db) => {
189
+ const tx = db.transaction('meta', 'readwrite');
190
+ tx.objectStore('meta').put({ key: 'vault_hash', value: hash });
191
+ })
192
+ .catch(() => {});
193
+ }
194
+
195
+ // Expose vault hash to clients via message API
196
+ self.addEventListener('message', (event) => {
197
+ if (event.data && event.data.type === 'GET_VAULT_HASH') {
198
+ // Return the cached vault hash so clients can skip fetches when stale
199
+ const respond = (hash) => {
200
+ event.ports?.[0]?.postMessage({ vault_hash: hash });
201
+ };
202
+
203
+ if (_cachedVaultHash !== null) {
204
+ respond(_cachedVaultHash);
205
+ } else {
206
+ // Try loading from IndexedDB
207
+ openDB()
208
+ .then((db) => {
209
+ const tx = db.transaction('meta', 'readonly');
210
+ const req = tx.objectStore('meta').get('vault_hash');
211
+ req.onsuccess = () => respond(req.result?.value || null);
212
+ req.onerror = () => respond(null);
213
+ })
214
+ .catch(() => respond(null));
215
+ }
216
+ }
217
+ });
218
+
219
+ // ─── IndexedDB Helpers ────────────────────────────────────────────────────────
220
+
221
+ function openDB() {
222
+ return new Promise((resolve, reject) => {
223
+ const req = indexedDB.open('total-recall-sw', 2);
224
+ req.onupgradeneeded = (event) => {
225
+ const db = req.result;
226
+ if (!db.objectStoreNames.contains('queue')) {
227
+ db.createObjectStore('queue', { keyPath: 'timestamp' });
228
+ }
229
+ if (!db.objectStoreNames.contains('meta')) {
230
+ db.createObjectStore('meta', { keyPath: 'key' });
231
+ }
232
+ };
233
+ req.onsuccess = () => resolve(req.result);
234
+ req.onerror = () => reject(req.error);
235
+ });
236
+ }
237
+
238
+ async function getQueue() {
239
+ const db = await openDB();
240
+ return new Promise((resolve, reject) => {
241
+ const tx = db.transaction('queue', 'readonly');
242
+ const store = tx.objectStore('queue');
243
+ const req = store.getAll();
244
+ req.onsuccess = () => resolve(req.result || []);
245
+ req.onerror = () => reject(req.error);
246
+ });
247
+ }
248
+
249
+ async function saveQueue(items) {
250
+ const db = await openDB();
251
+ const tx = db.transaction('queue', 'readwrite');
252
+ const store = tx.objectStore('queue');
253
+ store.clear();
254
+ for (const item of items) store.put(item);
255
+ }
256
+
257
+ async function replayQueue() {
258
+ const queue = await getQueue();
259
+ const failed = [];
260
+ for (const item of queue) {
261
+ try {
262
+ await fetch(item.url, {
263
+ method: item.method,
264
+ headers: item.headers,
265
+ body: item.body,
266
+ });
267
+ } catch {
268
+ failed.push(item);
269
+ }
270
+ }
271
+ await saveQueue(failed);
272
+ }
@@ -2,10 +2,15 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <meta name="description" content="Total Recall — portable personal memory for any IDE. Memory explorer, chat, dream consolidation, and system health." />
7
- <meta name="theme-color" content="#070b14" />
7
+ <meta name="theme-color" content="#3b82f6" />
8
+ <meta name="mobile-web-app-capable" content="yes" />
9
+ <meta name="apple-mobile-web-app-capable" content="yes" />
10
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
11
+ <meta name="apple-mobile-web-app-title" content="Total Recall" />
8
12
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
13
+ <link rel="apple-touch-icon" href="/icon-192.png" />
9
14
  <link rel="manifest" href="/manifest.json" />
10
15
  <link rel="preconnect" href="https://fonts.googleapis.com" />
11
16
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@simplewebauthn/browser": "^13.3.0",
14
+ "qrcode.react": "^4.2.0",
14
15
  "react": "^19.2.6",
15
16
  "react-dom": "^19.2.6",
16
17
  "react-router-dom": "^7.15.0",
@@ -26,6 +27,7 @@
26
27
  "eslint-plugin-react-hooks": "^7.1.1",
27
28
  "eslint-plugin-react-refresh": "^0.5.2",
28
29
  "globals": "^17.6.0",
30
+ "jsdom": "^29.1.1",
29
31
  "typescript": "~6.0.2",
30
32
  "typescript-eslint": "^8.59.2",
31
33
  "vite": "^8.0.12"
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import App from './App';
4
+ import * as api from './api';
5
+ import * as onboarding from './utils/onboarding';
6
+
7
+ vi.mock('./api', () => ({
8
+ checkSession: vi.fn(),
9
+ logout: vi.fn(),
10
+ registerUnauthedCallback: vi.fn(),
11
+ fetchHealth: vi.fn(),
12
+ getApiBase: vi.fn(),
13
+ setApiBase: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('./utils/onboarding', () => ({
17
+ isOnboardingComplete: vi.fn(),
18
+ }));
19
+
20
+ // Mock the components since App contains a lot of them and we just want to test routing/auth logic
21
+ vi.mock('./pages/LoginPage', () => ({
22
+ default: ({ onAuthenticated }: any) => <div data-testid="login-page"><button onClick={onAuthenticated}>Login</button></div>
23
+ }));
24
+ vi.mock('./pages/ChatPage', () => ({ default: () => <div data-testid="chat-page" /> }));
25
+ vi.mock('./pages/MemoryPage', () => ({ default: () => <div data-testid="memory-page" /> }));
26
+ vi.mock('./pages/SettingsPage', () => ({ default: () => <div data-testid="settings-page" /> }));
27
+ vi.mock('./pages/TasksPage', () => ({ default: () => <div data-testid="tasks-page" /> }));
28
+ vi.mock('./pages/ApiKeysPage', () => ({ default: () => <div data-testid="apikeys-page" /> }));
29
+ vi.mock('./pages/IntegrationsPage', () => ({ default: () => <div data-testid="integrations-page" /> }));
30
+ vi.mock('./components/BrainSelector', () => ({ default: () => <div data-testid="brain-selector" /> }));
31
+ vi.mock('./pages/SkillsPage', () => ({ default: () => <div data-testid="skills-page" /> }));
32
+ vi.mock('./pages/HelpPage', () => ({ default: () => <div data-testid="help-page" /> }));
33
+ vi.mock('./pages/GraphPage', () => ({ default: () => <div data-testid="graph-page" /> }));
34
+ vi.mock('./pages/OpenWikiPage', () => ({ default: () => <div data-testid="openwiki-page" /> }));
35
+ vi.mock('./pages/OnboardingPage', () => ({ default: () => <div data-testid="onboarding-page" /> }));
36
+
37
+ describe('App', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ it('renders loading state initially', () => {
43
+ vi.mocked(api.checkSession).mockImplementation(() => new Promise(() => {})); // Never resolves
44
+ render(<App />);
45
+ expect(document.querySelector('svg')).toBeInTheDocument(); // Loading spinner
46
+ });
47
+
48
+ it('renders login page if unauthed', async () => {
49
+ vi.mocked(api.checkSession).mockResolvedValue(false);
50
+ render(<App />);
51
+
52
+ await waitFor(() => {
53
+ expect(screen.getByTestId('login-page')).toBeInTheDocument();
54
+ });
55
+ });
56
+
57
+ it('renders app layout if authed', async () => {
58
+ vi.mocked(api.checkSession).mockResolvedValue(true);
59
+ vi.mocked(onboarding.isOnboardingComplete).mockReturnValue(true);
60
+
61
+ render(<App />);
62
+
63
+ await waitFor(() => {
64
+ // Sidebar should be present
65
+ expect(screen.getByText('Chat')).toBeInTheDocument();
66
+ expect(screen.getByText('Vault')).toBeInTheDocument();
67
+ // Chat page is always rendered initially (floating or main)
68
+ expect(screen.getByTestId('chat-page')).toBeInTheDocument();
69
+ });
70
+ });
71
+
72
+ it('redirects to onboarding if not complete', async () => {
73
+ vi.mocked(api.checkSession).mockResolvedValue(true);
74
+ vi.mocked(onboarding.isOnboardingComplete).mockReturnValue(false);
75
+
76
+ render(<App />);
77
+
78
+ await waitFor(() => {
79
+ expect(screen.getByTestId('onboarding-page')).toBeInTheDocument();
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,35 @@
1
+ // ─── Shared API base — imported by all domain modules ────────────────────────
2
+
3
+ import type { HealthData, MemoryNode, SandboxResult, ResearchItem, Conflict, UsageData, ConfigJson } from '../types'
4
+ export type { HealthData, MemoryNode, SandboxResult, ResearchItem, Conflict, UsageData, ConfigJson }
5
+
6
+ export let API_BASE = localStorage.getItem('TOTAL_RECALL_API_BASE') || ''
7
+
8
+ export function getApiBase() { return API_BASE }
9
+ export function setApiBase(url: string) {
10
+ API_BASE = url
11
+ localStorage.setItem('TOTAL_RECALL_API_BASE', url)
12
+ window.location.reload()
13
+ }
14
+
15
+ // ─── Global 401 handler ────────────────────────────────────────────────────────
16
+ // Components can register a callback to be notified when any request gets 401.
17
+ // App.tsx registers this on mount to flip auth state → shows login screen.
18
+ type UnauthedCallback = () => void
19
+ let onUnauthed: UnauthedCallback | null = null
20
+ export function registerUnauthedCallback(cb: UnauthedCallback) { onUnauthed = cb }
21
+ export function clearUnauthedCallback() { onUnauthed = null }
22
+
23
+ export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
24
+ const activeBrain = localStorage.getItem('total-recall-active-brain');
25
+ const headers = new Headers(options.headers || {});
26
+ if (activeBrain) {
27
+ headers.set('x-total-recall-brain', activeBrain);
28
+ }
29
+
30
+ const res = await fetch(url, { credentials: 'include', ...options, headers })
31
+ if (res.status === 401) {
32
+ onUnauthed?.()
33
+ }
34
+ return res
35
+ }