total-recall-brain 3.13.2 → 3.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/bin/total-recall.mjs +3 -0
  2. package/frontend/src/App.tsx +8 -7
  3. package/frontend/src/api.ts +25 -0
  4. package/frontend/src/components/UsageChart.tsx +1 -1
  5. package/frontend/src/pages/ApiKeysPage.tsx +118 -42
  6. package/frontend/src/pages/ChatPage.tsx +1 -1
  7. package/frontend/src/pages/DesignDocsPage.tsx +28 -21
  8. package/frontend/src/pages/FilesPage.tsx +6 -1
  9. package/frontend/src/pages/GraphPage.tsx +5 -1
  10. package/frontend/src/pages/HealthPage.tsx +1 -1
  11. package/frontend/src/pages/InboxPage.tsx +2 -1
  12. package/frontend/src/pages/InstructionsPage.tsx +26 -26
  13. package/frontend/src/pages/IntegrationsPage.tsx +11 -5
  14. package/frontend/src/pages/MemoryPage.tsx +27 -118
  15. package/frontend/src/pages/ModelsPage.tsx +2 -50
  16. package/frontend/src/pages/OnboardingPage.tsx +5 -10
  17. package/frontend/src/pages/OpenWikiPage.tsx +1 -0
  18. package/frontend/src/pages/SettingsPage.tsx +136 -131
  19. package/frontend/src/pages/SkillsPage.tsx +102 -46
  20. package/frontend/src/pages/TasksPage.tsx +60 -25
  21. package/frontend/src/pages/UsagePage.tsx +206 -95
  22. package/frontend/src/types.ts +17 -11
  23. package/frontend/src/utils/onboarding.ts +10 -0
  24. package/package.json +3 -2
  25. package/scaffold/.agent/skills/ssss/SKILL.md +235 -0
  26. package/scaffold/.agent/skills/ssss/evals/evals.json +37 -0
  27. package/scaffold/.agent/skills/ssss/references/contract-reference.md +95 -0
  28. package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +215 -0
  29. package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +22 -0
  30. package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
  31. package/src/cli/backup.mjs +3 -3
  32. package/src/cli/chat.mjs +2 -2
  33. package/src/cli/config.mjs +2 -9
  34. package/src/cli/connect.mjs +1 -1
  35. package/src/cli/deploy-ui.mjs +1 -1
  36. package/src/cli/deploy.mjs +2 -2
  37. package/src/cli/doctor.mjs +1 -1
  38. package/src/cli/help.mjs +1 -1
  39. package/src/cli/init.mjs +19 -1
  40. package/src/cli/key.mjs +150 -0
  41. package/src/cli/secret.mjs +30 -1
  42. package/src/cli/skill.mjs +0 -2
  43. package/src/cli/sync.mjs +19 -0
  44. package/src/cli/upgrade.mjs +1 -1
  45. package/src/cli/wizard.html +5 -5
  46. package/src/core/crons.mjs +11 -0
  47. package/src/core/daemon-loop.mjs +11 -0
  48. package/src/core/import-rules.mjs +4 -3
  49. package/src/core/okf-adapter.mjs +10 -10
  50. package/src/core/runtime.mjs +0 -2
  51. package/src/core/secrets-store.mjs +16 -0
  52. package/src/core/skills-registry.mjs +38 -0
  53. package/src/core/surface.mjs +1 -1
  54. package/src/core/task_runner.mjs +6 -0
  55. package/src/core/usage-tracker.mjs +16 -3
  56. package/src/core/vault-watcher.mjs +92 -0
  57. package/src/core/vault.mjs +0 -2
  58. package/src/server/api.mjs +1 -1
  59. package/src/server/rest.mjs +69 -57
  60. package/src/server/route-manifest.json +8 -0
  61. package/src/server/routes/_shared.mjs +1 -1
  62. package/src/server/routes/secrets.mjs +4 -4
  63. package/src/server/routes/skills.mjs +92 -10
@@ -62,7 +62,7 @@ const PRESETS: Preset[] = [
62
62
  }
63
63
  ]
64
64
 
