total-recall-brain 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,10 +83,9 @@ if (!process.env.GOOGLE_API_KEY) {
83
83
  }
84
84
  }
85
85
 
86
- const apiKey = process.env.GOOGLE_API_KEY;
86
+ const apiKey = process.env.GOOGLE_API_KEY || '';
87
87
  if (!apiKey) {
88
- console.error('Error: GOOGLE_API_KEY environment variable is not configured.');
89
- process.exit(1);
88
+ console.warn('Warning: GOOGLE_API_KEY environment variable is not configured. Attempting connection via GCP Application Default Credentials or ambient environment...');
90
89
  }
91
90
 
92
91
  // Resolve the model dynamically from the API registry based on the category/alias selected
@@ -109,7 +108,13 @@ async function resolveGenerativeModel(selectedModel) {
109
108
  }
110
109
 
111
110
  try {
112
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
111
+ const keyParam = apiKey ? `key=${apiKey}` : '';
112
+ const headers = {};
113
+ if (!apiKey && process.env.GCLOUD_TOKEN) {
114
+ headers['Authorization'] = `Bearer ${process.env.GCLOUD_TOKEN}`;
115
+ }
116
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?${keyParam}`, {
117
+ headers,
113
118
  signal: AbortSignal.timeout(5000)
114
119
  });
115
120
  if (res.ok) {
@@ -154,13 +159,19 @@ async function resolveGenerativeModel(selectedModel) {
154
159
  async function main() {
155
160
  try {
156
161
  const resolvedModel = await resolveGenerativeModel(requestedModel);
157
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${resolvedModel}:generateContent?key=${apiKey}`;
162
+ const keyParam = apiKey ? `?key=${apiKey}` : '';
163
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${resolvedModel}:generateContent${keyParam}`;
164
+
165
+ const headers = {
166
+ 'Content-Type': 'application/json',
167
+ };
168
+ if (!apiKey && process.env.GCLOUD_TOKEN) {
169
+ headers['Authorization'] = `Bearer ${process.env.GCLOUD_TOKEN}`;
170
+ }
158
171
 
159
172
  const res = await fetch(url, {
160
173
  method: 'POST',
161
- headers: {
162
- 'Content-Type': 'application/json',
163
- },
174
+ headers,
164
175
  body: JSON.stringify({
165
176
  contents: [{
166
177
  parts: [{ text: prompt }]
@@ -685,3 +685,23 @@ export async function fetchExtensionStatus(): Promise<{ available: boolean; conn
685
685
  if (!res.ok) return { available: false, connected: false }
686
686
  return res.json()
687
687
  }
688
+
689
+ // ─── Auto-Update APIs ──────────────────────────────────────────────────────────
690
+
691
+ export interface UpdateCheckResult {
692
+ currentVersion: string
693
+ latestVersion: string
694
+ updateAvailable: boolean
695
+ }
696
+
697
+ export async function checkUpdate(): Promise<UpdateCheckResult> {
698
+ const res = await apiFetch(`${API_BASE}/api/update/check`)
699
+ if (!res.ok) throw new Error(`Check update API error: ${res.status}`)
700
+ return res.json()
701
+ }
702
+
703
+ export async function runUpdate(): Promise<{ success: boolean; message: string }> {
704
+ const res = await apiFetch(`${API_BASE}/api/update/run`, { method: 'POST' })
705
+ if (!res.ok) throw new Error(`Run update API error: ${res.status}`)
706
+ return res.json()
707
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { fetchHealth } from '../api'
3
- import type { HealthData } from '../types'
2
+ import { fetchHealth, checkUpdate, runUpdate } from '../api'
3
+ import type { HealthData, UpdateCheckResult } from '../types'
4
4
 
5
5
  function formatUptime(seconds: number): string {
6
6
  const d = Math.floor(seconds / 86400)
@@ -15,6 +15,12 @@ export default function HealthPage() {
15
15
  const [health, setHealth] = useState<HealthData | null>(null)
16
16
  const [error, setError] = useState('')
17
17
  const [lastPoll, setLastPoll] = useState<Date | null>(null)
18
+
19
+ // Auto-Update States
20
+ const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null)
21
+ const [isUpdating, setIsUpdating] = useState(false)
22
+ const [showConfirmModal, setShowConfirmModal] = useState(false)
23
+ const [updateError, setUpdateError] = useState('')
18
24
 
19
25
  useEffect(() => {
20
26
  let active = true
@@ -33,17 +39,166 @@ export default function HealthPage() {
33
39
 
34
40
  poll()
35
41
  const interval = setInterval(poll, 5000)
42
+
43
+ // Check for updates
44
+ const checkVersion = async () => {
45
+ try {
46
+ const data = await checkUpdate()
47
+ if (active) setUpdateInfo(data)
48
+ } catch (e) {
49
+ console.error('Failed to check updates', e)
50
+ }
51
+ }
52
+ checkVersion()
53
+
36
54
  return () => { active = false; clearInterval(interval) }
37
55
  }, [])
38
56
 
57
+ const handleStartUpdate = async () => {
58
+ setShowConfirmModal(false)
59
+ setIsUpdating(true)
60
+ setUpdateError('')
61
+
62
+ try {
63
+ await runUpdate()
64
+
65
+ // Keep polling health to check when it goes down and recovers
66
+ let serverDown = false
67
+ const pollInterval = setInterval(async () => {
68
+ try {
69
+ const res = await fetchHealth()
70
+ if (serverDown && res.status === 'healthy') {
71
+ clearInterval(pollInterval)
72
+ window.location.reload()
73
+ }
74
+ } catch {
75
+ // Connection failed = server is offline / restarting
76
+ serverDown = true
77
+ }
78
+ }, 2000)
79
+
80
+ } catch (e) {
81
+ setUpdateError((e as Error).message)
82
+ setIsUpdating(false)
83
+ }
84
+ }
85
+
39
86
  return (
40
- <div className="page">
41
- <div className="page-header">
42
- <h1>System Health</h1>
43
- <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
87
+ <div className="page" style={{ position: 'relative' }}>
88
+ {isUpdating && (
89
+ <div style={{
90
+ position: 'fixed',
91
+ inset: 0,
92
+ background: 'rgba(13, 17, 23, 0.9)',
93
+ zIndex: 1000,
94
+ display: 'flex',
95
+ flexDirection: 'column',
96
+ alignItems: 'center',
97
+ justifyContent: 'center',
98
+ backdropFilter: 'blur(8px)',
99
+ animation: 'fadeIn 0.3s ease'
100
+ }}>
101
+ <div className="spinner" style={{
102
+ width: 50,
103
+ height: 50,
104
+ border: '4px solid var(--border)',
105
+ borderTopColor: 'var(--accent)',
106
+ borderRadius: '50%',
107
+ animation: 'spin 1s linear infinite',
108
+ marginBottom: 20
109
+ }} />
110
+ <h2 style={{ color: 'var(--text-primary)', marginBottom: 8 }}>Updating Total Recall...</h2>
111
+ <p style={{ color: 'var(--text-muted)', fontSize: 14 }}>Applying git changes, installing npm modules, and restarting daemon.</p>
112
+ <p style={{ color: 'var(--accent)', fontSize: 13, marginTop: 10 }}>Please do not close this window.</p>
113
+ </div>
114
+ )}
115
+
116
+ {showConfirmModal && (
117
+ <div style={{
118
+ position: 'fixed',
119
+ inset: 0,
120
+ background: 'rgba(0, 0, 0, 0.7)',
121
+ zIndex: 900,
122
+ display: 'flex',
123
+ alignItems: 'center',
124
+ justifyContent: 'center',
125
+ backdropFilter: 'blur(4px)',
126
+ padding: 20
127
+ }}>
128
+ <div className="card" style={{
129
+ maxWidth: 450,
130
+ width: '100%',
131
+ backgroundColor: 'var(--bg-surface)',
132
+ border: '1px solid var(--border)',
133
+ borderRadius: 12,
134
+ padding: 24,
135
+ boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)'
136
+ }}>
137
+ <h3 style={{ marginBottom: 12, color: 'var(--text-primary)' }}>Confirm Core Self-Update</h3>
138
+ <p style={{ color: 'var(--text-muted)', marginBottom: 20, fontSize: 14, lineHeight: 1.5 }}>
139
+ This will pull the latest version of Total Recall ({updateInfo?.latestVersion}) from the git repository, reinstall dependencies, rebuild frontend bundles, and reboot the system kernel.
140
+ </p>
141
+ <div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
142
+ <button
143
+ className="btn btn-secondary"
144
+ onClick={() => setShowConfirmModal(false)}
145
+ style={{ padding: '8px 16px', borderRadius: 8 }}
146
+ >
147
+ Cancel
148
+ </button>
149
+ <button
150
+ className="btn btn-primary"
151
+ onClick={handleStartUpdate}
152
+ style={{ padding: '8px 16px', borderRadius: 8, background: 'var(--accent)' }}
153
+ >
154
+ Update Now
155
+ </button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ )}
160
+
161
+ <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
162
+ <div>
163
+ <h1>System Health</h1>
164
+ <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
165
+ </div>
166
+ {updateInfo?.updateAvailable && (
167
+ <div className="badge badge-success" style={{
168
+ display: 'flex',
169
+ alignItems: 'center',
170
+ gap: 12,
171
+ padding: '12px 18px',
172
+ borderRadius: 12,
173
+ background: 'rgba(63, 185, 80, 0.1)',
174
+ border: '1px solid rgba(63, 185, 80, 0.3)',
175
+ animation: 'pulse 2s infinite'
176
+ }}>
177
+ <span style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 600 }}>
178
+ New version available: {updateInfo.latestVersion}
179
+ </span>
180
+ <button
181
+ className="btn btn-primary"
182
+ onClick={() => setShowConfirmModal(true)}
183
+ style={{
184
+ fontSize: 12,
185
+ padding: '6px 12px',
186
+ borderRadius: 6,
187
+ backgroundColor: '#3fb950',
188
+ color: '#fff',
189
+ border: 'none',
190
+ cursor: 'pointer',
191
+ fontWeight: 600
192
+ }}
193
+ >
194
+ Update Now
195
+ </button>
196
+ </div>
197
+ )}
44
198
  </div>
45
199
 
46
200
  {error && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ {error}</div>}
201
+ {updateError && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ Update failed: {updateError}</div>}
47
202
 
48
203
  {health ? (
49
204
  <>
@@ -55,7 +210,9 @@ export default function HealthPage() {
55
210
  <div className="label">Status</div>
56
211
  </div>
57
212
  <div className="stat-card">
58
- <div className="value" style={{ color: 'var(--accent)' }}>{health.version}</div>
213
+ <div className="value" style={{ color: 'var(--accent)' }}>
214
+ {health.version} {updateInfo && !updateInfo.updateAvailable && <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>(Latest)</span>}
215
+ </div>
59
216
  <div className="label">Version</div>
60
217
  </div>
61
218
  <div className="stat-card">
@@ -76,6 +233,10 @@ export default function HealthPage() {
76
233
  <tr><th>Version</th><td>{health.version}</td></tr>
77
234
  <tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
78
235
  <tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
236
+ <tr><th>Daemon Status</th><td>{health.daemon || 'unknown'}</td></tr>
237
+ {health.cli_agents && (
238
+ <tr><th>CLI Agents</th><td>{health.cli_agents.join(', ')}</td></tr>
239
+ )}
79
240
  <tr><th>Polling</th><td>Every 5 seconds</td></tr>
80
241
  </tbody>
81
242
  </table>
@@ -86,6 +247,17 @@ export default function HealthPage() {
86
247
  {[1, 2, 3, 4].map(i => <div key={i} className="skeleton" style={{ height: 100 }} />)}
87
248
  </div>
88
249
  ) : null}
250
+
251
+ <style>{`
252
+ @keyframes spin {
253
+ 0% { transform: rotate(0deg); }
254
+ 100% { transform: rotate(360deg); }
255
+ }
256
+ @keyframes fadeIn {
257
+ from { opacity: 0; }
258
+ to { opacity: 1; }
259
+ }
260
+ `}</style>
89
261
  </div>
90
262
  )
91
263
  }
@@ -35,6 +35,7 @@ export default function SettingsPage() {
35
35
  if (!data.budget.budget) data.budget.budget = { daily_cap_usd: 5.0, weekly_cap_usd: 25.0, enabled: true };
36
36
  if (!data.brain) data.brain = {};
37
37
  if (!data.brain.preferred_agent) data.brain.preferred_agent = 'auto';
38
+ if (!data.secrets) data.secrets = {};
38
39
 
39
40
  setConfigData(data);
40
41
  } catch (err: unknown) {
@@ -152,6 +153,17 @@ export default function SettingsPage() {
152
153
  });
153
154
  };
154
155
 
156
+ const updateSecretsProp = (prop: string, value: string) => {
157
+ if (!configData) return;
158
+ setConfigData({
159
+ ...configData,
160
+ secrets: {
161
+ ...configData.secrets,
162
+ [prop]: value
163
+ }
164
+ });
165
+ };
166
+
155
167
  return (
156
168
  <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
157
169
  <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
@@ -209,7 +221,7 @@ export default function SettingsPage() {
209
221
  </div>
210
222
  ) : viewMode === 'visual' && configData ? (
211
223
  /* ==================== VISUAL CONTROL PANEL UI ==================== */
212
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24, flex: 1, minHeight: 0, overflowY: 'auto', paddingBottom: 24 }}>
224
+ <form onSubmit={(e) => { e.preventDefault(); handleSaveVisual(); }} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24, flex: 1, minHeight: 0, overflowY: 'auto', paddingBottom: 24 }}>
213
225
 
214
226
  {/* Card 1: AI Reasoning & Autonomy */}
215
227
  <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
@@ -537,7 +549,190 @@ export default function SettingsPage() {
537
549
  </div>
538
550
  </div>
539
551
 
540
- </div>
552
+ {/* Card 5: API Keys & Integrations */}
553
+ <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
554
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
555
+ <span style={{ fontSize: 20 }}>🔑</span>
556
+ <div>
557
+ <h3 style={{ fontSize: 15, fontWeight: 600 }}>API Keys & Integrations</h3>
558
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Manage access keys and tokens for AI models and search providers</p>
559
+ </div>
560
+ </div>
561
+
562
+ {/* Google Gemini API Key */}
563
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
564
+ <label htmlFor="google_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Google Gemini API Key</label>
565
+ <input
566
+ id="google_api_key"
567
+ type="password"
568
+ placeholder="AIzaSy..."
569
+ value={configData.secrets?.google_api_key || ''}
570
+ onChange={(e) => updateSecretsProp('google_api_key', e.target.value)}
571
+ style={{
572
+ background: 'var(--bg-tertiary)',
573
+ color: 'var(--text-primary)',
574
+ border: '1px solid var(--border)',
575
+ padding: '8px 12px',
576
+ borderRadius: 6,
577
+ outline: 'none',
578
+ fontSize: 13
579
+ }}
580
+ />
581
+ </div>
582
+
583
+ {/* Anthropic API Key */}
584
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
585
+ <label htmlFor="anthropic_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Anthropic API Key (Claude Code)</label>
586
+ <input
587
+ id="anthropic_api_key"
588
+ type="password"
589
+ placeholder="sk-ant-..."
590
+ value={configData.secrets?.anthropic_api_key || ''}
591
+ onChange={(e) => updateSecretsProp('anthropic_api_key', e.target.value)}
592
+ style={{
593
+ background: 'var(--bg-tertiary)',
594
+ color: 'var(--text-primary)',
595
+ border: '1px solid var(--border)',
596
+ padding: '8px 12px',
597
+ borderRadius: 6,
598
+ outline: 'none',
599
+ fontSize: 13
600
+ }}
601
+ />
602
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Configuring this allows Claude Code CLI to run without prompting for OAuth login.</span>
603
+ </div>
604
+
605
+ {/* OpenAI API Key */}
606
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
607
+ <label htmlFor="openai_api_key" style={{ fontSize: 13, fontWeight: 500 }}>OpenAI API Key (Codex)</label>
608
+ <input
609
+ id="openai_api_key"
610
+ type="password"
611
+ placeholder="sk-proj-..."
612
+ value={configData.secrets?.openai_api_key || ''}
613
+ onChange={(e) => updateSecretsProp('openai_api_key', e.target.value)}
614
+ style={{
615
+ background: 'var(--bg-tertiary)',
616
+ color: 'var(--text-primary)',
617
+ border: '1px solid var(--border)',
618
+ padding: '8px 12px',
619
+ borderRadius: 6,
620
+ outline: 'none',
621
+ fontSize: 13
622
+ }}
623
+ />
624
+ </div>
625
+
626
+ {/* Tavily API Key */}
627
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
628
+ <label htmlFor="tavily_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Tavily Search API Key</label>
629
+ <input
630
+ id="tavily_api_key"
631
+ type="password"
632
+ placeholder="tvly-..."
633
+ value={configData.secrets?.tavily_api_key || ''}
634
+ onChange={(e) => updateSecretsProp('tavily_api_key', e.target.value)}
635
+ style={{
636
+ background: 'var(--bg-tertiary)',
637
+ color: 'var(--text-primary)',
638
+ border: '1px solid var(--border)',
639
+ padding: '8px 12px',
640
+ borderRadius: 6,
641
+ outline: 'none',
642
+ fontSize: 13
643
+ }}
644
+ />
645
+ </div>
646
+
647
+ {/* Other Search Keys (Brave, Exa, Serper) */}
648
+ <details style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
649
+ <summary style={{ cursor: 'pointer', fontWeight: 500, outline: 'none', padding: '4px 0' }}>More Provider Keys (Brave, Exa, Serper, GitHub)</summary>
650
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12, paddingLeft: 8, borderLeft: '2px solid var(--border)' }}>
651
+ {/* Brave Search Key */}
652
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
653
+ <label htmlFor="brave_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Brave Search API Key</label>
654
+ <input
655
+ id="brave_api_key"
656
+ type="password"
657
+ value={configData.secrets?.brave_api_key || ''}
658
+ onChange={(e) => updateSecretsProp('brave_api_key', e.target.value)}
659
+ style={{
660
+ background: 'var(--bg-tertiary)',
661
+ color: 'var(--text-primary)',
662
+ border: '1px solid var(--border)',
663
+ padding: '8px 12px',
664
+ borderRadius: 6,
665
+ outline: 'none',
666
+ fontSize: 13
667
+ }}
668
+ />
669
+ </div>
670
+
671
+ {/* Exa Search Key */}
672
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
673
+ <label htmlFor="exa_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Exa API Key</label>
674
+ <input
675
+ id="exa_api_key"
676
+ type="password"
677
+ value={configData.secrets?.exa_api_key || ''}
678
+ onChange={(e) => updateSecretsProp('exa_api_key', e.target.value)}
679
+ style={{
680
+ background: 'var(--bg-tertiary)',
681
+ color: 'var(--text-primary)',
682
+ border: '1px solid var(--border)',
683
+ padding: '8px 12px',
684
+ borderRadius: 6,
685
+ outline: 'none',
686
+ fontSize: 13
687
+ }}
688
+ />
689
+ </div>
690
+
691
+ {/* Serper API Key */}
692
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
693
+ <label htmlFor="serper_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Serper (Google Search API) Key</label>
694
+ <input
695
+ id="serper_api_key"
696
+ type="password"
697
+ value={configData.secrets?.serper_api_key || ''}
698
+ onChange={(e) => updateSecretsProp('serper_api_key', e.target.value)}
699
+ style={{
700
+ background: 'var(--bg-tertiary)',
701
+ color: 'var(--text-primary)',
702
+ border: '1px solid var(--border)',
703
+ padding: '8px 12px',
704
+ borderRadius: 6,
705
+ outline: 'none',
706
+ fontSize: 13
707
+ }}
708
+ />
709
+ </div>
710
+
711
+ {/* GitHub Token */}
712
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
713
+ <label htmlFor="github_token" style={{ fontSize: 12, fontWeight: 500 }}>GitHub Personal Access Token</label>
714
+ <input
715
+ id="github_token"
716
+ type="password"
717
+ placeholder="ghp_..."
718
+ value={configData.secrets?.github_token || ''}
719
+ onChange={(e) => updateSecretsProp('github_token', e.target.value)}
720
+ style={{
721
+ background: 'var(--bg-tertiary)',
722
+ color: 'var(--text-primary)',
723
+ border: '1px solid var(--border)',
724
+ padding: '8px 12px',
725
+ borderRadius: 6,
726
+ outline: 'none',
727
+ fontSize: 13
728
+ }}
729
+ />
730
+ </div>
731
+ </div>
732
+ </details>
733
+ </div>
734
+
735
+ </form>
541
736
  ) : (
542
737
  /* ==================== RAW YAML EDITOR UI ==================== */
543
738
  <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
@@ -159,5 +159,21 @@ export interface ConfigJson {
159
159
  brain?: {
160
160
  preferred_agent?: string
161
161
  }
162
+ secrets?: {
163
+ google_api_key?: string
164
+ anthropic_api_key?: string
165
+ openai_api_key?: string
166
+ tavily_api_key?: string
167
+ brave_api_key?: string
168
+ exa_api_key?: string
169
+ serper_api_key?: string
170
+ github_token?: string
171
+ }
172
+ }
173
+
174
+ export interface UpdateCheckResult {
175
+ currentVersion: string
176
+ latestVersion: string
177
+ updateAvailable: boolean
162
178
  }
163
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1 @@
1
+ {"id":"0efa65c2-0231-4267-a304-e0285e50011e","topic":"Total Recall repository improvement research: SSSS validated write pipeline, filesystem-native memory engines, local-first semantic indexing, Express 5 API hardening, 2026 agent memory architecture patterns","status":"pending","priority":"high","notes":"Queued from repo-wide analysis request on 2026-06-01. Focus on current best practices and meta-patterns for filesystem-native memory OS, validated operation envelopes, append-only logs, cache invalidation, research queue architecture, and secure local-first AI tools.","node_slug":null,"research_phase":"acquisition","created_at":"2026-06-01T06:07:40.102Z","updated_at":"2026-06-01T06:07:40.126Z","completed_at":null,"summary":{"id":"0efa65c2-0231-4267-a304-e0285e50011e","topic":"Total Recall repository improvement research: SSSS validated write pipeline, filesystem-native memory engines, local-first semantic indexing, Express 5 API hardening, 2026 agent memory architecture patterns","status":"pending","phase":"acquisition","last_updated":"2026-06-01T06:07:40.126Z","conclusions":[],"ongoing_directions":["Research project is enqueued / in progress in phase: acquisition"],"sources_count":0}}
@@ -254,6 +254,27 @@ export async function callLocalRuntime(prompt, system, config) {
254
254
 
255
255
  let lastError;
256
256
 
257
+ // Load dynamic secrets from secrets.enc to ensure changes take effect immediately without server restart
258
+ let dynamicSecrets = {};
259
+ try {
260
+ const pathsToCheck = [
261
+ path.join(process.cwd(), '.agent', 'secrets.enc'),
262
+ path.join(agentDir, 'secrets.enc'),
263
+ path.join(os.homedir(), '.agent', 'skills', 'total-recall', 'config', 'secrets.enc'),
264
+ path.join(agentDir, 'skills', 'total-recall', 'config', 'secrets.enc'),
265
+ ];
266
+ for (const p of pathsToCheck) {
267
+ if (fs.existsSync(p)) {
268
+ try {
269
+ dynamicSecrets = JSON.parse(fs.readFileSync(p, 'utf8') || '{}');
270
+ if (Object.keys(dynamicSecrets).length > 0) {
271
+ break;
272
+ }
273
+ } catch {}
274
+ }
275
+ }
276
+ } catch {}
277
+
257
278
  // Try agents in priority order with retry budget
258
279
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
259
280
  const agent = attempt === 0 ? resolveAgent(config) : resolveAgent({
@@ -289,13 +310,15 @@ export async function callLocalRuntime(prompt, system, config) {
289
310
  maxBuffer: 10 * 1024 * 1024,
290
311
  env: {
291
312
  ...process.env,
292
- GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || googleApiKey,
293
- GEMINI_API_KEY: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || googleApiKey,
294
- TAVILY_API_KEY: process.env.TAVILY_API_KEY || tavilyApiKey,
295
- BRAVE_API_KEY: process.env.BRAVE_API_KEY || braveApiKey,
296
- EXA_API_KEY: process.env.EXA_API_KEY || exaApiKey,
297
- SERPER_API_KEY: process.env.SERPER_API_KEY || serperApiKey,
298
- GITHUB_TOKEN: process.env.GITHUB_TOKEN || githubToken,
313
+ GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || dynamicSecrets.google_api_key || googleApiKey,
314
+ GEMINI_API_KEY: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || dynamicSecrets.google_api_key || googleApiKey,
315
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || dynamicSecrets.anthropic_api_key,
316
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY || dynamicSecrets.openai_api_key,
317
+ TAVILY_API_KEY: process.env.TAVILY_API_KEY || dynamicSecrets.tavily_api_key || tavilyApiKey,
318
+ BRAVE_API_KEY: process.env.BRAVE_API_KEY || dynamicSecrets.brave_api_key || braveApiKey,
319
+ EXA_API_KEY: process.env.EXA_API_KEY || dynamicSecrets.exa_api_key || exaApiKey,
320
+ SERPER_API_KEY: process.env.SERPER_API_KEY || dynamicSecrets.serper_api_key || serperApiKey,
321
+ GITHUB_TOKEN: process.env.GITHUB_TOKEN || dynamicSecrets.github_token || githubToken,
299
322
  },
300
323
  cwd: process.cwd(),
301
324
  });
@@ -312,6 +312,458 @@ export const SubQuerySchema = z.object({
312
312
  reasoning: z.string().describe("Explanation for why these sub-queries are necessary")
313
313
  });
314
314
 
315
+ // ─── Document Primitives (§5.1 of the SSSS spec) ───────────────────────────
316
+
317
+ export const ContactSchema = z.object({
318
+ type: z.literal('contact'),
319
+ name: z.string(),
320
+ first_name: z.string().optional().nullable(),
321
+ last_name: z.string().optional().nullable(),
322
+ email: z.string().optional().nullable(),
323
+ phone: z.string().optional().nullable(),
324
+ company: z.string().optional().nullable(),
325
+ job_title: z.string().optional().nullable(),
326
+ tags: z.array(z.unknown()).optional().nullable(),
327
+ notes: z.string().optional().nullable(),
328
+ source: z.string().optional().nullable(),
329
+ contact_type: z.string().optional().nullable(),
330
+ priority: z.string().optional().nullable(),
331
+ relationship_strength: z.number().optional().nullable(),
332
+ custom_fields: z.record(z.unknown()).optional().nullable(),
333
+ db_id: z.string().optional().nullable(),
334
+ owner_user_id: z.string().optional().nullable(),
335
+ workspace_id: z.string().optional().nullable(),
336
+ });
337
+
338
+ export const CompanySchema = z.object({
339
+ type: z.literal('company'),
340
+ name: z.string(),
341
+ domain: z.string().optional().nullable(),
342
+ industry: z.string().optional().nullable(),
343
+ website: z.string().optional().nullable(),
344
+ db_id: z.string().optional().nullable(),
345
+ workspace_id: z.string().optional().nullable(),
346
+ });
347
+
348
+ export const DealSchema = z.object({
349
+ type: z.literal('deal'),
350
+ name: z.string(),
351
+ value: z.number(),
352
+ stage: z.string(),
353
+ contact_id: z.string().optional().nullable(),
354
+ company_id: z.string().optional().nullable(),
355
+ currency: z.string().optional().nullable(),
356
+ probability: z.number().optional().nullable(),
357
+ expected_close_date: z.string().optional().nullable(),
358
+ is_archived: z.boolean().optional().nullable(),
359
+ description: z.string().optional().nullable(),
360
+ source: z.string().optional().nullable(),
361
+ tags: z.array(z.unknown()).optional().nullable(),
362
+ custom_fields: z.record(z.unknown()).optional().nullable(),
363
+ db_id: z.string().optional().nullable(),
364
+ workspace_id: z.string().optional().nullable(),
365
+ });
366
+
367
+ export const BrandSchema = z.object({
368
+ type: z.literal('brand'),
369
+ name: z.string(),
370
+ version: z.string().optional().nullable(),
371
+ description: z.string().optional().nullable(),
372
+ colors: z.record(z.unknown()).optional().nullable(),
373
+ typography: z.record(z.unknown()).optional().nullable(),
374
+ rounded: z.record(z.unknown()).optional().nullable(),
375
+ spacing: z.record(z.unknown()).optional().nullable(),
376
+ components: z.record(z.unknown()).optional().nullable(),
377
+ vibe: z.string().optional().nullable(),
378
+ locale: z.string().optional().nullable(),
379
+ assets: z.record(z.unknown()).optional().nullable(),
380
+ brand_voice: z.array(z.unknown()).optional().nullable(),
381
+ accessibility_palettes: z.record(z.unknown()).optional().nullable(),
382
+ tagline: z.string().optional().nullable(),
383
+ });
384
+
385
+ export const DeploymentSchema = z.object({
386
+ type: z.literal('deployment'),
387
+ name: z.string(),
388
+ slug: z.string(),
389
+ status: z.string().optional().nullable(),
390
+ runtime_url: z.string().optional().nullable(),
391
+ mcp_config: z.record(z.unknown()).optional().nullable(),
392
+ container_settings: z.record(z.unknown()).optional().nullable(),
393
+ workspace_id: z.string().optional().nullable(),
394
+ db_id: z.string().optional().nullable(),
395
+ });
396
+
397
+ export const ListingSchema = z.object({
398
+ type: z.literal('listing'),
399
+ name: z.string(),
400
+ pricing_model: z.string(),
401
+ credits_charge_rate: z.number().optional().nullable(),
402
+ connected_stripe_account: z.string().optional().nullable(),
403
+ is_published: z.boolean().optional().nullable(),
404
+ workspace_id: z.string().optional().nullable(),
405
+ db_id: z.string().optional().nullable(),
406
+ });
407
+
408
+ export const KnowledgeSchema = z.object({
409
+ type: z.literal('knowledge'),
410
+ name: z.string(),
411
+ slug: z.string(),
412
+ description: z.string().optional().nullable(),
413
+ category: z.string().optional().nullable(),
414
+ is_public: z.boolean().optional().nullable(),
415
+ is_active: z.boolean().optional().nullable(),
416
+ is_system_managed: z.boolean().optional().nullable(),
417
+ shared_across_workspaces: z.boolean().optional().nullable(),
418
+ settings: z.record(z.unknown()).optional().nullable(),
419
+ workspace_id: z.string().optional().nullable(),
420
+ db_id: z.string().optional().nullable(),
421
+ user_id: z.string().optional().nullable(),
422
+ });
423
+
424
+ export const KnowledgeSourceSchema = z.object({
425
+ type: z.literal('knowledge_source'),
426
+ title: z.string(),
427
+ knowledge_base_slug: z.string(),
428
+ content: z.string().optional().nullable(),
429
+ content_type: z.string().optional().nullable(),
430
+ source_url: z.string().optional().nullable(),
431
+ metadata: z.record(z.unknown()).optional().nullable(),
432
+ embedding_vector: z.array(z.unknown()).optional().nullable(),
433
+ workspace_id: z.string().optional().nullable(),
434
+ db_id: z.string().optional().nullable(),
435
+ });
436
+
437
+ export const CalendarEventSchema = z.object({
438
+ type: z.literal('calendar_event'),
439
+ title: z.string().optional().nullable(),
440
+ description: z.string().optional().nullable(),
441
+ start_time: z.string(),
442
+ end_time: z.string().optional().nullable(),
443
+ timezone: z.string().optional().nullable(),
444
+ all_day: z.boolean().optional().nullable(),
445
+ event_type: z.string(),
446
+ calendar_id: z.string().optional().nullable(),
447
+ location: z.string().optional().nullable(),
448
+ meeting_url: z.string().optional().nullable(),
449
+ attendees: z.array(z.unknown()).optional().nullable(),
450
+ location_context: z.record(z.unknown()).optional().nullable(),
451
+ db_id: z.string().optional().nullable(),
452
+ workspace_id: z.string().optional().nullable(),
453
+ user_id: z.string().optional().nullable(),
454
+ created_at: z.string().optional().nullable(),
455
+ updated_at: z.string().optional().nullable(),
456
+ });
457
+
458
+ export const EventSchema = z.object({
459
+ type: z.literal('event'),
460
+ event_type: z.string(),
461
+ thread_id: z.string().optional().nullable(),
462
+ turn_index: z.number().optional().nullable(),
463
+ rating: z.number().optional().nullable(),
464
+ comment: z.string().optional().nullable(),
465
+ comment_lang: z.string().optional().nullable(),
466
+ timestamp: z.string().optional().nullable(),
467
+ });
468
+
469
+ export const LanguageConventionSchema = z.object({
470
+ type: z.literal('language_convention'),
471
+ locale: z.string(),
472
+ language_name: z.string(),
473
+ keys: z.record(z.unknown()),
474
+ });
475
+
476
+ export const ProfileSchema = z.object({
477
+ type: z.literal('profile'),
478
+ user_id: z.string(),
479
+ locale: z.string(),
480
+ display_name: z.string().optional().nullable(),
481
+ timezone: z.string().optional().nullable(),
482
+ workspaces: z.array(z.unknown()).optional().nullable(),
483
+ notification_preferences: z.record(z.unknown()).optional().nullable(),
484
+ });
485
+
486
+ export const SharingGroupSchema = z.object({
487
+ type: z.literal('sharing_group'),
488
+ name: z.string(),
489
+ color: z.string(),
490
+ description: z.string().optional().nullable(),
491
+ owner_user_id: z.string(),
492
+ members: z.array(z.unknown()).optional().nullable(),
493
+ share_credits: z.boolean().optional().nullable(),
494
+ share_skills: z.boolean().optional().nullable(),
495
+ share_assistants: z.boolean().optional().nullable(),
496
+ share_email_accounts: z.boolean().optional().nullable(),
497
+ share_email_domains: z.boolean().optional().nullable(),
498
+ share_phone_numbers: z.boolean().optional().nullable(),
499
+ share_api_keys: z.boolean().optional().nullable(),
500
+ share_knowledge_base: z.boolean().optional().nullable(),
501
+ share_credentials: z.boolean().optional().nullable(),
502
+ share_team_members: z.boolean().optional().nullable(),
503
+ share_workflows: z.boolean().optional().nullable(),
504
+ share_templates: z.boolean().optional().nullable(),
505
+ share_branding: z.boolean().optional().nullable(),
506
+ share_crm: z.boolean().optional().nullable(),
507
+ share_integrations: z.boolean().optional().nullable(),
508
+ share_notifications: z.boolean().optional().nullable(),
509
+ share_deployments: z.boolean().optional().nullable(),
510
+ });
511
+
512
+ export const PageSchema = z.object({
513
+ type: z.literal('page'),
514
+ slug: z.string(),
515
+ name: z.string(),
516
+ icon: z.string().optional().nullable(),
517
+ layout: z.string().optional().nullable(),
518
+ sandbox_entry: z.string(),
519
+ is_active: z.boolean().optional().nullable(),
520
+ workspace_id: z.string().optional().nullable(),
521
+ db_id: z.string().optional().nullable(),
522
+ });
523
+
524
+ export const VoicemailSchema = z.object({
525
+ type: z.literal('voicemail'),
526
+ mailbox_id: z.string(),
527
+ caller_id: z.string().optional().nullable(),
528
+ caller_name: z.string().optional().nullable(),
529
+ duration_seconds: z.number(),
530
+ recording_url: z.string(),
531
+ recording_path: z.string().optional().nullable(),
532
+ recording_size_bytes: z.number().optional().nullable(),
533
+ transcript: z.string().optional().nullable(),
534
+ transcript_confidence: z.number().optional().nullable(),
535
+ transcription_status: z.string().optional().nullable(),
536
+ is_urgent: z.boolean().optional().nullable(),
537
+ is_read: z.boolean().optional().nullable(),
538
+ is_deleted: z.boolean().optional().nullable(),
539
+ folder: z.string().optional().nullable(),
540
+ callback_number: z.string().optional().nullable(),
541
+ inbox_item_id: z.string().optional().nullable(),
542
+ workspace_id: z.string().optional().nullable(),
543
+ user_id: z.string().optional().nullable(),
544
+ db_id: z.string().optional().nullable(),
545
+ created_at: z.string().optional().nullable(),
546
+ });
547
+
548
+ export const PersonalizationProfileSchema = z.object({
549
+ type: z.literal('personalization_profile'),
550
+ name: z.string(),
551
+ slug: z.string(),
552
+ user_id: z.string(),
553
+ profile_type: z.string(),
554
+ description: z.string().optional().nullable(),
555
+ personalization_text: z.string().optional().nullable(),
556
+ is_default: z.boolean().optional().nullable(),
557
+ is_active: z.boolean().optional().nullable(),
558
+ scope: z.string().optional().nullable(),
559
+ workspace_id: z.string().optional().nullable(),
560
+ attached_files: z.array(z.unknown()).optional().nullable(),
561
+ active_times: z.array(z.unknown()).optional().nullable(),
562
+ });
563
+
564
+ export const AccountAssistantSchema = z.object({
565
+ type: z.literal('account_assistant'),
566
+ name: z.string(),
567
+ user_id: z.string(),
568
+ scope: z.string(),
569
+ enabled: z.boolean(),
570
+ description: z.string().optional().nullable(),
571
+ system_message: z.string().optional().nullable(),
572
+ avatar_url: z.string().optional().nullable(),
573
+ color_theme: z.string().optional().nullable(),
574
+ voice_id: z.string().optional().nullable(),
575
+ is_receptionist_enabled: z.boolean().optional().nullable(),
576
+ auto_answer_calls: z.boolean().optional().nullable(),
577
+ verification_pin: z.string().optional().nullable(),
578
+ greeting_known: z.string().optional().nullable(),
579
+ greeting_unknown: z.string().optional().nullable(),
580
+ routing_rules: z.array(z.unknown()).optional().nullable(),
581
+ enabled_tabs: z.array(z.unknown()).optional().nullable(),
582
+ });
583
+
584
+ export const AccountMemorySchema = z.object({
585
+ type: z.literal('account_memory'),
586
+ title: z.string(),
587
+ user_id: z.string(),
588
+ category: z.string(),
589
+ confidence: z.number().optional().nullable(),
590
+ source: z.string().optional().nullable(),
591
+ });
592
+
593
+ export const AccountWorkflowSchema = z.object({
594
+ type: z.literal('account_workflow'),
595
+ name: z.string(),
596
+ slug: z.string(),
597
+ user_id: z.string(),
598
+ scope: z.string(),
599
+ description: z.string().optional().nullable(),
600
+ enabled: z.boolean().optional().nullable(),
601
+ trigger_type: z.string().optional().nullable(),
602
+ trigger_config: z.record(z.unknown()).optional().nullable(),
603
+ steps: z.array(z.unknown()).optional().nullable(),
604
+ actions: z.array(z.unknown()).optional().nullable(),
605
+ target_workspaces: z.array(z.unknown()).optional().nullable(),
606
+ last_run: z.string().optional().nullable(),
607
+ run_count: z.number().optional().nullable(),
608
+ });
609
+
610
+ export const WorkspaceTransferSchema = z.object({
611
+ type: z.literal('workspace_transfer'),
612
+ transfer_id: z.string(),
613
+ source_user_id: z.string(),
614
+ target_email: z.string(),
615
+ workspace_id: z.string(),
616
+ status: z.string(),
617
+ initiated_at: z.string(),
618
+ target_user_id: z.string().optional().nullable(),
619
+ workspace_name: z.string().optional().nullable(),
620
+ resolved_at: z.string().optional().nullable(),
621
+ expires_at: z.string().optional().nullable(),
622
+ });
623
+
624
+ export const ExtensionSchema = z.object({
625
+ type: z.literal('extension'),
626
+ extension: z.string(),
627
+ display_name: z.string(),
628
+ owner_user_id: z.string().optional().nullable(),
629
+ workspace_id: z.string().optional().nullable(),
630
+ assistant_id: z.string().optional().nullable(),
631
+ status: z.string(),
632
+ voicemail_enabled: z.boolean().optional().nullable(),
633
+ metadata: z.record(z.unknown()).optional().nullable(),
634
+ });
635
+
636
+ export const PhoneNumberSchema = z.object({
637
+ type: z.literal('phone_number'),
638
+ phone_number: z.string(),
639
+ provider: z.string(),
640
+ provider_number_id: z.string().optional().nullable(),
641
+ receptionist_id: z.string().optional().nullable(),
642
+ status: z.string(),
643
+ workspace_id: z.string().optional().nullable(),
644
+ user_id: z.string().optional().nullable(),
645
+ });
646
+
647
+ export const WorkspaceSchema = z.object({
648
+ type: z.literal('workspace'),
649
+ name: z.string(),
650
+ slug: z.string().optional().nullable(),
651
+ description: z.string().optional().nullable(),
652
+ business_type: z.string().optional().nullable(),
653
+ domain: z.string().optional().nullable(),
654
+ provisioning_mode: z.string().optional().nullable(),
655
+ canonical_version: z.number().optional().nullable(),
656
+ content_hash: z.string().optional().nullable(),
657
+ brand_ref: z.string().optional().nullable(),
658
+ page_refs: z.array(z.unknown()).optional().nullable(),
659
+ assistant_refs: z.array(z.unknown()).optional().nullable(),
660
+ workflow_refs: z.array(z.unknown()).optional().nullable(),
661
+ knowledge_refs: z.array(z.unknown()).optional().nullable(),
662
+ commerce_ref: z.string().optional().nullable(),
663
+ cms_refs: z.array(z.unknown()).optional().nullable(),
664
+ social_refs: z.array(z.unknown()).optional().nullable(),
665
+ ticketing_refs: z.array(z.unknown()).optional().nullable(),
666
+ comms_refs: z.array(z.unknown()).optional().nullable(),
667
+ economy: z.record(z.unknown()).optional().nullable(),
668
+ demo_fixtures_ref: z.string().optional().nullable(),
669
+ capabilities: z.array(z.unknown()).optional().nullable(),
670
+ workspace_id: z.string().optional().nullable(),
671
+ db_id: z.string().optional().nullable(),
672
+ created_at: z.string().optional().nullable(),
673
+ updated_at: z.string().optional().nullable(),
674
+ });
675
+
676
+ export const CommerceCatalogSchema = z.object({
677
+ type: z.literal('commerce_catalog'),
678
+ name: z.string(),
679
+ currency: z.string().optional().nullable(),
680
+ tax_behavior: z.string().optional().nullable(),
681
+ shipping_zones: z.array(z.unknown()).optional().nullable(),
682
+ collections: z.array(z.unknown()).optional().nullable(),
683
+ fulfillment: z.record(z.unknown()).optional().nullable(),
684
+ stripe_account_ref: z.string().optional().nullable(),
685
+ workspace_id: z.string().optional().nullable(),
686
+ db_id: z.string().optional().nullable(),
687
+ });
688
+
689
+ export const ProductSchema = z.object({
690
+ type: z.literal('product'),
691
+ name: z.string(),
692
+ slug: z.string(),
693
+ description: z.string().optional().nullable(),
694
+ product_type: z.string().optional().nullable(),
695
+ status: z.string().optional().nullable(),
696
+ variants: z.array(z.unknown()).optional().nullable(),
697
+ prices: z.array(z.unknown()).optional().nullable(),
698
+ subscription: z.record(z.unknown()).optional().nullable(),
699
+ collection: z.string().optional().nullable(),
700
+ media_refs: z.array(z.unknown()).optional().nullable(),
701
+ inventory: z.record(z.unknown()).optional().nullable(),
702
+ metadata: z.record(z.unknown()).optional().nullable(),
703
+ workspace_id: z.string().optional().nullable(),
704
+ db_id: z.string().optional().nullable(),
705
+ });
706
+
707
+ export const CmsCollectionSchema = z.object({
708
+ type: z.literal('cms_collection'),
709
+ name: z.string(),
710
+ slug: z.string(),
711
+ description: z.string().optional().nullable(),
712
+ fields: z.array(z.unknown()).optional().nullable(),
713
+ ordering: z.string().optional().nullable(),
714
+ is_published: z.boolean().optional().nullable(),
715
+ workspace_id: z.string().optional().nullable(),
716
+ db_id: z.string().optional().nullable(),
717
+ });
718
+
719
+ export const CmsEntrySchema = z.object({
720
+ type: z.literal('cms_entry'),
721
+ title: z.string(),
722
+ slug: z.string(),
723
+ collection_slug: z.string(),
724
+ status: z.string().optional().nullable(),
725
+ publish_at: z.string().optional().nullable(),
726
+ order: z.number().optional().nullable(),
727
+ fields: z.record(z.unknown()).optional().nullable(),
728
+ media_refs: z.array(z.unknown()).optional().nullable(),
729
+ excerpt: z.string().optional().nullable(),
730
+ seo: z.record(z.unknown()).optional().nullable(),
731
+ workspace_id: z.string().optional().nullable(),
732
+ db_id: z.string().optional().nullable(),
733
+ });
734
+
735
+ export const SocialSchema = z.object({
736
+ type: z.literal('social'),
737
+ name: z.string(),
738
+ platform: z.string().optional().nullable(),
739
+ handle: z.string().optional().nullable(),
740
+ connection_ref: z.string().optional().nullable(),
741
+ posting_calendar: z.array(z.unknown()).optional().nullable(),
742
+ creative_refs: z.array(z.unknown()).optional().nullable(),
743
+ status: z.string().optional().nullable(),
744
+ workspace_id: z.string().optional().nullable(),
745
+ db_id: z.string().optional().nullable(),
746
+ });
747
+
748
+ export const TicketedEventSchema = z.object({
749
+ type: z.literal('ticketed_event'),
750
+ name: z.string(),
751
+ slug: z.string(),
752
+ start_time: z.string(),
753
+ description: z.string().optional().nullable(),
754
+ status: z.string().optional().nullable(),
755
+ end_time: z.string().optional().nullable(),
756
+ timezone: z.string().optional().nullable(),
757
+ venue: z.record(z.unknown()).optional().nullable(),
758
+ sessions: z.array(z.unknown()).optional().nullable(),
759
+ ticket_tiers: z.array(z.unknown()).optional().nullable(),
760
+ capacity: z.number().optional().nullable(),
761
+ stripe_account_ref: z.string().optional().nullable(),
762
+ media_refs: z.array(z.unknown()).optional().nullable(),
763
+ workspace_id: z.string().optional().nullable(),
764
+ db_id: z.string().optional().nullable(),
765
+ });
766
+
315
767
  // ─── Schema Registry (§5 of the SSSS spec) ─────────────────────────────────
316
768
 
317
769
  /** Map from SSSS `type` value to its Zod schema. Used by the operation validator. */
@@ -330,4 +782,33 @@ export const SSSS_SCHEMAS = {
330
782
  'schema-proposal': SchemaProposalSchema,
331
783
  migration: MigrationSchema,
332
784
  release: ReleaseSchema,
785
+ contact: ContactSchema,
786
+ company: CompanySchema,
787
+ deal: DealSchema,
788
+ brand: BrandSchema,
789
+ deployment: DeploymentSchema,
790
+ listing: ListingSchema,
791
+ knowledge: KnowledgeSchema,
792
+ knowledge_source: KnowledgeSourceSchema,
793
+ calendar_event: CalendarEventSchema,
794
+ event: EventSchema,
795
+ language_convention: LanguageConventionSchema,
796
+ profile: ProfileSchema,
797
+ sharing_group: SharingGroupSchema,
798
+ page: PageSchema,
799
+ voicemail: VoicemailSchema,
800
+ personalization_profile: PersonalizationProfileSchema,
801
+ account_assistant: AccountAssistantSchema,
802
+ account_memory: AccountMemorySchema,
803
+ account_workflow: AccountWorkflowSchema,
804
+ workspace_transfer: WorkspaceTransferSchema,
805
+ extension: ExtensionSchema,
806
+ phone_number: PhoneNumberSchema,
807
+ workspace: WorkspaceSchema,
808
+ commerce_catalog: CommerceCatalogSchema,
809
+ product: ProductSchema,
810
+ cms_collection: CmsCollectionSchema,
811
+ cms_entry: CmsEntrySchema,
812
+ social: SocialSchema,
813
+ ticketed_event: TicketedEventSchema,
333
814
  };
@@ -1371,10 +1371,12 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
1371
1371
  const securityPath = path.join(CONFIG_DIR, 'security.yml');
1372
1372
  const budgetPath = path.join(CONFIG_DIR, 'budget.yml');
1373
1373
  const brainPath = path.join(CONFIG_DIR, 'brain.json');
1374
+ const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
1374
1375
 
1375
1376
  let security = {};
1376
1377
  let budget = {};
1377
1378
  let brain = {};
1379
+ let secrets = {};
1378
1380
 
1379
1381
  if (fs.existsSync(securityPath)) {
1380
1382
  try {
@@ -1391,6 +1393,11 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
1391
1393
  brain = JSON.parse(fs.readFileSync(brainPath, 'utf8')) || {};
1392
1394
  } catch {}
1393
1395
  }
1396
+ if (fs.existsSync(secretsPath)) {
1397
+ try {
1398
+ secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
1399
+ } catch {}
1400
+ }
1394
1401
 
1395
1402
  const safeBrain = { ...brain };
1396
1403
  if (safeBrain.token) {
@@ -1398,16 +1405,25 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
1398
1405
  delete safeBrain.token;
1399
1406
  }
1400
1407
 
1401
- res.json({ security, budget, brain: safeBrain });
1408
+ const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
1409
+ const safeSecrets = {};
1410
+ for (const key of allowedKeys) {
1411
+ if (secrets[key] !== undefined) {
1412
+ safeSecrets[key] = secrets[key];
1413
+ }
1414
+ }
1415
+
1416
+ res.json({ security, budget, brain: safeBrain, secrets: safeSecrets });
1402
1417
  } catch (err) { serverError(res, err); }
1403
1418
  });
1404
1419
 
1405
1420
  router.post('/api/config-json', requireAuth, requireScope('config:write'), (req, res) => {
1406
1421
  try {
1407
- const { security, budget, brain } = req.body;
1422
+ const { security, budget, brain, secrets } = req.body;
1408
1423
  const securityPath = path.join(CONFIG_DIR, 'security.yml');
1409
1424
  const budgetPath = path.join(CONFIG_DIR, 'budget.yml');
1410
1425
  const brainPath = path.join(CONFIG_DIR, 'brain.json');
1426
+ const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
1411
1427
 
1412
1428
  if (!fs.existsSync(CONFIG_DIR)) {
1413
1429
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -1433,6 +1449,25 @@ router.post('/api/config-json', requireAuth, requireScope('config:write'), (req,
1433
1449
  delete nextBrain.has_token;
1434
1450
  fs.writeFileSync(brainPath, JSON.stringify(nextBrain, null, 2), { encoding: 'utf8', mode: 0o600 });
1435
1451
  }
1452
+ if (secrets) {
1453
+ let existingSecrets = {};
1454
+ if (fs.existsSync(secretsPath)) {
1455
+ try {
1456
+ existingSecrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
1457
+ } catch {}
1458
+ }
1459
+ const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
1460
+ for (const key of allowedKeys) {
1461
+ if (secrets[key] !== undefined) {
1462
+ if (secrets[key] === '') {
1463
+ delete existingSecrets[key];
1464
+ } else {
1465
+ existingSecrets[key] = secrets[key];
1466
+ }
1467
+ }
1468
+ }
1469
+ fs.writeFileSync(secretsPath, JSON.stringify(existingSecrets, null, 2), { encoding: 'utf8', mode: 0o600 });
1470
+ }
1436
1471
 
1437
1472
  res.json({ success: true });
1438
1473
  } catch (err) { serverError(res, err); }
@@ -1960,4 +1995,76 @@ function getLastModified(filePath) {
1960
1995
  return null;
1961
1996
  }
1962
1997
 
1998
+ /**
1999
+ * GET /api/update/check
2000
+ */
2001
+ router.get('/api/update/check', requireAuth, async (req, res) => {
2002
+ try {
2003
+ const localPkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
2004
+ const currentVersion = localPkg.version || '0.0.0';
2005
+
2006
+ const registryRes = await fetch('https://registry.npmjs.org/total-recall-brain/latest');
2007
+ if (!registryRes.ok) {
2008
+ throw new Error(`Failed to fetch latest version from npm: ${registryRes.status}`);
2009
+ }
2010
+ const registryData = await registryRes.json();
2011
+ const latestVersion = registryData.version || '0.0.0';
2012
+
2013
+ const updateAvailable = latestVersion !== currentVersion;
2014
+
2015
+ res.json({
2016
+ currentVersion,
2017
+ latestVersion,
2018
+ updateAvailable
2019
+ });
2020
+ } catch (err) {
2021
+ serverError(res, err);
2022
+ }
2023
+ });
2024
+
2025
+ /**
2026
+ * POST /api/update/run
2027
+ */
2028
+ router.post('/api/update/run', requireAuth, async (req, res) => {
2029
+ try {
2030
+ res.json({ success: true, message: 'Update started. Server is updating and will restart shortly.' });
2031
+
2032
+ setImmediate(async () => {
2033
+ logger.info('update', 'Starting auto-update process...');
2034
+ try {
2035
+ const { exec } = await import('node:child_process');
2036
+
2037
+ const runCmd = (cmd, cwd) => new Promise((resolve, reject) => {
2038
+ logger.info('update', `Running: ${cmd} in ${cwd}`);
2039
+ exec(cmd, { cwd }, (err, stdout, stderr) => {
2040
+ if (err) {
2041
+ logger.error('update', `Command failed: ${cmd}. Error: ${err.message}`);
2042
+ return reject(err);
2043
+ }
2044
+ resolve({ stdout, stderr });
2045
+ });
2046
+ });
2047
+
2048
+ // 1. git pull
2049
+ await runCmd('git pull', ROOT);
2050
+ // 2. npm install
2051
+ await runCmd('npm install', ROOT);
2052
+ // 3. build/install frontend if package.json exists
2053
+ const frontendPath = path.join(ROOT, 'frontend');
2054
+ if (fs.existsSync(path.join(frontendPath, 'package.json'))) {
2055
+ await runCmd('npm install', frontendPath);
2056
+ await runCmd('npm run build', frontendPath);
2057
+ }
2058
+
2059
+ logger.info('update', 'Update successfully applied. Restarting server...');
2060
+ process.exit(0);
2061
+ } catch (err) {
2062
+ logger.error('update', `Auto-update failed: ${err.message}`);
2063
+ }
2064
+ });
2065
+ } catch (err) {
2066
+ serverError(res, err);
2067
+ }
2068
+ });
2069
+
1963
2070
  export { router as restRouter };
@@ -295,6 +295,14 @@
295
295
  "method": "GET",
296
296
  "path": "/api/tts/status"
297
297
  },
298
+ {
299
+ "method": "GET",
300
+ "path": "/api/update/check"
301
+ },
302
+ {
303
+ "method": "POST",
304
+ "path": "/api/update/run"
305
+ },
298
306
  {
299
307
  "method": "GET",
300
308
  "path": "/api/usage"