total-recall-brain 3.13.2 → 3.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/bin/total-recall.mjs +3 -0
  2. package/frontend/src/App.tsx +8 -7
  3. package/frontend/src/api.ts +25 -0
  4. package/frontend/src/components/UsageChart.tsx +1 -1
  5. package/frontend/src/pages/ApiKeysPage.tsx +118 -42
  6. package/frontend/src/pages/ChatPage.tsx +1 -1
  7. package/frontend/src/pages/DesignDocsPage.tsx +28 -21
  8. package/frontend/src/pages/FilesPage.tsx +6 -1
  9. package/frontend/src/pages/GraphPage.tsx +5 -1
  10. package/frontend/src/pages/HealthPage.tsx +1 -1
  11. package/frontend/src/pages/InboxPage.tsx +2 -1
  12. package/frontend/src/pages/InstructionsPage.tsx +26 -26
  13. package/frontend/src/pages/IntegrationsPage.tsx +11 -5
  14. package/frontend/src/pages/MemoryPage.tsx +27 -118
  15. package/frontend/src/pages/ModelsPage.tsx +2 -50
  16. package/frontend/src/pages/OnboardingPage.tsx +5 -10
  17. package/frontend/src/pages/OpenWikiPage.tsx +1 -0
  18. package/frontend/src/pages/SettingsPage.tsx +136 -131
  19. package/frontend/src/pages/SkillsPage.tsx +102 -46
  20. package/frontend/src/pages/TasksPage.tsx +60 -25
  21. package/frontend/src/pages/UsagePage.tsx +206 -95
  22. package/frontend/src/types.ts +17 -11
  23. package/frontend/src/utils/onboarding.ts +10 -0
  24. package/package.json +3 -2
  25. package/scaffold/.agent/skills/ssss/SKILL.md +235 -0
  26. package/scaffold/.agent/skills/ssss/evals/evals.json +37 -0
  27. package/scaffold/.agent/skills/ssss/references/contract-reference.md +95 -0
  28. package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +215 -0
  29. package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +22 -0
  30. package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
  31. package/src/cli/backup.mjs +3 -3
  32. package/src/cli/chat.mjs +2 -2
  33. package/src/cli/config.mjs +2 -9
  34. package/src/cli/connect.mjs +1 -1
  35. package/src/cli/deploy-ui.mjs +1 -1
  36. package/src/cli/deploy.mjs +2 -2
  37. package/src/cli/doctor.mjs +1 -1
  38. package/src/cli/help.mjs +1 -1
  39. package/src/cli/init.mjs +19 -1
  40. package/src/cli/key.mjs +150 -0
  41. package/src/cli/secret.mjs +30 -1
  42. package/src/cli/skill.mjs +0 -2
  43. package/src/cli/sync.mjs +19 -0
  44. package/src/cli/upgrade.mjs +1 -1
  45. package/src/cli/wizard.html +5 -5
  46. package/src/core/crons.mjs +11 -0
  47. package/src/core/daemon-loop.mjs +11 -0
  48. package/src/core/import-rules.mjs +4 -3
  49. package/src/core/okf-adapter.mjs +10 -10
  50. package/src/core/runtime.mjs +0 -2
  51. package/src/core/secrets-store.mjs +16 -0
  52. package/src/core/skills-registry.mjs +38 -0
  53. package/src/core/surface.mjs +1 -1
  54. package/src/core/task_runner.mjs +6 -0
  55. package/src/core/usage-tracker.mjs +16 -3
  56. package/src/core/vault-watcher.mjs +92 -0
  57. package/src/core/vault.mjs +0 -2
  58. package/src/server/api.mjs +1 -1
  59. package/src/server/rest.mjs +69 -57
  60. package/src/server/route-manifest.json +8 -0
  61. package/src/server/routes/_shared.mjs +1 -1
  62. package/src/server/routes/secrets.mjs +4 -4
  63. package/src/server/routes/skills.mjs +92 -10
@@ -1,27 +1,8 @@
1
1
  import { useState, useEffect, useMemo } from 'react';
2
2
  import { fetchUsageStats, fetchConfigJson } from '../api';
3
- import type { ConfigJson } from '../types';
3
+ import type { ConfigJson, UsageData } from '../types';
4
4
  import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
5
5
 
