total-recall-brain 3.13.2 → 3.14.3
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/total-recall.mjs +3 -0
- package/frontend/src/App.tsx +8 -7
- package/frontend/src/api.ts +25 -0
- package/frontend/src/components/UsageChart.tsx +1 -1
- package/frontend/src/pages/ApiKeysPage.tsx +118 -42
- package/frontend/src/pages/ChatPage.tsx +1 -1
- package/frontend/src/pages/DesignDocsPage.tsx +28 -21
- package/frontend/src/pages/FilesPage.tsx +6 -1
- package/frontend/src/pages/GraphPage.tsx +5 -1
- package/frontend/src/pages/HealthPage.tsx +1 -1
- package/frontend/src/pages/InboxPage.tsx +2 -1
- package/frontend/src/pages/InstructionsPage.tsx +2 -2
- package/frontend/src/pages/IntegrationsPage.tsx +11 -5
- package/frontend/src/pages/MemoryPage.tsx +27 -118
- package/frontend/src/pages/ModelsPage.tsx +2 -50
- package/frontend/src/pages/OnboardingPage.tsx +5 -10
- package/frontend/src/pages/OpenWikiPage.tsx +1 -0
- package/frontend/src/pages/SettingsPage.tsx +136 -131
- package/frontend/src/pages/SkillsPage.tsx +102 -46
- package/frontend/src/pages/TasksPage.tsx +60 -25
- package/frontend/src/pages/UsagePage.tsx +206 -95
- package/frontend/src/types.ts +17 -11
- package/frontend/src/utils/onboarding.ts +10 -0
- package/package.json +3 -2
- package/scaffold/.agent/skills/ssss/SKILL.md +235 -0
- package/scaffold/.agent/skills/ssss/evals/evals.json +37 -0
- package/scaffold/.agent/skills/ssss/references/contract-reference.md +95 -0
- package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +215 -0
- package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +22 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
- package/src/cli/backup.mjs +3 -3
- package/src/cli/chat.mjs +2 -2
- package/src/cli/config.mjs +2 -9
- package/src/cli/connect.mjs +1 -1
- package/src/cli/deploy-ui.mjs +1 -1
- package/src/cli/deploy.mjs +2 -2
- package/src/cli/doctor.mjs +1 -1
- package/src/cli/help.mjs +1 -1
- package/src/cli/init.mjs +19 -1
- package/src/cli/key.mjs +150 -0
- package/src/cli/secret.mjs +30 -1
- package/src/cli/skill.mjs +0 -2
- package/src/cli/sync.mjs +19 -0
- package/src/cli/upgrade.mjs +1 -1
- package/src/cli/wizard.html +5 -5
- package/src/core/crons.mjs +11 -0
- package/src/core/daemon-loop.mjs +11 -0
- package/src/core/import-rules.mjs +4 -3
- package/src/core/okf-adapter.mjs +10 -10
- package/src/core/runtime.mjs +0 -2
- package/src/core/secrets-store.mjs +16 -0
- package/src/core/skills-registry.mjs +38 -0
- package/src/core/surface.mjs +1 -1
- package/src/core/task_runner.mjs +6 -0
- package/src/core/usage-tracker.mjs +16 -3
- package/src/core/vault-watcher.mjs +92 -0
- package/src/core/vault.mjs +0 -2
- package/src/server/api.mjs +1 -1
- package/src/server/rest.mjs +69 -57
- package/src/server/route-manifest.json +8 -0
- package/src/server/routes/_shared.mjs +1 -1
- package/src/server/routes/secrets.mjs +4 -4
- package/src/server/routes/skills.mjs +92 -10
package/bin/total-recall.mjs
CHANGED
|
@@ -62,6 +62,9 @@ const COMMANDS = {
|
|
|
62
62
|
task: 'task.mjs',
|
|
63
63
|
share: 'share.mjs',
|
|
64
64
|
'generate-pat': 'generate-pat.mjs',
|
|
65
|
+
key: 'key.mjs',
|
|
66
|
+
keys: 'key.mjs',
|
|
67
|
+
pat: 'key.mjs',
|
|
65
68
|
'hash-password': 'hash-password.mjs',
|
|
66
69
|
'reset-password': 'reset-password.mjs',
|
|
67
70
|
compile: 'rebuild.mjs',
|
package/frontend/src/App.tsx
CHANGED
|
@@ -15,7 +15,8 @@ import SkillsPage from './pages/SkillsPage'
|
|
|
15
15
|
import HelpPage from './pages/HelpPage'
|
|
16
16
|
import GraphPage from './pages/GraphPage'
|
|
17
17
|
import OpenWikiPage from './pages/OpenWikiPage'
|
|
18
|
-
import OnboardingPage
|
|
18
|
+
import OnboardingPage from './pages/OnboardingPage'
|
|
19
|
+
import { isOnboardingComplete } from './utils/onboarding'
|
|
19
20
|
import BrandMark from './components/brand/BrandMark'
|
|
20
21
|
|
|
21
22
|
// ─── Auth state type ──────────────────────────────────────────────────────────
|
|
@@ -122,7 +123,7 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
|
|
|
122
123
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
123
124
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
|
124
125
|
</svg>
|
|
125
|
-
|
|
126
|
+
Keys & Usage
|
|
126
127
|
</NavLink>
|
|
127
128
|
<NavLink to="/settings" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-settings">
|
|
128
129
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
@@ -299,13 +300,13 @@ function MainContent({ activeBrainId, onBrainChange }: { activeBrainId: string;
|
|
|
299
300
|
<Route path="/onboarding" element={<OnboardingPage />} />
|
|
300
301
|
<Route path="/memory" element={<MemoryPage activeBrainId={activeBrainId} />} />
|
|
301
302
|
<Route path="/graph" element={<GraphPage activeBrainId={activeBrainId} />} />
|
|
302
|
-
<Route path="/tasks" element={<TasksPage />} />
|
|
303
|
-
<Route path="/skills" element={<SkillsPage />} />
|
|
303
|
+
<Route path="/tasks" element={<TasksPage activeBrainId={activeBrainId} />} />
|
|
304
|
+
<Route path="/skills" element={<SkillsPage activeBrainId={activeBrainId} />} />
|
|
304
305
|
<Route path="/openwiki" element={<OpenWikiPage activeBrainId={activeBrainId} />} />
|
|
305
|
-
<Route path="/integrations" element={<IntegrationsPage />} />
|
|
306
|
+
<Route path="/integrations" element={<IntegrationsPage activeBrainId={activeBrainId} />} />
|
|
306
307
|
<Route path="/keys" element={<ApiKeysPage />} />
|
|
307
|
-
<Route path="/settings" element={<SettingsPage />} />
|
|
308
|
-
<Route path="/help" element={<HelpPage />} />
|
|
308
|
+
<Route path="/settings" element={<SettingsPage activeBrainId={activeBrainId} />} />
|
|
309
|
+
<Route path="/help" element={<HelpPage activeBrainId={activeBrainId} />} />
|
|
309
310
|
{/* Legacy OS-control-plane routes → core product */}
|
|
310
311
|
<Route path="/vault" element={<Navigate to="/memory" replace />} />
|
|
311
312
|
<Route path="/inbox" element={<Navigate to="/tasks" replace />} />
|
package/frontend/src/api.ts
CHANGED
|
@@ -453,6 +453,11 @@ export async function fetchConfigJson(): Promise<ConfigJson> {
|
|
|
453
453
|
return res.json()
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
export async function fetchBrains(): Promise<Record<string, unknown>[]> {
|
|
457
|
+
const res = await apiFetch(`${API_BASE}/api/brains`)
|
|
458
|
+
return res.json()
|
|
459
|
+
}
|
|
460
|
+
|
|
456
461
|
export async function saveConfigJson(config: ConfigJson): Promise<void> {
|
|
457
462
|
const res = await apiFetch(`${API_BASE}/api/config-json`, {
|
|
458
463
|
method: 'POST',
|
|
@@ -1414,3 +1419,23 @@ export async function postDecision(id: string, action: string, notes?: string):
|
|
|
1414
1419
|
}
|
|
1415
1420
|
return res.json()
|
|
1416
1421
|
}
|
|
1422
|
+
|
|
1423
|
+
export async function toggleSkillRepo(skillName: string, targetRepo: string, enabled: boolean): Promise<{ success: boolean; message: string }> {
|
|
1424
|
+
const res = await apiFetch(`${API_BASE}/api/skills/toggle`, {
|
|
1425
|
+
method: 'POST',
|
|
1426
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1427
|
+
body: JSON.stringify({ skillName, targetRepo, enabled })
|
|
1428
|
+
});
|
|
1429
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
1430
|
+
return res.json();
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
export async function auditSkill(skillName: string, repoPath?: string): Promise<{ success: boolean; audit: { outdated: boolean; problematic: boolean; reason: string; proposed_changes: string } }> {
|
|
1434
|
+
const res = await apiFetch(`${API_BASE}/api/skills/audit`, {
|
|
1435
|
+
method: 'POST',
|
|
1436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1437
|
+
body: JSON.stringify({ skillName, repoPath })
|
|
1438
|
+
});
|
|
1439
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
1440
|
+
return res.json();
|
|
1441
|
+
}
|
|
@@ -63,7 +63,7 @@ export const UsageChart: React.FC<UsageChartProps> = ({ usageData }) => {
|
|
|
63
63
|
<Tooltip
|
|
64
64
|
contentStyle={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 12 }}
|
|
65
65
|
itemStyle={{ fontSize: 12 }}
|
|
66
|
-
formatter={(value:
|
|
66
|
+
formatter={(value: number | string) => [`$${Number(value).toFixed(4)}`, undefined]}
|
|
67
67
|
/>
|
|
68
68
|
<Legend wrapperStyle={{ fontSize: 11 }} />
|
|
69
69
|
<Bar isAnimationActive={false} dataKey="gemini" name="Gemini" stackId="a" fill="#10b981" radius={[0,0,0,0]} />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, type CSSProperties, type ReactNode } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, type CSSProperties, type ReactNode } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
listApiKeys,
|
|
4
4
|
issueApiKey,
|
|
@@ -32,8 +32,9 @@ import {
|
|
|
32
32
|
} from '../api'
|
|
33
33
|
import type { ConfigJson, GeminiModelInfo } from '../types'
|
|
34
34
|
import { startRegistration, startAuthentication, browserSupportsWebAuthn } from '@simplewebauthn/browser'
|
|
35
|
+
import UsagePage from './UsagePage'
|
|
35
36
|
|
|
36
|
-
type Tab = 'catalog' | 'pats' | 'import' | 'cloud'
|
|
37
|
+
type Tab = 'catalog' | 'pats' | 'import' | 'cloud' | 'usage'
|
|
37
38
|
|
|
38
39
|
/** Form state — string inputs; converted on save */
|
|
39
40
|
type MetaEdit = {
|
|
@@ -50,6 +51,46 @@ type MetaEdit = {
|
|
|
50
51
|
project_path: string
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function metaForm(k: SecretCatalogKey): MetaEdit {
|
|
55
|
+
return {
|
|
56
|
+
label: k.label || '',
|
|
57
|
+
provider: k.provider || '',
|
|
58
|
+
repos: (k.repos || []).join(', '),
|
|
59
|
+
subscription_tier: k.subscription_tier || '',
|
|
60
|
+
monthly_cost_usd: k.monthly_cost_usd != null ? String(k.monthly_cost_usd) : '',
|
|
61
|
+
monthly_cap_usd: k.monthly_cap_usd != null ? String(k.monthly_cap_usd) : '',
|
|
62
|
+
api_docs_url: k.api_docs_url || '',
|
|
63
|
+
rotate_every_days: k.rotate_every_days != null ? String(k.rotate_every_days) : '',
|
|
64
|
+
auto_rotate: !!k.auto_rotate,
|
|
65
|
+
notes: k.notes || '',
|
|
66
|
+
project_path: k.project_path || '',
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function groupKeysByProvider(keys: SecretCatalogKey[]) {
|
|
71
|
+
const groups = new Map<string, { id: string; label: string; kind: 'repo'; keys: SecretCatalogKey[] }>()
|
|
72
|
+
|
|
73
|
+
keys.forEach((k) => {
|
|
74
|
+
const provider = (k.provider || 'unknown').toLowerCase()
|
|
75
|
+
if (!groups.has(provider)) {
|
|
76
|
+
groups.set(provider, { id: provider, label: provider.toUpperCase(), kind: 'repo', keys: [] })
|
|
77
|
+
}
|
|
78
|
+
groups.get(provider)!.keys.push(k)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return [...groups.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ProviderLogo({ provider }: { provider?: string }) {
|
|
85
|
+
if (!provider) return null;
|
|
86
|
+
const p = provider.toLowerCase();
|
|
87
|
+
if (p === 'openai') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0462 6.0462 0 0 0 5.3-3.71 6.0415 6.0415 0 0 0 4.4646-6.4463A5.9847 5.9847 0 0 0 22.282 9.8211Z"/></svg>;
|
|
88
|
+
if (p === 'anthropic') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M17.433 2.115L15.939 12h3.048l1.494-9.885h-3.048zM14.616 2.115l-1.392 9.885h3.048l1.392-9.885h-3.048zM10.371 14.542l-5.637 7.343H7.78l3.966-5.166 2.766 5.166h3.018l-7.159-12.836-5.836 12.836h3.043l1.838-4.043h4.636l-3.681-3.3z"/></svg>;
|
|
89
|
+
if (p === 'google') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>;
|
|
90
|
+
if (p === 'github') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd"/></svg>;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
export default function ApiKeysPage() {
|
|
54
95
|
const [tab, setTab] = useState<Tab>('catalog')
|
|
55
96
|
|
|
@@ -64,6 +105,7 @@ export default function ApiKeysPage() {
|
|
|
64
105
|
const [loading, setLoading] = useState(true)
|
|
65
106
|
const [error, setError] = useState<string | null>(null)
|
|
66
107
|
const [filter, setFilter] = useState('')
|
|
108
|
+
const [groupBy, setGroupBy] = useState<'repo' | 'api'>('repo')
|
|
67
109
|
const [selected, setSelected] = useState<SecretCatalogKey | null>(null)
|
|
68
110
|
const [edit, setEdit] = useState<MetaEdit>({
|
|
69
111
|
label: '',
|
|
@@ -122,9 +164,11 @@ export default function ApiKeysPage() {
|
|
|
122
164
|
setOpenaiModels(open)
|
|
123
165
|
const or = await fetchOpenRouterModels().catch(() => [])
|
|
124
166
|
setOrModels(or)
|
|
125
|
-
} catch
|
|
167
|
+
} catch {
|
|
168
|
+
// ignore fetching models error
|
|
169
|
+
}
|
|
126
170
|
|
|
127
|
-
|
|
171
|
+
const data = await fetchSecretsCatalog()
|
|
128
172
|
setCatalog(data)
|
|
129
173
|
if (selected) {
|
|
130
174
|
const fresh = data.keys.find((k) => k.key === selected.key)
|
|
@@ -136,7 +180,7 @@ export default function ApiKeysPage() {
|
|
|
136
180
|
} finally {
|
|
137
181
|
setLoading(false)
|
|
138
182
|
}
|
|
139
|
-
}, [selected?.key])
|
|
183
|
+
}, [selected?.key, selected])
|
|
140
184
|
|
|
141
185
|
const loadPats = useCallback(async () => {
|
|
142
186
|
try {
|
|
@@ -156,10 +200,13 @@ export default function ApiKeysPage() {
|
|
|
156
200
|
}, [])
|
|
157
201
|
|
|
158
202
|
useEffect(() => {
|
|
203
|
+
// eslint-disable-next-line
|
|
159
204
|
void loadCatalog()
|
|
205
|
+
|
|
160
206
|
void loadPats()
|
|
207
|
+
|
|
161
208
|
void loadWebAuthn()
|
|
162
|
-
}, [])
|
|
209
|
+
}, [loadCatalog, loadPats, loadWebAuthn])
|
|
163
210
|
|
|
164
211
|
|
|
165
212
|
const updateSecretsProp = (prop: string, value: string) => {
|
|
@@ -194,21 +241,7 @@ export default function ApiKeysPage() {
|
|
|
194
241
|
}
|
|
195
242
|
};
|
|
196
243
|
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
label: k.label || '',
|
|
200
|
-
provider: k.provider || '',
|
|
201
|
-
repos: (k.repos || []).join(', '),
|
|
202
|
-
subscription_tier: k.subscription_tier || '',
|
|
203
|
-
monthly_cost_usd: k.monthly_cost_usd != null ? String(k.monthly_cost_usd) : '',
|
|
204
|
-
monthly_cap_usd: k.monthly_cap_usd != null ? String(k.monthly_cap_usd) : '',
|
|
205
|
-
api_docs_url: k.api_docs_url || '',
|
|
206
|
-
rotate_every_days: k.rotate_every_days != null ? String(k.rotate_every_days) : '',
|
|
207
|
-
auto_rotate: !!k.auto_rotate,
|
|
208
|
-
notes: k.notes || '',
|
|
209
|
-
project_path: k.project_path || '',
|
|
210
|
-
}
|
|
211
|
-
}
|
|
244
|
+
// metaForm hoisted
|
|
212
245
|
|
|
213
246
|
function openKey(k: SecretCatalogKey, sectionId?: string) {
|
|
214
247
|
setSelected(k)
|
|
@@ -437,7 +470,18 @@ export default function ApiKeysPage() {
|
|
|
437
470
|
})
|
|
438
471
|
|
|
439
472
|
/** Group filtered keys into repo sections (one key → one repo; multi = error; none = developer). */
|
|
440
|
-
const repoSections =
|
|
473
|
+
const repoSections = useMemo<{ id: string; label: string; kind: 'error' | 'developer' | 'repo'; keys: SecretCatalogKey[] }[]>(() => {
|
|
474
|
+
if (!catalog?.keys) return []
|
|
475
|
+
const f = filter.toLowerCase()
|
|
476
|
+
const filtered = catalog.keys.filter(
|
|
477
|
+
(k) =>
|
|
478
|
+
k.key.toLowerCase().includes(f) ||
|
|
479
|
+
(k.provider || '').toLowerCase().includes(f) ||
|
|
480
|
+
(k.repos || []).some((r) => r.toLowerCase().includes(f)) ||
|
|
481
|
+
(k.label || '').toLowerCase().includes(f)
|
|
482
|
+
)
|
|
483
|
+
return groupBy === 'api' ? groupKeysByProvider(filtered) : groupKeysByRepo(filtered)
|
|
484
|
+
}, [catalog, filter, groupBy])
|
|
441
485
|
|
|
442
486
|
// Default accordion: open errors + first product/developer section only
|
|
443
487
|
useEffect(() => {
|
|
@@ -448,7 +492,9 @@ export default function ApiKeysPage() {
|
|
|
448
492
|
}
|
|
449
493
|
const first = repoSections.find((s) => s.kind !== 'error')
|
|
450
494
|
if (first) next.add(first.id)
|
|
495
|
+
// eslint-disable-next-line
|
|
451
496
|
setExpandedSections(next)
|
|
497
|
+
|
|
452
498
|
setSectionsInited(true)
|
|
453
499
|
}, [repoSections, sectionsInited])
|
|
454
500
|
|
|
@@ -707,6 +753,12 @@ export default function ApiKeysPage() {
|
|
|
707
753
|
))}
|
|
708
754
|
</div>
|
|
709
755
|
|
|
756
|
+
{tab === 'usage' && (
|
|
757
|
+
<div style={{ height: 'calc(100vh - 280px)', minHeight: 420, position: 'relative', overflowY: 'auto' }}>
|
|
758
|
+
<UsagePage />
|
|
759
|
+
</div>
|
|
760
|
+
)}
|
|
761
|
+
|
|
710
762
|
{tab === 'catalog' && (
|
|
711
763
|
<div
|
|
712
764
|
style={{
|
|
@@ -722,13 +774,34 @@ export default function ApiKeysPage() {
|
|
|
722
774
|
<div
|
|
723
775
|
style={{
|
|
724
776
|
display: 'flex',
|
|
725
|
-
gap:
|
|
726
|
-
flexWrap: 'wrap',
|
|
777
|
+
gap: 12,
|
|
727
778
|
alignItems: 'center',
|
|
728
|
-
marginBottom:
|
|
729
|
-
|
|
779
|
+
marginBottom: 16,
|
|
780
|
+
flexWrap: 'wrap',
|
|
730
781
|
}}
|
|
731
782
|
>
|
|
783
|
+
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderRadius: 6, padding: 2 }}>
|
|
784
|
+
<button
|
|
785
|
+
onClick={() => setGroupBy('repo')}
|
|
786
|
+
style={{
|
|
787
|
+
padding: '4px 10px', fontSize: 11, fontWeight: 500, borderRadius: 4, cursor: 'pointer', border: 'none',
|
|
788
|
+
background: groupBy === 'repo' ? 'var(--bg-active)' : 'transparent',
|
|
789
|
+
color: groupBy === 'repo' ? 'var(--text-primary)' : 'var(--text-secondary)'
|
|
790
|
+
}}
|
|
791
|
+
>
|
|
792
|
+
By Repo
|
|
793
|
+
</button>
|
|
794
|
+
<button
|
|
795
|
+
onClick={() => setGroupBy('api')}
|
|
796
|
+
style={{
|
|
797
|
+
padding: '4px 10px', fontSize: 11, fontWeight: 500, borderRadius: 4, cursor: 'pointer', border: 'none',
|
|
798
|
+
background: groupBy === 'api' ? 'var(--bg-active)' : 'transparent',
|
|
799
|
+
color: groupBy === 'api' ? 'var(--text-primary)' : 'var(--text-secondary)'
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
By API
|
|
803
|
+
</button>
|
|
804
|
+
</div>
|
|
732
805
|
<input
|
|
733
806
|
value={filter}
|
|
734
807
|
onChange={(e) => setFilter(e.target.value)}
|
|
@@ -741,9 +814,21 @@ export default function ApiKeysPage() {
|
|
|
741
814
|
border: '1px solid var(--border)',
|
|
742
815
|
background: 'var(--bg-primary)',
|
|
743
816
|
color: 'var(--text-primary)',
|
|
744
|
-
|
|
817
|
+
outline: 'none',
|
|
745
818
|
}}
|
|
746
819
|
/>
|
|
820
|
+
</div>
|
|
821
|
+
|
|
822
|
+
<div
|
|
823
|
+
style={{
|
|
824
|
+
display: 'flex',
|
|
825
|
+
gap: 8,
|
|
826
|
+
flexWrap: 'wrap',
|
|
827
|
+
alignItems: 'center',
|
|
828
|
+
marginBottom: 8,
|
|
829
|
+
flexShrink: 0,
|
|
830
|
+
}}
|
|
831
|
+
>
|
|
747
832
|
<button
|
|
748
833
|
type="button"
|
|
749
834
|
className="btn btn-ghost btn-sm"
|
|
@@ -801,6 +886,7 @@ export default function ApiKeysPage() {
|
|
|
801
886
|
title={open ? 'Currently expanded' : 'Expand this repo only'}
|
|
802
887
|
>
|
|
803
888
|
{open ? '▾ ' : '▸ '}
|
|
889
|
+
{groupBy === 'api' && <ProviderLogo provider={sec.id} />}
|
|
804
890
|
{sec.label}
|
|
805
891
|
<span style={{ marginLeft: 6, opacity: 0.7 }}>{sec.keys.length}</span>
|
|
806
892
|
</button>
|
|
@@ -873,7 +959,7 @@ export default function ApiKeysPage() {
|
|
|
873
959
|
color: sec.kind === 'error' ? '#f87171' : 'var(--text-primary)',
|
|
874
960
|
}}
|
|
875
961
|
>
|
|
876
|
-
{open ? '▾' : '▸'} {sec.label}
|
|
962
|
+
{open ? '▾' : '▸'} {groupBy === 'api' && <ProviderLogo provider={sec.id} />} {sec.label}
|
|
877
963
|
</div>
|
|
878
964
|
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 2 }}>
|
|
879
965
|
{sec.kind === 'error'
|
|
@@ -1367,7 +1453,6 @@ export default function ApiKeysPage() {
|
|
|
1367
1453
|
))}
|
|
1368
1454
|
</div>
|
|
1369
1455
|
)}
|
|
1370
|
-
|
|
1371
1456
|
{tab === 'cloud' && (
|
|
1372
1457
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
1373
1458
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
|
|
@@ -1380,7 +1465,7 @@ export default function ApiKeysPage() {
|
|
|
1380
1465
|
✓ {configSuccess}
|
|
1381
1466
|
</div>
|
|
1382
1467
|
)}
|
|
1383
|
-
|
|
1468
|
+
<div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
1384
1469
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
|
|
1385
1470
|
<span style={{ fontSize: 20 }}>🔑</span>
|
|
1386
1471
|
<div>
|
|
@@ -1673,24 +1758,15 @@ export default function ApiKeysPage() {
|
|
|
1673
1758
|
)}
|
|
1674
1759
|
</div>
|
|
1675
1760
|
|
|
1676
|
-
|
|
1677
|
-
{/* Search & Tool APIs */}
|
|
1678
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, paddingBottom: 16, borderBottom: '1px solid rgba(255,255,255,0.05)', marginTop: 16 }}>
|
|
1679
|
-
<h4 style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 8 }}>Search & External Tools</h4>
|
|
1680
|
-
{['tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'].map(key => (
|
|
1681
|
-
<div key={key} style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 8 }}>
|
|
1682
|
-
<label style={{ fontSize: 12, fontWeight: 500 }}>{key.replace(/_/g, ' ').replace('api', 'API').replace('key', 'Key').replace('token', 'Token').replace(/\\b\\w/g, c => c.toUpperCase())}</label>
|
|
1683
|
-
<input type="password" placeholder="Enter token..." value={(configData?.secrets as any)?.[key] || ''} onChange={(e) => updateSecretsProp(key, e.target.value)} style={{ background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border)', padding: '8px 12px', borderRadius: 6, outline: 'none', fontSize: 13 }} />
|
|
1684
|
-
</div>
|
|
1685
|
-
))}
|
|
1686
|
-
</div>
|
|
1687
1761
|
<span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>
|
|
1688
1762
|
These keys are stored locally and injected into the CLI reasoning agents on dispatch.
|
|
1689
1763
|
</span>
|
|
1690
|
-
|
|
1764
|
+
</div>
|
|
1691
1765
|
</div>
|
|
1692
1766
|
)}
|
|
1693
1767
|
|
|
1768
|
+
|
|
1769
|
+
|
|
1694
1770
|
<p style={{ marginTop: 24, fontSize: 11, color: 'var(--text-tertiary)' }}>
|
|
1695
1771
|
CLI: <code>secret catalog</code> · <code>secret meta KEY --repo app --tier pro --monthly-cost 25 --rotate-days 90</code> ·{' '}
|
|
1696
1772
|
<code>secret rotation-due</code> · <code>secret usage</code> · <code>secret providers</code>
|
|
@@ -27,17 +27,6 @@ interface KanbanColumn {
|
|
|
27
27
|
|
|
28
28
|
// ─── Static Data ────────────────────────────────────────────────────────────────
|
|
29
29
|
|
|
30
|
-
const CORE_DOCS: DocItem[] = [
|
|
31
|
-
{ name: 'ARCHITECTURE.md', path: 'docs/ARCHITECTURE.md', icon: '🏗️', category: 'Core Docs' },
|
|
32
|
-
{ name: 'SSSS.md', path: 'docs/SSSS.md', icon: '🔐', category: 'Core Docs' },
|
|
33
|
-
{ name: 'IDE_INTEGRATION.md', path: 'docs/IDE_INTEGRATION.md', icon: '🔌', category: 'Core Docs' },
|
|
34
|
-
{ name: 'CHANGELOG.md', path: 'docs/CHANGELOG.md', icon: '📋', category: 'Core Docs' },
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const DEV_GUIDES: DocItem[] = [
|
|
38
|
-
{ name: 'README.md', path: 'README.md', icon: '📖', category: 'Developer Guides' },
|
|
39
|
-
];
|
|
40
|
-
|
|
41
30
|
const KANBAN_COLUMNS: KanbanColumn[] = [
|
|
42
31
|
{
|
|
43
32
|
id: 'in-progress',
|
|
@@ -173,11 +162,11 @@ export default function DesignDocsPage() {
|
|
|
173
162
|
const [activeTab, setActiveTab] = useState<'architecture' | 'board'>('architecture');
|
|
174
163
|
|
|
175
164
|
// Architecture docs state
|
|
176
|
-
const [selectedDoc, setSelectedDoc] = useState<DocItem>(
|
|
165
|
+
const [selectedDoc, setSelectedDoc] = useState<DocItem | null>(null);
|
|
177
166
|
const [docContent, setDocContent] = useState<string>('');
|
|
178
167
|
const [docLoading, setDocLoading] = useState(false);
|
|
179
168
|
const [docError, setDocError] = useState<string | null>(null);
|
|
180
|
-
const [, setDocsFromApi] = useState<
|
|
169
|
+
const [docsFromApi, setDocsFromApi] = useState<DocItem[]>([]);
|
|
181
170
|
|
|
182
171
|
// Kanban state
|
|
183
172
|
const [expandedColumns, setExpandedColumns] = useState<Record<string, boolean>>({});
|
|
@@ -187,9 +176,18 @@ export default function DesignDocsPage() {
|
|
|
187
176
|
async function loadDocsList() {
|
|
188
177
|
try {
|
|
189
178
|
const data = await fetchDesignDocs();
|
|
190
|
-
|
|
179
|
+
const docItems: DocItem[] = data.map(d => ({
|
|
180
|
+
name: d.name,
|
|
181
|
+
path: d.path,
|
|
182
|
+
category: d.category || 'Other',
|
|
183
|
+
icon: '📄',
|
|
184
|
+
}));
|
|
185
|
+
setDocsFromApi(docItems);
|
|
186
|
+
if (docItems.length > 0) {
|
|
187
|
+
setSelectedDoc(docItems[0]);
|
|
188
|
+
}
|
|
191
189
|
} catch {
|
|
192
|
-
// Silently fall back
|
|
190
|
+
// Silently fall back
|
|
193
191
|
}
|
|
194
192
|
}
|
|
195
193
|
loadDocsList();
|
|
@@ -204,11 +202,11 @@ export default function DesignDocsPage() {
|
|
|
204
202
|
setDocLoading(true);
|
|
205
203
|
setDocError(null);
|
|
206
204
|
try {
|
|
207
|
-
const data = await fetchDesignDocContent(selectedDoc
|
|
205
|
+
const data = await fetchDesignDocContent(selectedDoc!.path);
|
|
208
206
|
if (!cancelled) setDocContent(data.content);
|
|
209
207
|
} catch {
|
|
210
208
|
if (!cancelled) {
|
|
211
|
-
setDocError(selectedDoc
|
|
209
|
+
setDocError(selectedDoc!.path);
|
|
212
210
|
setDocContent('');
|
|
213
211
|
}
|
|
214
212
|
} finally {
|
|
@@ -241,7 +239,7 @@ export default function DesignDocsPage() {
|
|
|
241
239
|
{title}
|
|
242
240
|
</div>
|
|
243
241
|
{items.map(item => {
|
|
244
|
-
const isActive = selectedDoc
|
|
242
|
+
const isActive = selectedDoc?.path === item.path;
|
|
245
243
|
return (
|
|
246
244
|
<button
|
|
247
245
|
id={`doc-nav-${item.name.replace(/\./g, '-').toLowerCase()}`}
|
|
@@ -294,8 +292,17 @@ export default function DesignDocsPage() {
|
|
|
294
292
|
overflowY: 'auto',
|
|
295
293
|
}}
|
|
296
294
|
>
|
|
297
|
-
{
|
|
298
|
-
|
|
295
|
+
{Object.entries(
|
|
296
|
+
docsFromApi.reduce((acc, doc) => {
|
|
297
|
+
acc[doc.category] = acc[doc.category] || [];
|
|
298
|
+
acc[doc.category].push(doc);
|
|
299
|
+
return acc;
|
|
300
|
+
}, {} as Record<string, DocItem[]>)
|
|
301
|
+
).map(([category, items]) => (
|
|
302
|
+
<React.Fragment key={category}>
|
|
303
|
+
{renderSidebarSection(category, items)}
|
|
304
|
+
</React.Fragment>
|
|
305
|
+
))}
|
|
299
306
|
</div>
|
|
300
307
|
|
|
301
308
|
{/* Main Viewer */}
|
|
@@ -328,7 +335,7 @@ export default function DesignDocsPage() {
|
|
|
328
335
|
<span style={{ color: 'var(--text-tertiary)' }}>›</span>
|
|
329
336
|
<span>docs</span>
|
|
330
337
|
<span style={{ color: 'var(--text-tertiary)' }}>›</span>
|
|
331
|
-
<span style={{ color: 'var(--accent)' }}>{selectedDoc
|
|
338
|
+
<span style={{ color: 'var(--accent)' }}>{selectedDoc?.name}</span>
|
|
332
339
|
</div>
|
|
333
340
|
|
|
334
341
|
{/* Content */}
|
|
@@ -134,7 +134,12 @@ export default function FilesPage() {
|
|
|
134
134
|
setNewScriptName("")
|
|
135
135
|
setIsCreatingScript(false)
|
|
136
136
|
await fetchScriptsList()
|
|
137
|
-
|
|
137
|
+
|
|
138
|
+
// Yield to the event loop to ensure React state (scripts list)
|
|
139
|
+
// is fully committed to the DOM before selecting it.
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
handleSelectScript(name).catch(console.error)
|
|
142
|
+
}, 0)
|
|
138
143
|
} catch (e) {
|
|
139
144
|
setError(`Failed to create script: ${(e as Error).message}`)
|
|
140
145
|
} finally {
|
|
@@ -16,7 +16,7 @@ export default function GraphPage({ activeBrainId }: { activeBrainId?: string })
|
|
|
16
16
|
setLoading(true)
|
|
17
17
|
try {
|
|
18
18
|
const [nodes, researchRes, threadsRes] = await Promise.all([
|
|
19
|
-
listMemory(activeBrainId),
|
|
19
|
+
listMemory(activeBrainId).catch(() => []),
|
|
20
20
|
listResearch().catch(() => ({ items: [] })),
|
|
21
21
|
fetchChatThreads().catch(() => [])
|
|
22
22
|
])
|
|
@@ -44,6 +44,10 @@ export default function GraphPage({ activeBrainId }: { activeBrainId?: string })
|
|
|
44
44
|
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
|
|
45
45
|
{loading ? (
|
|
46
46
|
<div className="skeleton" style={{ width: '100%', height: '100%' }} />
|
|
47
|
+
) : memoryNodes.length === 0 && researchItems.length === 0 && threads.length === 0 ? (
|
|
48
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--text-secondary)' }}>
|
|
49
|
+
No nodes found for graph
|
|
50
|
+
</div>
|
|
47
51
|
) : (
|
|
48
52
|
<Graph3D
|
|
49
53
|
threads={threads}
|
|
@@ -186,7 +186,7 @@ export default function HealthPage() {
|
|
|
186
186
|
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
187
187
|
<div>
|
|
188
188
|
<h1>System Health</h1>
|
|
189
|
-
<p>Live monitoring of your
|
|
189
|
+
<p>Live monitoring of your memory brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
|
|
190
190
|
</div>
|
|
191
191
|
{updateInfo?.updateAvailable ? (
|
|
192
192
|
<div className="badge badge-success" style={{
|
|
@@ -25,6 +25,7 @@ export default function InboxPage({ activeBrainId }: { activeBrainId?: string })
|
|
|
25
25
|
}, [loadData])
|
|
26
26
|
|
|
27
27
|
const handleDecision = async (path: string, action: string) => {
|
|
28
|
+
setError("")
|
|
28
29
|
try {
|
|
29
30
|
const doc = await readDoc(path, activeBrainId)
|
|
30
31
|
// Extract proposal ID from path (e.g. proposals/p2.md -> p2)
|
|
@@ -40,7 +41,7 @@ export default function InboxPage({ activeBrainId }: { activeBrainId?: string })
|
|
|
40
41
|
}
|
|
41
42
|
loadData()
|
|
42
43
|
} catch (err: unknown) {
|
|
43
|
-
|
|
44
|
+
setError("Decision failed: " + (err instanceof Error ? err.message : "Unknown error"))
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -268,9 +268,9 @@ export default function InstructionsPage() {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
useEffect(() => {
|
|
271
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
271
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate data fetch on mount
|
|
272
272
|
void loadSurfaces()
|
|
273
|
-
}, [])
|
|
273
|
+
}, [loadSurfaces])
|
|
274
274
|
|
|
275
275
|
// ─── Derived Stats ───────────────────────────────────────────────────────────
|
|
276
276
|
|
|
@@ -62,7 +62,7 @@ const PRESETS: Preset[] = [
|
|
|
62
62
|
}
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
-
export default function IntegrationsPage() {
|
|
65
|
+
export default function IntegrationsPage({ activeBrainId }: { activeBrainId?: string }) {
|
|
66
66
|
const [copied, setCopied] = useState<string | null>(null)
|
|
67
67
|
const [connecting, setConnecting] = useState<string | null>(null)
|
|
68
68
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
|
@@ -82,7 +82,9 @@ export default function IntegrationsPage() {
|
|
|
82
82
|
.then(res => {
|
|
83
83
|
if (active) setKeys(res.keys || [])
|
|
84
84
|
})
|
|
85
|
-
.catch(
|
|
85
|
+
.catch(e => {
|
|
86
|
+
if (active) setMessage({ type: 'error', text: `Failed to list API keys: ${(e as Error).message || 'Unknown error'}` })
|
|
87
|
+
})
|
|
86
88
|
|
|
87
89
|
fetchActiveIntegrations()
|
|
88
90
|
.then(res => {
|
|
@@ -90,16 +92,20 @@ export default function IntegrationsPage() {
|
|
|
90
92
|
setActiveIdes(res.active)
|
|
91
93
|
}
|
|
92
94
|
})
|
|
93
|
-
.catch(
|
|
95
|
+
.catch(e => {
|
|
96
|
+
if (active) setMessage({ type: 'error', text: `Failed to fetch active integrations: ${(e as Error).message || 'Unknown error'}` })
|
|
97
|
+
})
|
|
94
98
|
|
|
95
99
|
fetchExtensionStatus()
|
|
96
100
|
.then(res => {
|
|
97
101
|
if (active) setExtStatus(res)
|
|
98
102
|
})
|
|
99
|
-
.catch(
|
|
103
|
+
.catch(e => {
|
|
104
|
+
if (active) setMessage({ type: 'error', text: `Failed to fetch extension status: ${(e as Error).message || 'Unknown error'}` })
|
|
105
|
+
})
|
|
100
106
|
|
|
101
107
|
return () => { active = false }
|
|
102
|
-
}, [])
|
|
108
|
+
}, [activeBrainId])
|
|
103
109
|
|
|
104
110
|
const filteredPresets = useMemo(() => {
|
|
105
111
|
if (activeIdes.length === 0) return PRESETS
|