65
- export default function IntegrationsPage() {
65
+ export default function IntegrationsPage({ activeBrainId }: { activeBrainId?: string }) {
66
66
  const [copied, setCopied] = useState<string | null>(null)
67
67
  const [connecting, setConnecting] = useState<string | null>(null)
68
68
  const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
@@ -82,7 +82,9 @@ export default function IntegrationsPage() {
82
82
  .then(res => {
83
83
  if (active) setKeys(res.keys || [])
84
84
  })
85
- .catch(() => {})
85
+ .catch(e => {
86
+ if (active) setMessage({ type: 'error', text: `Failed to list API keys: ${(e as Error).message || 'Unknown error'}` })
87
+ })
86
88
 
87
89
  fetchActiveIntegrations()
88
90
  .then(res => {
@@ -90,16 +92,20 @@ export default function IntegrationsPage() {
90
92
  setActiveIdes(res.active)
91
93
  }
92
94
  })
93
- .catch(() => {})
95
+ .catch(e => {
96
+ if (active) setMessage({ type: 'error', text: `Failed to fetch active integrations: ${(e as Error).message || 'Unknown error'}` })
97
+ })
94
98
 
95
99
  fetchExtensionStatus()
96
100
  .then(res => {
97
101
  if (active) setExtStatus(res)
98
102
  })
99
- .catch(() => {})
103
+ .catch(e => {
104
+ if (active) setMessage({ type: 'error', text: `Failed to fetch extension status: ${(e as Error).message || 'Unknown error'}` })
105
+ })
100
106
 
101
107
  return () => { active = false }
102
- }, [])
108
+ }, [activeBrainId])
103
109
 