6
- interface UsageBreakdown {
7
- dailyUsd: number;
8
- weeklyUsd: number;
9
- dailyTokens?: number;
10
- weeklyTokens?: number;
11
- }
12
-
13
- interface UsageData {
14
- timestamp: string;
15
- dailyUsd: number;
16
- weeklyUsd: number;
17
- breakdown: {
18
- gemini: UsageBreakdown;
19
- claude: UsageBreakdown;
20
- codex: UsageBreakdown;
21
- openrouter?: UsageBreakdown;
22
- };
23
- }
24
-
25
6
  export default function UsagePage() {
26
7
  const [usage, setUsage] = useState<UsageData | null>(null);
27
8
  const [config, setConfig] = useState<ConfigJson | null>(null);
@@ -65,7 +46,7 @@ export default function UsagePage() {
65
46
  return 'var(--success)';
66
47
  };
67
48
 
68
- // eslint-disable-next-line react-hooks/exhaustive-deps -- usage reference is stable per fetch cycle
49
+
69
50
  const timeseriesData = useMemo(() => {
70
51
  if ((usage as unknown as Record<string, unknown>)?.timeseries && Object.keys((usage as unknown as Record<string, unknown>).timeseries as Record<string, unknown>).length > 0) {
71
52
  const ts = (usage as unknown as Record<string, unknown>).timeseries as Record<string, Record<string, { provider: string; cost: number }>>;
@@ -146,7 +127,7 @@ export default function UsagePage() {
146
127
  <div>
147
128
  <h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Daily Expenditure</h3>
148
129
  <div style={{ fontSize: 28, fontWeight: 700, marginTop: 4, color: '#fff' }}>
149
- ${usage?.dailyUsd.toFixed(4) || '0.0000'}
130
+ ${usage?.dailyUsd?.toFixed(4) ?? '0.0000'}
150
131
  </div>
151
132
  </div>
152
133
  <span className="badge" style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)', border: '1px solid var(--border)', fontSize: 11 }}>
@@ -181,7 +162,7 @@ export default function UsagePage() {
181
162
  <div>
182
163
  <h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Weekly Expenditure</h3>
183
164
  <div style={{ fontSize: 28, fontWeight: 700, marginTop: 4, color: '#fff' }}>
184
- ${usage?.weeklyUsd.toFixed(4) || '0.0000'}
165
+ ${usage?.weeklyUsd?.toFixed(4) ?? '0.0000'}
185
166
  </div>
186
167
  </div>
187
168
  <span className="badge" style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)', border: '1px solid var(--border)', fontSize: 11 }}>
@@ -225,7 +206,10 @@ export default function UsagePage() {
225
206
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
226
207
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
227
208
  <span style={{ fontSize: 18 }}>♊</span>
228
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Gemini</span>
209
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
210
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Gemini CLI</span>
211
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>~/.gemini/tmp</span>
212
+ </div>
229
213
  </div>
230
214
  <span className="badge" style={{ background: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
231
215
  Google API
@@ -235,15 +219,15 @@ export default function UsagePage() {
235
219
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
236
220
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
237
221
  <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
238
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.gemini.dailyUsd.toFixed(4)}</span>
222
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.gemini?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
239
223
  </div>
240
224
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
241
225
  <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
242
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.gemini.weeklyUsd.toFixed(4)}</span>
226
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.gemini?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
243
227
  </div>
244
228
  <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
245
229
  <span>Rolling Tokens:</span>
246
- <span>{usage?.breakdown.gemini.dailyTokens?.toLocaleString() || 0} (24h)</span>
230
+ <span>{usage?.breakdown?.gemini?.dailyTokens?.toLocaleString() || 0} (24h)</span>
247
231
  </div>
248
232
  </div>
249
233
  </div>
@@ -253,7 +237,10 @@ export default function UsagePage() {
253
237
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
254
238
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
255
239
  <span style={{ fontSize: 18 }}>🍊</span>
256
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Claude</span>
240
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
241
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Claude Code CLI</span>
242
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>~/.claude</span>
243
+ </div>
257
244
  </div>
258
245
  <span className="badge" style={{ background: 'rgba(239, 107, 107, 0.1)', color: '#f87171', border: '1px solid rgba(239, 107, 107, 0.2)' }}>
259
246
  Anthropic API
@@ -263,15 +250,15 @@ export default function UsagePage() {
263
250
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
264
251
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
265
252
  <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
266
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.claude.dailyUsd.toFixed(4)}</span>
253
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.claude?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
267
254
  </div>
268
255
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
269
256
  <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
270
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.claude.weeklyUsd.toFixed(4)}</span>
257
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.claude?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
271
258
  </div>
272
259
  <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
273
260
  <span>Rolling Tokens:</span>
274
- <span>(Ingested stats-cache)</span>
261
+ <span>{usage?.breakdown?.claude?.dailyTokens?.toLocaleString() || 0} (24h)</span>
275
262
  </div>
276
263
  </div>
277
264
  </div>
@@ -281,7 +268,10 @@ export default function UsagePage() {
281
268
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
282
269
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
283
270
  <span style={{ fontSize: 18 }}>💻</span>
284
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Codex</span>
271
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
272
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Codex CLI</span>
273
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>~/.codex/sessions</span>
274
+ </div>
285
275
  </div>
286
276
  <span className="badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: '#34d399', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
287
277
  OpenAI API
@@ -291,15 +281,15 @@ export default function UsagePage() {
291
281
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
292
282
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
293
283
  <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
294
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.codex.dailyUsd.toFixed(4)}</span>
284
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.codex?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
295
285
  </div>
296
286
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
297
287
  <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
298
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.codex.weeklyUsd.toFixed(4)}</span>
288
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.codex?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
299
289
  </div>
300
290
  <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
301
291
  <span>Rolling Tokens:</span>
302
- <span>{usage?.breakdown.codex.dailyTokens?.toLocaleString() || 0} (24h)</span>
292
+ <span>{usage?.breakdown?.codex?.dailyTokens?.toLocaleString() || 0} (24h)</span>
303
293
  </div>
304
294
  </div>
305
295
  </div>
@@ -318,19 +308,130 @@ export default function UsagePage() {
318
308
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
319
309
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
320
310
  <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
321
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.openrouter?.dailyUsd.toFixed(4) || '0.0000'}</span>
311
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.openrouter?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
312
+ </div>
313
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
314
+ <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
315
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.openrouter?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
316
+ </div>
317
+ <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
318
+ <span>Rolling Tokens:</span>
319
+ <span>{usage?.breakdown?.openrouter?.dailyTokens?.toLocaleString() || 0} (24h)</span>
320
+ </div>
321
+ </div>
322
+ </div>
323
+
324
+ {/* Tavily */}
325
+ <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
326
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
327
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
328
+ <span style={{ fontSize: 18 }}>🔎</span>
329
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Tavily Search</span>
330
+ </div>
331
+ <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
332
+ Web API
333
+ </span>
334
+ </div>
335
+
336
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
337
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
338
+ <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
339
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.tavily?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
340
+ </div>
341
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
342
+ <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
343
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.tavily?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
344
+ </div>
345
+ <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
346
+ <span>Rolling Tokens:</span>
347
+ <span>{usage?.breakdown?.tavily?.dailyTokens?.toLocaleString() || 0} (24h)</span>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ {/* Brave */}
353
+ <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
354
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
355
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
356
+ <span style={{ fontSize: 18 }}>🦁</span>
357
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Brave Search</span>
358
+ </div>
359
+ <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
360
+ Web API
361
+ </span>
362
+ </div>
363
+
364
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
365
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
366
+ <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
367
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.brave?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
368
+ </div>
369
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
370
+ <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
371
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.brave?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
372
+ </div>
373
+ <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
374
+ <span>Rolling Tokens:</span>
375
+ <span>{usage?.breakdown?.brave?.dailyTokens?.toLocaleString() || 0} (24h)</span>
376
+ </div>
377
+ </div>
378
+ </div>
379
+
380
+ {/* Exa */}
381
+ <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
382
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
383
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
384
+ <span style={{ fontSize: 18 }}>✨</span>
385
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Exa Search</span>
386
+ </div>
387
+ <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
388
+ Web API
389
+ </span>
390
+ </div>
391
+
392
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
393
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
394
+ <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
395
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.exa?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
322
396
  </div>
323
397
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
324
398
  <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
325
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.openrouter?.weeklyUsd.toFixed(4) || '0.0000'}</span>
399
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.exa?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
326
400
  </div>
327
401
  <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
328
402
  <span>Rolling Tokens:</span>
329
- <span>{usage?.breakdown.openrouter?.dailyTokens?.toLocaleString() || 0} (24h)</span>
403
+ <span>{usage?.breakdown?.exa?.dailyTokens?.toLocaleString() || 0} (24h)</span>
330
404
  </div>
331
405
  </div>
332
406
  </div>
333
407
 
408
+ {/* Serper */}
409
+ <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
410
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
411
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
412
+ <span style={{ fontSize: 18 }}>🔍</span>
413
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Serper Search</span>
414
+ </div>
415
+ <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
416
+ Web API
417
+ </span>
418
+ </div>
419
+
420
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
421
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
422
+ <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
423
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.serper?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
424
+ </div>
425
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
426
+ <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
427
+ <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.serper?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
428
+ </div>
429
+ <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
430
+ <span>Rolling Tokens:</span>
431
+ <span>{usage?.breakdown?.serper?.dailyTokens?.toLocaleString() || 0} (24h)</span>
432
+ </div>
433
+ </div>
434
+ </div>
334
435
 
335
436
  </div>
336
437
  </div>
@@ -340,41 +441,47 @@ export default function UsagePage() {
340
441
  <h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid var(--border)', paddingBottom: 10, marginBottom: 20 }}>
341
442
  30-Day Cost Trend
342
443
  </h3>
343
- <ResponsiveContainer width="100%" height={300}>
344
- <AreaChart data={timeseriesData} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
345
- <defs>
346
- <linearGradient id="gradGemini" x1="0" y1="0" x2="0" y2="1">
347
- <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.4} />
348
- <stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
349
- </linearGradient>
350
- <linearGradient id="gradClaude" x1="0" y1="0" x2="0" y2="1">
351
- <stop offset="5%" stopColor="#e17055" stopOpacity={0.4} />
352
- <stop offset="95%" stopColor="#e17055" stopOpacity={0} />
353
- </linearGradient>
354
- <linearGradient id="gradOpenRouter" x1="0" y1="0" x2="0" y2="1">
355
- <stop offset="5%" stopColor="#74b9ff" stopOpacity={0.4} />
356
- <stop offset="95%" stopColor="#74b9ff" stopOpacity={0} />
357
- </linearGradient>
358
- <linearGradient id="gradCodex" x1="0" y1="0" x2="0" y2="1">
359
- <stop offset="5%" stopColor="#00cec9" stopOpacity={0.4} />
360
- <stop offset="95%" stopColor="#00cec9" stopOpacity={0} />
361
- </linearGradient>
362
- </defs>
363
- <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
364
- <XAxis dataKey="date" tick={{ fontSize: 11, fill: 'var(--text-secondary)' }} axisLine={{ stroke: 'var(--border)' }} tickLine={false} />
365
- <YAxis tick={{ fontSize: 11, fill: 'var(--text-secondary)' }} axisLine={{ stroke: 'var(--border)' }} tickLine={false} tickFormatter={(v: number) => `$${v.toFixed(2)}`} />
366
- <Tooltip
367
- contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, fontSize: 12, color: '#e8e8e8' }}
368
- labelStyle={{ color: '#fff', fontWeight: 600, marginBottom: 4 }}
369
- formatter={(value: any, name: any) => [`$${Number(value).toFixed(3)}`, String(name).charAt(0).toUpperCase() + String(name).slice(1)]}
370
- />
371
- <Legend wrapperStyle={{ fontSize: 12, color: 'var(--text-secondary)' }} />
372
- <Area type="monotone" dataKey="gemini" stackId="1" stroke="#3b82f6" fill="url(#gradGemini)" strokeWidth={2} />
373
- <Area type="monotone" dataKey="claude" stackId="1" stroke="#e17055" fill="url(#gradClaude)" strokeWidth={2} />
374
- <Area type="monotone" dataKey="openrouter" stackId="1" stroke="#74b9ff" fill="url(#gradOpenRouter)" strokeWidth={2} />
375
- <Area type="monotone" dataKey="codex" stackId="1" stroke="#00cec9" fill="url(#gradCodex)" strokeWidth={2} />
376
- </AreaChart>
377
- </ResponsiveContainer>
444
+ {timeseriesData.length === 0 ? (
445
+ <div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-secondary)' }}>
446
+ No usage data available for the last 30 days.
447
+ </div>
448
+ ) : (
449
+ <ResponsiveContainer width="100%" height={300}>
450
+ <AreaChart data={timeseriesData} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
451
+ <defs>
452
+ <linearGradient id="gradGemini" x1="0" y1="0" x2="0" y2="1">
453
+ <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.4} />
454
+ <stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
455
+ </linearGradient>
456
+ <linearGradient id="gradClaude" x1="0" y1="0" x2="0" y2="1">
457
+ <stop offset="5%" stopColor="#e17055" stopOpacity={0.4} />
458
+ <stop offset="95%" stopColor="#e17055" stopOpacity={0} />
459
+ </linearGradient>
460
+ <linearGradient id="gradOpenRouter" x1="0" y1="0" x2="0" y2="1">
461
+ <stop offset="5%" stopColor="#74b9ff" stopOpacity={0.4} />
462
+ <stop offset="95%" stopColor="#74b9ff" stopOpacity={0} />
463
+ </linearGradient>
464
+ <linearGradient id="gradCodex" x1="0" y1="0" x2="0" y2="1">
465
+ <stop offset="5%" stopColor="#00cec9" stopOpacity={0.4} />
466
+ <stop offset="95%" stopColor="#00cec9" stopOpacity={0} />
467
+ </linearGradient>
468
+ </defs>
469
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
470
+ <XAxis dataKey="date" tick={{ fontSize: 11, fill: 'var(--text-secondary)' }} axisLine={{ stroke: 'var(--border)' }} tickLine={false} />
471
+ <YAxis tick={{ fontSize: 11, fill: 'var(--text-secondary)' }} axisLine={{ stroke: 'var(--border)' }} tickLine={false} tickFormatter={(v: number) => `$${v.toFixed(2)}`} />
472
+ <Tooltip
473
+ contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, fontSize: 12, color: '#e8e8e8' }}
474
+ labelStyle={{ color: '#fff', fontWeight: 600, marginBottom: 4 }}
475
+ formatter={(value: number | string, name: string) => [`$${Number(value).toFixed(3)}`, String(name).charAt(0).toUpperCase() + String(name).slice(1)]}
476
+ />
477
+ <Legend wrapperStyle={{ fontSize: 12, color: 'var(--text-secondary)' }} />
478
+ <Area type="monotone" dataKey="gemini" stackId="1" stroke="#3b82f6" fill="url(#gradGemini)" strokeWidth={2} />
479
+ <Area type="monotone" dataKey="claude" stackId="1" stroke="#e17055" fill="url(#gradClaude)" strokeWidth={2} />
480
+ <Area type="monotone" dataKey="openrouter" stackId="1" stroke="#74b9ff" fill="url(#gradOpenRouter)" strokeWidth={2} />
481
+ <Area type="monotone" dataKey="codex" stackId="1" stroke="#00cec9" fill="url(#gradCodex)" strokeWidth={2} />
482
+ </AreaChart>
483
+ </ResponsiveContainer>
484
+ )}
378
485
  </div>
