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.
- package/bin/antigravity.mjs +19 -8
- package/frontend/src/api.ts +20 -0
- package/frontend/src/pages/HealthPage.tsx +179 -7
- package/frontend/src/pages/SettingsPage.tsx +197 -2
- package/frontend/src/types.ts +16 -0
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/research-queue.jsonl +1 -0
- package/src/core/runtime.mjs +30 -7
- package/src/core/schema.mjs +481 -0
- package/src/server/rest.mjs +109 -2
- package/src/server/route-manifest.json +8 -0
package/bin/antigravity.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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 }]
|
package/frontend/src/api.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
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)' }}>
|
|
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
|
-
<
|
|
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
|
-
|
|
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 }}>
|
package/frontend/src/types.ts
CHANGED
|
@@ -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
|
@@ -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}}
|
package/src/core/runtime.mjs
CHANGED
|
@@ -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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
});
|
package/src/core/schema.mjs
CHANGED
|
@@ -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
|
};
|
package/src/server/rest.mjs
CHANGED
|
@@ -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
|
-
|
|
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"
|