104
110
  const filteredPresets = useMemo(() => {
105
111
  if (activeIdes.length === 0) return PRESETS
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, useRef } from "react"
1
+ import { useState, useEffect, useCallback } from "react"
2
2
  import { useSearchParams } from "react-router-dom"
3
3
  import { listMemory, searchMemory, readMemory, fetchConflicts, resolveConflict, saveMemory, createMemory, deleteMemory, fetchSessions, deleteSession } from "../api"
4
4
  import type { MemoryNode, Conflict } from "../types"
@@ -41,70 +41,6 @@ function renderMarkdown(md: string) {
41
41
  return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />;
42
42
  }
43
43
 
44
- function markdownToHtml(md: string): string {
45
- if (!md) return "";
46
- let html = md
47
- .replace(/&/g, "&amp;")
48
- .replace(/</g, "&lt;")
49
- .replace(/>/g, "&gt;");
50
-
51
- // Headings
52
- html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
53
- html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
54
- html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
55
-
56
- // Bold & Italic
57
- html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
58
- html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
59
-
60
- // Blockquotes
61
- html = html.replace(/^> (.*$)/gim, "<blockquote>$1</blockquote>");
62
-
63
- // Lists
64
- html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
65
-
66
- // Newlines
67
- html = html.replace(/\n/g, "<br />");
68
-
69
- return html;
70
- }
71
-
72
- function htmlToMarkdown(html: string): string {
73
- if (!html) return "";
74
- let md = html;
75
-
76
- // Replace <br> and <br /> with newline
77
- md = md.replace(/<br\s*\/?>/gi, "\n");
78
-
79
- // Headings
80
- md = md.replace(/<h1>(.*?)<\/h1>/gi, "# $1\n");
81
- md = md.replace(/<h2>(.*?)<\/h2>/gi, "## $1\n");
82
- md = md.replace(/<h3>(.*?)<\/h3>/gi, "### $1\n");
83
-
84
- // Bold & Italic
85
- md = md.replace(/<strong>(.*?)<\/strong>/gi, "**$1**");
86
- md = md.replace(/<b>(.*?)<\/b>/gi, "**$1**");
87
- md = md.replace(/<em>(.*?)<\/em>/gi, "*$1*");
88
- md = md.replace(/<i>(.*?)<\/i>/gi, "*$1*");
89
-
90
- // Blockquotes
91
- md = md.replace(/<blockquote>(.*?)<\/blockquote>/gi, "> $1\n");
92
-
93
- // Lists
94
- md = md.replace(/<li>(.*?)<\/li>/gi, "- $1\n");
95
-
96
- // Clean up any remaining HTML tags safely
97
- md = md.replace(/<[^>]+>/g, "");
98
-
99
- // Decode HTML entities
100
- md = md
101
- .replace(/&amp;/g, "&")
102
- .replace(/&lt;/g, "<")
103
- .replace(/&gt;/g, ">");
104
-
105
- return md.trim();
106
- }
107
-
108
44
  export default function MemoryPage({ activeBrainId }: { activeBrainId?: string }) {
109
45
  const [searchParams, setSearchParams] = useSearchParams()
110
46
  const urlSlug = searchParams.get("slug")
@@ -143,23 +79,6 @@ export default function MemoryPage({ activeBrainId }: { activeBrainId?: string }
143
79
  const [editContradicts, setEditContradicts] = useState("")
144
80
  const [editSupersedes, setEditSupersedes] = useState("")
145
81
 
146
- const editorRef = useRef<HTMLDivElement>(null)
147
-
148
- const execCommand = (command: string, value: string = "") => {
149
- document.execCommand(command, false, value)
150
- editorRef.current?.focus()
151
- if (editorRef.current) {
152
- setEditContent(htmlToMarkdown(editorRef.current.innerHTML))
153
- }
154
- }
155
-
156
- useEffect(() => {
157
- if ((isEditing || isCreating) && editorRef.current) {
158
- editorRef.current.innerHTML = markdownToHtml(editContent)
159
- }
160
- // eslint-disable-next-line react-hooks/exhaustive-deps -- only sync editor HTML on toggle edit/create
161
- }, [isEditing, isCreating])
162
-
163
82
  const fetchNodes = useCallback(async () => {
164
83
  setLoading(true)
165
84
  setError("")
@@ -584,42 +503,28 @@ export default function MemoryPage({ activeBrainId }: { activeBrainId?: string }
584
503
  </div>
585
504
  </div>
586
505
 
587
- {/* WYSIWYG visual HTML Editor - NO RAW CODE PREVIEW! EDIT Styled preview DIRECTLY */}
506
+ {/* Markdown Content Editor */}
588
507
  <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
589
- <label style={{ fontSize: 12, color: "var(--text-secondary)" }}>Visual WYSIWYG Memory Content Editor</label>
590
- <div style={{ background: "rgba(10,15,30,0.5)", border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden" }}>
591
- {/* Rich Formatting Toolbar */}
592
- <div style={{ display: "flex", gap: 8, padding: 8, background: "rgba(255,255,255,0.03)", borderBottom: "1px solid var(--border)", flexWrap: "wrap" }}>
593
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("bold")} style={{ fontWeight: "bold", padding: "4px 10px" }}>B</button>
594
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("italic")} style={{ fontStyle: "italic", padding: "4px 10px" }}>I</button>
595
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<h1>")} style={{ padding: "4px 8px" }}>H1</button>
596
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<h2>")} style={{ padding: "4px 8px" }}>H2</button>
597
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<blockquote>")} style={{ padding: "4px 8px" }}>“ Quote</button>
598
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("insertUnorderedList")} style={{ padding: "4px 8px" }}>• List</button>
599
- <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("removeFormat")} style={{ padding: "4px 8px", fontSize: 11 }}>Clear</button>
600
- </div>
601
-
602
- {/* Direct contentEditable Editor Pane */}
603
- <div
604
- ref={editorRef}
605
- contentEditable
606
- onInput={() => {
607
- if (editorRef.current) {
608
- setEditContent(htmlToMarkdown(editorRef.current.innerHTML))
609
- }
610
- }}
611
- style={{
612
- minHeight: 300,
613
- padding: 16,
614
- color: "#fff",
615
- background: "#090d16",
616
- outline: "none",
617
- fontSize: 14,
618
- lineHeight: 1.6,
619
- overflowY: "auto"
620
- }}
621
- />
622
- </div>
508
+ <label style={{ fontSize: 12, color: "var(--text-secondary)" }}>Memory Content</label>
509
+ <textarea
510
+ value={editContent}
511
+ onChange={(e) => setEditContent(e.target.value)}
512
+ placeholder="Write markdown here..."
513
+ style={{
514
+ minHeight: 300,
515
+ padding: 16,
516
+ color: "#fff",
517
+ background: "#090d16",
518
+ border: "1px solid var(--border)",
519
+ borderRadius: 8,
520
+ outline: "none",
521
+ fontSize: 14,
522
+ lineHeight: 1.6,
523
+ resize: "vertical",
524
+ width: "100%",
525
+ boxSizing: "border-box"
526
+ }}
527
+ />
623
528
  </div>
624
529
 
625
530
  {/* Action Buttons */}
@@ -934,7 +839,11 @@ export default function MemoryPage({ activeBrainId }: { activeBrainId?: string }
934
839
  <div className="memory-list" style={{ display: "grid", gridTemplateColumns: "1fr", gap: 12 }}>
935
840
  {nodes.length === 0 && !loading && (
936
841
  <div style={{ textAlign: "center", padding: 40, color: "var(--text-tertiary)" }}>
937
- {query ? "No matching nodes found." : "No memory nodes loaded. Is the backend running?"}
842
+ {query
843
+ ? "No matching nodes found."
844
+ : category !== "all"
845
+ ? `No memory nodes found in category "${category}".`
846
+ : "No memory nodes loaded. Is the backend running?"}
938
847
  </div>
939
848
  )}
940
849
  {nodes.map(n => {
@@ -126,7 +126,8 @@ export default function ModelsPage() {
126
126
  { id: 'antigravity', name: 'Antigravity (Gemini SDK)', desc: 'Primary core developer agent' },
127
127
  { id: 'gemini', name: 'Gemini CLI', desc: 'Direct Gemini assistant binary' },
128
128
  { id: 'claude', name: 'Claude Code', desc: 'Anthropic developer CLI wrapper' },
129
- { id: 'codex', name: 'Codex CLI', desc: 'OpenAI agent binary integration' }
129
+ { id: 'codex', name: 'Codex CLI', desc: 'OpenAI agent binary integration' },
130
+ { id: 'grok', name: 'Grok CLI', desc: 'xAI developer binary integration' }
130
131
  ];
131
132
 
132
133
 
@@ -177,55 +178,6 @@ export default function ModelsPage() {
177
178
 
178
179
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24 }}>
179
180
 
180
- {/* Ollama Panel */}
181
- <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
182
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
183
- <span style={{ fontSize: 20 }}>🦙</span>
184
- <div>
185
- <h3 style={{ fontSize: 14, fontWeight: 600 }}>Local Provider</h3>
186
- <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Connect Ollama, LM Studio, vLLM, or Llama.cpp</p>
187
- </div>
188
- </div>
189
-
190
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
191
- <label htmlFor="local_endpoint" style={{ fontSize: 13, fontWeight: 500 }}>Provider Base URL</label>
192
- <input
193
- id="local_endpoint"
194
- type="text"
195
- placeholder="http://127.0.0.1:11434 or http://127.0.0.1:1234/v1"
196
- value={configData?.brain?.local_endpoint || ''}
197
- onChange={(e) => updateBrainProp('local_endpoint', e.target.value)}
198
- style={{
199
- background: 'var(--bg-tertiary)',
200
- color: 'var(--text-primary)',
201
- border: '1px solid var(--border)',
202
- padding: '8px 12px',
203
- borderRadius: 6,
204
- outline: 'none',
205
- fontSize: 13
206
- }}
207
- />
208
- <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Leave blank to use the default localhost endpoint.</span>
209
- </div>
210
-
211
- <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8 }}>
212
- <span className="badge" style={{
213
- background: health?.ollama === 'running' ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
214
- color: health?.ollama === 'running' ? '#10b981' : '#ef4444',
215
- border: health?.ollama === 'running' ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(239, 68, 68, 0.2)',
216
- fontSize: 10,
217
- padding: '4px 8px'
218
- }}>
219
- {health?.ollama === 'running' ? 'Connected' : 'Offline'}
220
- </span>
221
- {health?.ollama === 'running' && health?.ollama_models && (
222
- <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
223
- {health.ollama_models.length} local models available
224
- </span>
225
- )}
226
- </div>
227
- </div>
228
-
229
181
  {/* Cloud API Keys Panel */}
230
182
  <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
231
183
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
@@ -67,14 +67,7 @@ function saveProgress(p: { step: number; fact?: string; complete?: boolean }) {
67
67
  if (p.complete) localStorage.setItem('tr-onboarding-complete', '1')
68
68
  }
69
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
- }
70
+ import { isOnboardingComplete } from '../utils/onboarding'
78
71
 
79
72
  /** Client-side .env parse so we can re-send pairs on import without server holding paste. */
80
73
  function parseEnvClient(text: string): Record<string, string> {
@@ -140,8 +133,10 @@ export default function OnboardingPage() {
140
133
  useEffect(() => {
141
134
  if (step.id !== 'import-env' || importMode !== 'scan') return
142
135
  let cancelled = false
143
- setScanLoading(true)
144
- setScanError('')
136
+ Promise.resolve().then(() => {
137
+ setScanLoading(true)
138
+ setScanError('')
139
+ })
145
140
  scanEnvSecrets()
146
141
  .then((data) => {
147
142
  if (cancelled) return
@@ -94,6 +94,7 @@ export default function OpenWikiPage({ activeBrainId = 'global' }: { activeBrain
94
94
  };
95
95
 
96
96
  useEffect(() => {
97
+ // eslint-disable-next-line react-hooks/set-state-in-effect
97
98
  void fetchNodes(activeBrainId || 'global');
98
99
  void fetchHealthData();
99
100
  const interval = setInterval(fetchHealthData, 15000);