379
486
 
380
487
  {/* ── Provider Surface Cards ── */}
@@ -384,8 +491,12 @@ export default function UsagePage() {
384
491
  { name: 'Claude', color: '#e17055', emoji: '🍊', key: 'claude' as const },
385
492
  { name: 'OpenRouter', color: '#74b9ff', emoji: '🌐', key: 'openrouter' as const },
386
493
  { name: 'Codex', color: '#00cec9', emoji: '💻', key: 'codex' as const },
494
+ { name: 'Tavily', color: '#eab308', emoji: '🔎', key: 'tavily' as const },
495
+ { name: 'Brave', color: '#eab308', emoji: '🦁', key: 'brave' as const },
496
+ { name: 'Exa', color: '#eab308', emoji: '✨', key: 'exa' as const },
497
+ { name: 'Serper', color: '#eab308', emoji: '🔍', key: 'serper' as const },
387
498
  ] as const).map((provider) => {
388
- const data = provider.key === 'openrouter' ? usage?.breakdown.openrouter : usage?.breakdown[provider.key];
499
+ const data = usage?.breakdown?.[provider.key as keyof typeof usage.breakdown];
389
500
  return (
390
501
  <div key={provider.key} style={{
391
502
  background: 'rgba(18, 18, 26, 0.6)',
@@ -409,11 +520,11 @@ export default function UsagePage() {
409
520
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
410
521
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
411
522
  <span style={{ color: 'var(--text-secondary)' }}>Daily</span>
412
- <span style={{ fontWeight: 600, color: provider.color }}>${data?.dailyUsd.toFixed(4) || '0.0000'}</span>
523
+ <span style={{ fontWeight: 600, color: provider.color }}>${data?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
413
524
  </div>
414
525
  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
415
526
  <span style={{ color: 'var(--text-secondary)' }}>Weekly</span>
416
- <span style={{ fontWeight: 600, color: provider.color }}>${data?.weeklyUsd.toFixed(4) || '0.0000'}</span>
527
+ <span style={{ fontWeight: 600, color: provider.color }}>${data?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
417
528
  </div>
418
529
  {data?.dailyTokens != null && (
419
530
  <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 2, fontSize: 11, color: 'var(--text-tertiary)' }}>
@@ -448,12 +559,12 @@ export default function UsagePage() {
448
559
  { name: 'Codex', key: 'codex' as const, color: '#00cec9' },
449
560
  { name: 'OpenRouter', key: 'openrouter' as const, color: '#74b9ff' },
450
561
  ] as const).map((row, idx) => {
451
- const d = row.key === 'openrouter' ? usage?.breakdown.openrouter : usage?.breakdown[row.key];
562
+ const d = row.key === 'openrouter' ? usage?.breakdown?.openrouter : usage?.breakdown?.[row.key];
452
563
  return (
453
564
  <tr key={row.key} style={{ background: idx % 2 === 1 ? 'rgba(255,255,255,0.02)' : 'transparent', borderBottom: '1px solid var(--border)' }}>
454
565
  <td style={{ padding: '10px 12px', fontWeight: 600, color: row.color }}>{row.name}</td>
455
- <td style={{ padding: '10px 12px', color: '#fff' }}>${d?.dailyUsd.toFixed(4) || '0.0000'}</td>
456
- <td style={{ padding: '10px 12px', color: '#fff' }}>${d?.weeklyUsd.toFixed(4) || '0.0000'}</td>
566
+ <td style={{ padding: '10px 12px', color: '#fff' }}>${d?.dailyUsd?.toFixed(4) ?? '0.0000'}</td>
567
+ <td style={{ padding: '10px 12px', color: '#fff' }}>${d?.weeklyUsd?.toFixed(4) ?? '0.0000'}</td>
457
568
  <td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{d?.dailyTokens?.toLocaleString() || '—'}</td>
458
569
  <td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{d?.weeklyTokens?.toLocaleString() || '—'}</td>
459
570
  </tr>
@@ -464,34 +575,34 @@ export default function UsagePage() {
464
575
  <td style={{ padding: '10px 12px', fontWeight: 700, color: '#fff' }}>Total</td>
465
576
  <td style={{ padding: '10px 12px', fontWeight: 700, color: '#fff' }}>
466
577
  ${(
467
- (usage?.breakdown.gemini.dailyUsd || 0) +
468
- (usage?.breakdown.claude.dailyUsd || 0) +
469
- (usage?.breakdown.codex.dailyUsd || 0) +
470
- (usage?.breakdown.openrouter?.dailyUsd || 0)
578
+ (usage?.breakdown?.gemini?.dailyUsd || 0) +
579
+ (usage?.breakdown?.claude?.dailyUsd || 0) +
580
+ (usage?.breakdown?.codex?.dailyUsd || 0) +
581
+ (usage?.breakdown?.openrouter?.dailyUsd || 0)
471
582
  ).toFixed(4)}
472
583
  </td>
473
584
  <td style={{ padding: '10px 12px', fontWeight: 700, color: '#fff' }}>
474
585
  ${(
475
- (usage?.breakdown.gemini.weeklyUsd || 0) +
476
- (usage?.breakdown.claude.weeklyUsd || 0) +
477
- (usage?.breakdown.codex.weeklyUsd || 0) +
478
- (usage?.breakdown.openrouter?.weeklyUsd || 0)
586
+ (usage?.breakdown?.gemini?.weeklyUsd || 0) +
587
+ (usage?.breakdown?.claude?.weeklyUsd || 0) +
588
+ (usage?.breakdown?.codex?.weeklyUsd || 0) +
589
+ (usage?.breakdown?.openrouter?.weeklyUsd || 0)
479
590
  ).toFixed(4)}
480
591
  </td>
481
592
  <td style={{ padding: '10px 12px', fontWeight: 700, color: 'var(--text-secondary)' }}>
482
593
  {(
483
- (usage?.breakdown.gemini.dailyTokens || 0) +
484
- (usage?.breakdown.claude.dailyTokens || 0) +
485
- (usage?.breakdown.codex.dailyTokens || 0) +
486
- (usage?.breakdown.openrouter?.dailyTokens || 0)
594
+ (usage?.breakdown?.gemini?.dailyTokens || 0) +
595
+ (usage?.breakdown?.claude?.dailyTokens || 0) +
596
+ (usage?.breakdown?.codex?.dailyTokens || 0) +
597
+ (usage?.breakdown?.openrouter?.dailyTokens || 0)
487
598
  ).toLocaleString()}
488
599
  </td>
489
600
  <td style={{ padding: '10px 12px', fontWeight: 700, color: 'var(--text-secondary)' }}>
490
601
  {(
491
- (usage?.breakdown.gemini.weeklyTokens || 0) +
492
- (usage?.breakdown.claude.weeklyTokens || 0) +
493
- (usage?.breakdown.codex.weeklyTokens || 0) +
494
- (usage?.breakdown.openrouter?.weeklyTokens || 0)
602
+ (usage?.breakdown?.gemini?.weeklyTokens || 0) +
603
+ (usage?.breakdown?.claude?.weeklyTokens || 0) +
604
+ (usage?.breakdown?.codex?.weeklyTokens || 0) +
605
+ (usage?.breakdown?.openrouter?.weeklyTokens || 0)
495
606
  ).toLocaleString()}
496
607
  </td>
497
608
  </tr>
@@ -129,6 +129,10 @@ export interface UsageData {
129
129
  claude: UsageBreakdown
130
130
  codex: UsageBreakdown
131
131
  openrouter?: UsageBreakdown
132
+ tavily?: UsageBreakdown
133
+ brave?: UsageBreakdown
134
+ exa?: UsageBreakdown
135
+ serper?: UsageBreakdown
132
136
  }
133
137
  timeseries?: {
134
138
  [dateStr: string]: {
@@ -163,19 +167,19 @@ export interface ConfigJson {
163
167
  require_https?: boolean
164
168
  public_health?: boolean
165
169
  allowed_origins?: string[]
166
- trusted_proxies?: string
170
+ trusted_proxies?: string[]
167
171
  }
168
172
  rate_limits?: {
169
173
  api_requests_per_minute?: number
170
174
  sandbox_requests_per_minute?: number
171
175
  ingest_requests_per_minute?: number
172
176
  }
173
- auth?: {
174
- allow_static_pats?: boolean
175
- }
176
- telemetry?: {
177
+ sandbox?: {
177
178
  enabled?: boolean
178
179
  }
180
+ api?: {
181
+ allow_static_pats?: boolean
182
+ }
179
183
  [key: string]: unknown
180
184
  }
181
185
  budget: {
@@ -187,18 +191,20 @@ export interface ConfigJson {
187
191
  [key: string]: unknown
188
192
  }
189
193
  brain?: {
194
+ url?: string
195
+ has_token?: boolean
196
+ token?: string
197
+ name?: string
198
+ role?: string
199
+ layer?: string
200
+ tags?: string[]
201
+ full_brain?: boolean
190
202
  preferred_agent?: string
191
203
  local_endpoint?: string
192
204
  openrouter_model?: string
193
205
  gemini_model?: string
194
206
  claude_model?: string
195
207
  openai_model?: string
196
- url?: string
197
- name?: string
198
- role?: string
199
- layer?: string
200
- tags?: string
201
- full_brain?: boolean
202
208
  }
203
209
  secrets?: {
204
210
  google_api_key?: string
@@ -0,0 +1,10 @@
1
+ const STORAGE_KEY = 'tr-local-config-v2'
2
+
3
+ export function isOnboardingComplete(): boolean {
4
+ return localStorage.getItem('tr-onboarding-complete') === '1'
5
+ }
6
+
7
+ export function resetOnboarding() {
8
+ localStorage.removeItem(STORAGE_KEY)
9
+ localStorage.removeItem('tr-onboarding-complete')
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.13.2",
3
+ "version": "3.14.4",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,8 @@
36
36
  "dev": "node src/server/index.mjs",
37
37
  "start": "node src/server/index.mjs",
38
38
  "test": "vitest run",
39
- "clean": "rm -rf scratch/* .remember/* *.log"
39
+ "clean": "rm -rf scratch/* .remember/* *.log",
40
+ "version": "node .agent/skills/push/scripts/sync-skill-versions.mjs && git add scaffold/.agent/skills/total-recall/SKILL.md"
40
41
  },
41
42
  "repository": {
42
43
  "type": "git",