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
@@ -10,7 +10,9 @@ import {
10
10
  installRegistrySkill,
11
11
  runSandbox,
12
12
  listResearch,
13
- createResearch
13
+ createResearch,
14
+ toggleSkillRepo,
15
+ auditSkill
14
16
  } from '../api';
15
17
  import type { ResearchItem } from '../types';
16
18
 
@@ -64,11 +66,6 @@ interface SubFileEntry {
64
66
 
65
67
  // ─── Automation tab types ───────────────────────────────────────────────────────
66
68
 
67
- interface ImprovementSuggestion {
68
- skillName: string;
69
- type: 'eval_gap' | 'missing_reference' | 'stale_script';
70
- description: string;
71
- }
72
69
 
73
70
  type TabId = 'active' | 'registry' | 'lifecycle' | 'network' | 'automation';
74
71
 
@@ -150,30 +147,13 @@ function computeHealthChecks(skill: SkillItem, content: string): HealthCheck[] {
150
147
  return [hasSkillMd, ...dirChecks, hasEvals, descCheck];
151
148
  }
152
149
 
153
- // ─── Helper: generate improvement suggestions ───────────────────────────────────
154
-
155
- function generateSuggestions(skills: SkillItem[]): ImprovementSuggestion[] {
156
- const suggestions: ImprovementSuggestion[] = [];
157
- skills.forEach(s => {
158
- const subs = (s.subSkills || []).map(x => x.toLowerCase());
159
- if (!subs.includes('evals')) {
160
- suggestions.push({ skillName: s.name, type: 'eval_gap', description: `Add evals directory with assertion tests for "${s.name}"` });
161
- }
162
- if (!subs.includes('references')) {
163
- suggestions.push({ skillName: s.name, type: 'missing_reference', description: `Add reference documentation for "${s.name}"` });
164
- }
165
- if (!subs.includes('scripts')) {
166
- suggestions.push({ skillName: s.name, type: 'stale_script', description: `Add automation scripts for "${s.name}"` });
167
- }
168
- });
169
- return suggestions.slice(0, 15); // Cap at 15
170
- }
150
+ // Removed generateSuggestions helper since we now use the backend AI audit
171
151
 
172
152
  // ─── Helper: format file sizes ──────────────────────────────────────────────────
173
153
 
174
154
 
175
155
 
176
- export default function SkillsPage() {
156
+ export default function SkillsPage({ activeBrainId }: { activeBrainId?: string }) {
177
157
  const [skills, setSkills] = useState<SkillItem[]>([]);
178
158
  const [repoGroups, setRepoGroups] = useState<RepoSkillsGroup[]>([]);
179
159
  const [selectedSkill, setSelectedSkill] = useState<SkillItem | null>(null);
@@ -216,12 +196,45 @@ export default function SkillsPage() {
216
196
  const [researchItems, setResearchItems] = useState<ResearchItem[]>([]);
217
197
  const [researchLoading, setResearchLoading] = useState(false);
218
198
  const [checkingUpdates, setCheckingUpdates] = useState(false);
219
- const improvementSuggestions = useMemo(() => skills.length > 0 ? generateSuggestions(skills) : [], [skills]);
199
+ const [aiSuggestions, setAiSuggestions] = useState<{skillName: string, description: string, problematic: boolean}[]>([]);
200
+ const [auditing, setAuditing] = useState(false);
201
+
202
+ const handleRunAudit = async () => {
203
+ if (auditing || skills.length === 0) return;
204
+ setAuditing(true);
205
+ setAiSuggestions([]);
206
+ try {
207
+ // Limit to max 5 to prevent long waits
208
+ const toAudit = skills.slice(0, 5);
209
+ const results = [];
210
+ for (const skill of toAudit) {
211
+ try {
212
+ const res = await auditSkill(skill.name, skill.repo || activeBrainId);
213
+ if (res.audit.outdated || res.audit.problematic) {
214
+ results.push({
215
+ skillName: skill.name,
216
+ description: res.audit.proposed_changes,
217
+ problematic: res.audit.problematic
218
+ });
219
+ }
220
+ } catch (e) {
221
+ console.error('Audit error for', skill.name, e);
222
+ }
223
+ }
224
+ setAiSuggestions(results);
225
+ if (results.length === 0) {
226
+ setSuccess('AI Audit complete: All checked skills are well-configured.');
227
+ setTimeout(() => setSuccess(null), 3000);
228
+ }
229
+ } finally {
230
+ setAuditing(false);
231
+ }
232
+ };
220
233
 
221
234
  /**
222
235
  * Syncs project skills by repo (excludes total-recall / tr-* system skills).
223
236
  */
224
- const fetchSkillsList = async (selectName?: string, selectRepo?: string) => {
237
+ const fetchSkillsList = useCallback(async (selectName?: string, selectRepo?: string) => {
225
238
  setLoading(true);
226
239
  setError(null);
227
240
  try {
@@ -249,6 +262,11 @@ export default function SkillsPage() {
249
262
  groups = (data.repos || []) as RepoSkillsGroup[];
250
263
  }
251
264
 
265
+ if (activeBrainId) {
266
+ groups = groups.filter(g => g.repo === 'Global' || g.repo === activeBrainId);
267
+ list = groups.flatMap(g => g.skills || []);
268
+ }
269
+
252
270
  setSkills(list);
253
271
  setRepoGroups(groups);
254
272
 
@@ -278,12 +296,12 @@ export default function SkillsPage() {
278
296
  } finally {
279
297
  setLoading(false);
280
298
  }
281
- };
299
+ }, [activeBrainId]);
282
300
 
283
301
  useEffect(() => {
284
- // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate data fetch on mount
302
+ // eslint-disable-next-line react-hooks/set-state-in-effect
285
303
  void fetchSkillsList();
286
- }, []);
304
+ }, [fetchSkillsList]);
287
305
 
288
306
 
289
307
 
@@ -466,8 +484,9 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
466
484
  try {
467
485
  const files = await fetchSkillFiles(selectedSkill.name, dir, selectedSkill.repo);
468
486
  setSubDirFiles(prev => ({ ...prev, [dir]: files }));
469
- } catch (err) {
487
+ } catch (err: unknown) {
470
488
  console.error('Failed to fetch skill files', err);
489
+ setError((err as Error).message || 'Failed to fetch skill files.');
471
490
  setSubDirFiles(prev => ({ ...prev, [dir]: [] }));
472
491
  }
473
492
  }
@@ -497,8 +516,8 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
497
516
  try {
498
517
  const res = await listResearch();
499
518
  setResearchItems(res.items || []);
500
- } catch {
501
- // Silently handle — research API might not be available
519
+ } catch (err: unknown) {
520
+ setError((err as Error).message || 'Failed to fetch research items.');
502
521
  } finally {
503
522
  setResearchLoading(false);
504
523
  }
@@ -506,6 +525,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
506
525
 
507
526
  useEffect(() => {
508
527
  if (activeTab === 'automation') {
528
+ // eslint-disable-next-line react-hooks/set-state-in-effect
509
529
  void handleFetchResearch();
510
530
  }
511
531
  }, [activeTab, handleFetchResearch]);
@@ -632,7 +652,26 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
632
652
  >
633
653
  {skill.name}
634
654
  </span>
635
- <button
655
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
656
+ {activeBrainId && activeBrainId !== 'Global' && group.repo === 'Global' && (
657
+ <input
658
+ type="checkbox"
659
+ checked={!!repoGroups.find(g => g.repo === activeBrainId)?.skills?.some(s => s.name === skill.name)}
660
+ onClick={(e) => e.stopPropagation()}
661
+ onChange={async (e) => {
662
+ e.stopPropagation();
663
+ try {
664
+ await toggleSkillRepo(skill.name, activeBrainId, e.target.checked);
665
+ void fetchSkillsList();
666
+ } catch (err) {
667
+ setError((err as Error).message);
668
+ }
669
+ }}
670
+ style={{ cursor: 'pointer' }}
671
+ title="Enable/Disable skill for this repo"
672
+ />
673
+ )}
674
+ <button
636
675
  type="button"
637
676
  onClick={(e) => {
638
677
  e.stopPropagation();
@@ -650,6 +689,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
650
689
  >
651
690
  ✖
652
691
  </button>
692
+ </div>
653
693
  </div>
654
694
  {skill.source && (
655
695
  <div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 4 }}>
@@ -1353,13 +1393,32 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
1353
1393
 
1354
1394
  {/* Skill Improvement Queue */}
1355
1395
  <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.5)', border: '1px solid var(--border)' }}>
1356
- <h3 id="automation-improvements-heading" style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 8 }}>
1357
- 💡 Skill Improvement Queue
1358
- </h3>
1396
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 8 }}>
1397
+ <h3 id="automation-improvements-heading" style={{ margin: 0, fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>
1398
+ 💡 AI Skill Improvement Queue
1399
+ </h3>
1400
+ <button
1401
+ onClick={handleRunAudit}
1402
+ disabled={auditing}
1403
+ style={{
1404
+ background: auditing ? 'var(--bg-secondary)' : 'linear-gradient(135deg, #10b981, #059669)',
1405
+ color: '#fff',
1406
+ border: 'none',
1407
+ borderRadius: 6,
1408
+ padding: '6px 14px',
1409
+ fontSize: 11,
1410
+ fontWeight: 600,
1411
+ cursor: auditing ? 'not-allowed' : 'pointer',
1412
+ flexShrink: 0,
1413
+ }}
1414
+ >
1415
+ {auditing ? '⏳ Auditing Skills...' : '🤖 Run AI Audit'}
1416
+ </button>
1417
+ </div>
1359
1418
 
1360
- {improvementSuggestions.length > 0 ? (
1419
+ {aiSuggestions.length > 0 ? (
1361
1420
  <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1362
- {improvementSuggestions.map((suggestion, idx) => (
1421
+ {aiSuggestions.map((suggestion, idx) => (
1363
1422
  <div
1364
1423
  key={idx}
1365
1424
  id={`automation-suggestion-${idx}`}
@@ -1384,14 +1443,11 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
1384
1443
  borderRadius: 10,
1385
1444
  textTransform: 'uppercase',
1386
1445
  letterSpacing: 0.3,
1387
- background: suggestion.type === 'eval_gap' ? 'rgba(245, 158, 11, 0.1)' :
1388
- suggestion.type === 'missing_reference' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(139, 92, 246, 0.1)',
1389
- color: suggestion.type === 'eval_gap' ? '#f59e0b' :
1390
- suggestion.type === 'missing_reference' ? '#60a5fa' : '#8b5cf6',
1391
- border: `1px solid ${suggestion.type === 'eval_gap' ? 'rgba(245, 158, 11, 0.2)' :
1392
- suggestion.type === 'missing_reference' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(139, 92, 246, 0.2)'}`,
1446
+ background: suggestion.problematic ? 'rgba(239, 68, 68, 0.1)' : 'rgba(245, 158, 11, 0.1)',
1447
+ color: suggestion.problematic ? '#f87171' : '#f59e0b',
1448
+ border: `1px solid ${suggestion.problematic ? 'rgba(239, 68, 68, 0.2)' : 'rgba(245, 158, 11, 0.2)'}`,
1393
1449
  }}>
1394
- {suggestion.type.replace(/_/g, ' ')}
1450
+ {suggestion.problematic ? 'Problematic' : 'Outdated'}
1395
1451
  </span>
1396
1452
  </div>
1397
1453
  <div style={{ fontSize: 11, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
@@ -14,7 +14,7 @@ import ResearchAgendaTab from '../components/ResearchAgendaTab'
14
14
  import type { ResearchItem } from '../components/ResearchAgendaTab'
15
15
  import DaemonLogsTab from '../components/DaemonLogsTab'
16
16
 
17
- export default function TasksPage() {
17
+ export default function TasksPage({ activeBrainId }: { activeBrainId?: string }) {
18
18
  const [tasks, setTasks] = useState<Task[]>([])
19
19
  const [researchItems, setResearchItems] = useState<ResearchItem[]>([])
20
20
  const [researchCounts, setResearchCounts] = useState({ pending: 0, in_progress: 0, done: 0, failed: 0 })
@@ -46,6 +46,7 @@ export default function TasksPage() {
46
46
  setLoadedDiscoveries(prev => ({ ...prev, [item.node_slug!]: node }))
47
47
  } catch (e) {
48
48
  console.error('Failed to load memory node for research', e)
49
+ setError(`Failed to load memory node: ${(e as Error).message}`)
49
50
  } finally {
50
51
  setLoadingNodeSlugs(prev => ({ ...prev, [item.node_slug!]: false }))
51
52
  }
@@ -63,6 +64,7 @@ export default function TasksPage() {
63
64
  const [showResearchForm, setShowResearchForm] = useState(false)
64
65
  const [researchTopic, setResearchTopic] = useState('')
65
66
  const [researchPriority, setResearchPriority] = useState<'low' | 'medium' | 'high' | 'critical'>('medium')
67
+
66
68
  const [researchNotes, setResearchNotes] = useState('')
67
69
  const [researchSubmitting, setResearchSubmitting] = useState(false)
68
70
 
@@ -73,68 +75,101 @@ export default function TasksPage() {
73
75
  setTasks(data || [])
74
76
  setError('')
75
77
  } catch (e) {
76
- setError((e as Error).message)
78
+ console.error(e)
79
+ setError(`Failed to fetch tasks: ${(e as Error).message}`)
77
80
  }
78
81
  }, [])
79
82
 
80
- // Fetch Research Agenda
81
- const fetchResearch = useCallback(async () => {
83
+ const fetchResearchItems = useCallback(async () => {
82
84
  try {
83
- const res = await listResearch('all')
85
+ const res = await listResearch()
84
86
  setResearchItems(res.items || [])
85
- setResearchCounts(res.counts || { pending: 0, in_progress: 0, done: 0, failed: 0 })
87
+ const counts = { pending: 0, in_progress: 0, done: 0, failed: 0 }
88
+ for (const item of (res.items || [])) {
89
+ if (counts[item.status as keyof typeof counts] !== undefined) {
90
+ counts[item.status as keyof typeof counts]++
91
+ }
92
+ }
93
+ setResearchCounts(counts)
86
94
  setError('')
87
95
  } catch (e) {
88
- setError((e as Error).message)
96
+ console.error(e)
97
+ setError(`Failed to fetch research: ${(e as Error).message}`)
89
98
  }
90
99
  }, [])
91
100
 
92
101
  // Fetch Daemon (Cognitive Engine) Logs
93
102
  const fetchDaemonLogs = useCallback(async () => {
103
+ if (activeTab !== 'logs') return
94
104
  try {
95
- const data = await fetchLogs('daemon')
105
+ const data = await fetchLogs("server")
96
106
  setLogs(data.content || '(no logs streamed yet)')
107
+ return true
97
108
  } catch (e) {
98
109
  setLogs(`Error connecting to cognitive logs: ${(e as Error).message}`)
110
+ return false
99
111
  }
100
- }, [])
112
+ }, [activeTab])
101
113
 
102
114
  // Combined Refresh
103
115
  const refreshAll = useCallback(async () => {
104
116
  setLoading(true)
105
- await Promise.all([fetchTasks(), fetchResearch(), fetchDaemonLogs()])
117
+ await Promise.all([fetchTasks(), fetchResearchItems(), fetchDaemonLogs()])
106
118
  setLoading(false)
107
- }, [fetchTasks, fetchResearch, fetchDaemonLogs])
119
+ }, [fetchTasks, fetchResearchItems, fetchDaemonLogs])
108
120
 
109
121
  // Polling loop
110
122
  useEffect(() => {
123
+ let timeoutId: ReturnType<typeof setTimeout>
124
+ let active = true
125
+ let failCount = 0
126
+
127
+ const poll = async () => {
128
+ if (!active) return
129
+
130
+ const results = await Promise.all([fetchTasks(), fetchResearchItems(), fetchDaemonLogs()])
131
+ const hasError = results.includes(false)
132
+
133
+ if (hasError) {
134
+ failCount++
135
+ } else {
136
+ failCount = 0
137
+ }
138
+
139
+ if (active) {
140
+ const delay = Math.min(3000 * Math.pow(2, failCount), 60000)
141
+ timeoutId = setTimeout(poll, delay)
142
+ }
143
+ }
144
+
111
145
  const init = async () => {
112
- await Promise.all([fetchTasks(), fetchResearch(), fetchDaemonLogs()])
146
+ await Promise.all([fetchTasks(), fetchResearchItems(), fetchDaemonLogs()])
113
147
  setLoading(false)
148
+ if (active) {
149
+ timeoutId = setTimeout(poll, 3000)
150
+ }
114
151
  }
115
152
  void init()
116
153
 
117
- const interval = setInterval(() => {
118
- void fetchTasks()
119
- void fetchResearch()
120
- void fetchDaemonLogs()
121
- }, 3000)
122
-
123
- return () => clearInterval(interval)
124
- }, [fetchTasks, fetchResearch, fetchDaemonLogs])
154
+ return () => {
155
+ active = false
156
+ clearTimeout(timeoutId)
157
+ }
158
+ }, [fetchTasks, fetchResearchItems, fetchDaemonLogs])
125
159
 
126
160
  // Trigger Brain Recompilation
127
161
  const handleRecompile = async () => {
162
+ if (recompiling) return
128
163
  setRecompiling(true)
129
164
  setError('')
130
165
  setActionSuccess('')
131
166
  try {
132
- const res = await triggerRecompile()
133
- setActionSuccess(res.message || 'Brain surface recompiled successfully!')
167
+ await triggerRecompile(activeBrainId)
168
+ setActionSuccess('Surface projections recompiled successfully')
134
169
  setTimeout(() => setActionSuccess(''), 4000)
135
170
  void refreshAll()
136
171
  } catch (e) {
137
- setError((e as Error).message)
172
+ setError(`Recompile failed: ${(e as Error).message}`)
138
173
  } finally {
139
174
  setRecompiling(false)
140
175
  }
@@ -219,7 +254,7 @@ export default function TasksPage() {
219
254
  setShowResearchForm(false)
220
255
  setActionSuccess('Research project enqueued successfully!')
221
256
  setTimeout(() => setActionSuccess(''), 3000)
222
- void fetchResearch()
257
+ void fetchResearchItems()
223
258
  } catch (err) {
224
259
  setError((err as Error).message)
225
260
  } finally {
@@ -380,7 +415,7 @@ export default function TasksPage() {
380
415
  handleToggleExpand={handleToggleExpand}
381
416
  loadedDiscoveries={loadedDiscoveries}
382
417
  loadingNodeSlugs={loadingNodeSlugs}
383
- refreshResearch={fetchResearch}
418
+ refreshResearch={fetchResearchItems}
384
419
  />
385
420
  )}
386
421