total-recall-brain 3.18.2 → 3.19.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.
Files changed (83) hide show
  1. package/frontend/dist/assets/{index-D5xRw5uq.js → index-B6AfuKv9.js} +126 -134
  2. package/frontend/dist/index.html +1 -1
  3. package/frontend/src/api/config.ts +16 -2
  4. package/frontend/src/api/keys.ts +111 -0
  5. package/frontend/src/api/notifications.ts +54 -6
  6. package/frontend/src/api/update.ts +41 -5
  7. package/frontend/src/components/BrainSelector.spec.tsx +71 -47
  8. package/frontend/src/components/BrainSelector.tsx +112 -35
  9. package/frontend/src/components/MobilePairing.spec.tsx +44 -14
  10. package/frontend/src/components/MobilePairing.tsx +229 -22
  11. package/frontend/src/pages/HealthPage.tsx +12 -16
  12. package/frontend/src/pages/NotificationsPage.tsx +6 -3
  13. package/frontend/src/pages/RulesPage.spec.tsx +46 -15
  14. package/frontend/src/pages/RulesPage.tsx +41 -9
  15. package/frontend/src/pages/SecretsPage.tsx +316 -3
  16. package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
  17. package/frontend/src/pages/SettingsPage.tsx +228 -86
  18. package/package.json +1 -1
  19. package/src/cli/agent-dir.mjs +17 -2
  20. package/src/cli/chat.mjs +1 -1
  21. package/src/cli/daemon.mjs +2 -1
  22. package/src/cli/dream.mjs +14 -10
  23. package/src/cli/forget.mjs +14 -2
  24. package/src/cli/friction.mjs +1 -1
  25. package/src/cli/import-rules.mjs +8 -3
  26. package/src/cli/ingest.mjs +3 -2
  27. package/src/cli/lint.mjs +9 -6
  28. package/src/cli/rebuild.mjs +9 -4
  29. package/src/cli/remember.mjs +21 -15
  30. package/src/cli/secret.mjs +184 -4
  31. package/src/cli/share.mjs +5 -3
  32. package/src/cli/skill-projection.mjs +21 -2
  33. package/src/cli/snapshot.mjs +10 -8
  34. package/src/cli/update.mjs +2 -1
  35. package/src/core/clarity-rewriter.mjs +31 -12
  36. package/src/core/config.mjs +10 -4
  37. package/src/core/daemon-loop.mjs +33 -6
  38. package/src/core/embeddings.mjs +51 -3
  39. package/src/core/memory-title.mjs +70 -0
  40. package/src/core/notifications.mjs +204 -34
  41. package/src/core/package-auto-update.mjs +99 -3
  42. package/src/core/pairing.mjs +168 -0
  43. package/src/core/provider-account-sync.mjs +1145 -0
  44. package/src/core/provider-catalog.mjs +11 -4
  45. package/src/core/research.mjs +98 -24
  46. package/src/core/runtime.mjs +11 -1
  47. package/src/core/scheduler.mjs +31 -9
  48. package/src/core/schema.mjs +23 -0
  49. package/src/core/secret-integration-research.mjs +325 -0
  50. package/src/core/secrets-store.mjs +284 -14
  51. package/src/core/secrets-sync.mjs +1 -1
  52. package/src/core/skills-registry.mjs +12 -0
  53. package/src/core/source-adapters.mjs +247 -22
  54. package/src/core/surface.mjs +4 -4
  55. package/src/core/task-executors.mjs +161 -14
  56. package/src/core/throttled-fetch.mjs +5 -0
  57. package/src/core/vault.mjs +7 -1
  58. package/src/core/webhook-handlers.mjs +123 -14
  59. package/src/server/auth.mjs +6 -0
  60. package/src/server/index.mjs +56 -0
  61. package/src/server/rest.mjs +2 -0
  62. package/src/server/route-manifest.json +40 -0
  63. package/src/server/routes/_shared.mjs +33 -1
  64. package/src/server/routes/config.mjs +64 -23
  65. package/src/server/routes/context.mjs +14 -9
  66. package/src/server/routes/dashboard.mjs +39 -18
  67. package/src/server/routes/docs.mjs +27 -13
  68. package/src/server/routes/dream.mjs +10 -11
  69. package/src/server/routes/export.mjs +43 -12
  70. package/src/server/routes/field.mjs +11 -9
  71. package/src/server/routes/graph.mjs +31 -13
  72. package/src/server/routes/import.mjs +35 -16
  73. package/src/server/routes/instructions.mjs +41 -5
  74. package/src/server/routes/memory.mjs +61 -21
  75. package/src/server/routes/mesh.mjs +2 -1
  76. package/src/server/routes/notifications.mjs +103 -0
  77. package/src/server/routes/rules.mjs +75 -20
  78. package/src/server/routes/secrets.mjs +110 -4
  79. package/src/server/routes/share.mjs +2 -1
  80. package/src/server/routes/ssss.mjs +11 -3
  81. package/src/server/routes/system.mjs +20 -1
  82. package/src/server/routes/update.mjs +33 -7
  83. package/src/server/routes/vault.mjs +53 -28
@@ -1,11 +1,40 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { fetchConfigJson, saveConfigJson, runSandbox, fetchHealth, runAgentDiagnostics, checkUpdate, runUpdate, fetchBrains } from '../api';
2
+ import {
3
+ fetchConfigJson,
4
+ saveConfigJson,
5
+ fetchHealth,
6
+ runAgentDiagnostics,
7
+ checkUpdate,
8
+ runUpdate,
9
+ triggerRecompile,
10
+ apiFetch,
11
+ getApiBase,
12
+ } from '../api';
3
13
  import type { HealthData } from '../types';
4
14
  import type { ConfigJson } from '../types';
5
15
  import { MobilePairing } from '../components/MobilePairing';
6
16
 
17
+ function normalizeConfigJson(data: ConfigJson): ConfigJson {
18
+ const next = { ...data } as ConfigJson & Record<string, any>;
19
+ if (!next.security) next.security = {};
20
+ if (!next.security.dashboard) next.security.dashboard = {};
21
+ if (!next.security.api) next.security.api = {};
22
+ if (!next.security.network) next.security.network = {};
23
+ if (!next.security.bind) next.security.bind = {};
24
+ if (!next.security.rate_limits) next.security.rate_limits = {};
25
+ if (!next.security.sandbox) next.security.sandbox = {};
26
+ if (!next.security.privacy) next.security.privacy = {};
27
+ if (!next.budget) next.budget = { budget: {} };
28
+ if (!next.budget.budget) next.budget.budget = {};
29
+ if (!next.brain) next.brain = {};
30
+ if (!next.secrets) next.secrets = {};
31
+ return next;
32
+ }
33
+
7
34
  export default function SettingsPage({ activeBrainId }: { activeBrainId?: string }) {
8
35
  const [configData, setConfigData] = useState<ConfigJson | null>(null);
36
+ const [loadError, setLoadError] = useState<string | null>(null);
37
+ const [loading, setLoading] = useState(true);
9
38
  const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
10
39
  const [sandboxLog, setSandboxLog] = useState('');
11
40
 
@@ -14,35 +43,43 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
14
43
  const [runningDiagnostics, setRunningDiagnostics] = useState(false);
15
44
  const [diagnosticLogs, setDiagnosticLogs] = useState<string | null>(null);
16
45
 
17
- const [updateInfo, setUpdateInfo] = useState<{ updateAvailable: boolean, latestVersion?: string } | null>(null);
46
+ const [updateInfo, setUpdateInfo] = useState<{
47
+ updateAvailable: boolean;
48
+ latestVersion?: string;
49
+ currentVersion?: string;
50
+ } | null>(null);
18
51
  const [updating, setUpdating] = useState(false);
19
52
  const [updateMessage, setUpdateMessage] = useState<string | null>(null);
53
+ const [rebuilding, setRebuilding] = useState(false);
20
54
 
21
- const [brains, setBrains] = useState<unknown[]>([]);
22
- const activeBrain = activeBrainId || localStorage.getItem('total-recall-active-brain') || '';
55
+ const loadConfig = async () => {
56
+ setLoading(true);
57
+ setLoadError(null);
58
+ try {
59
+ const data = await fetchConfigJson();
60
+ setConfigData(normalizeConfigJson(data));
61
+ } catch (err) {
62
+ console.error(err);
63
+ setLoadError(err instanceof Error ? err.message : 'Failed to load settings');
64
+ setConfigData(null);
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
23
69
 
24
70
  useEffect(() => {
25
71
  fetchHealth().then(setHealth).catch(console.error);
26
- checkUpdate().then(setUpdateInfo).catch(console.error);
27
- fetchBrains().then(setBrains).catch(console.error);
28
- fetchConfigJson()
29
- .then(data => {
30
- // Ensure all nested structures exist for controlled inputs
31
- if (!data.security) data.security = {};
32
- if (!data.security.dashboard) data.security.dashboard = {};
33
- if (!data.security.api) data.security.api = {};
34
- if (!data.security.network) data.security.network = {};
35
- if (!data.security.bind) data.security.bind = {};
36
- if (!data.security.rate_limits) data.security.rate_limits = {};
37
- if (!data.security.sandbox) data.security.sandbox = {};
38
- if (!data.security.privacy) data.security.privacy = {};
39
- if (!data.budget) data.budget = { budget: {} };
40
- if (!data.budget.budget) data.budget.budget = {};
41
- if (!data.brain) data.brain = {};
42
- if (!data.secrets) data.secrets = {};
43
- setConfigData(data);
44
- })
72
+ // Update check must never block config load (server used to spawnSync npm view).
73
+ checkUpdate()
74
+ .then((u) =>
75
+ setUpdateInfo({
76
+ updateAvailable: !!u.updateAvailable,
77
+ latestVersion: u.latestVersion,
78
+ currentVersion: u.currentVersion,
79
+ }),
80
+ )
45
81
  .catch(console.error);
82
+ loadConfig();
46
83
  }, [activeBrainId]);
47
84
 
48
85
 
@@ -54,6 +91,12 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
54
91
  { id: 'grok', name: 'Grok CLI', desc: 'xAI developer binary integration' }
55
92
  ];
56
93
 
94
+ const availableAgents: string[] = Array.isArray(health?.cli_agents)
95
+ ? health.cli_agents
96
+ : Array.isArray((health as unknown as { agents?: string[] } | null)?.agents)
97
+ ? ((health as unknown as { agents: string[] }).agents)
98
+ : [];
99
+
57
100
  const handleRunDiagnostics = async (e: React.FormEvent) => {
58
101
  e.preventDefault();
59
102
  setRunningDiagnostics(true);
@@ -74,7 +117,36 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
74
117
  setUpdateMessage(null);
75
118
  try {
76
119
  const res = await runUpdate();
77
- setUpdateMessage(res.success ? 'Update complete. Restarting...' : 'Update failed: ' + res.message);
120
+ if (res.success) {
121
+ const s = res.summary;
122
+ const bits = [
123
+ s?.updated != null ? `${s.updated} updated` : null,
124
+ s?.up_to_date != null ? `${s.up_to_date} current` : null,
125
+ s?.latest ? `latest ${s.latest}` : null,
126
+ ].filter(Boolean);
127
+ setUpdateMessage(
128
+ bits.length
129
+ ? `Update complete — ${bits.join(' Ā· ')}. Restart daemon/server if this host still runs old code.`
130
+ : res.message || 'Update complete.',
131
+ );
132
+ } else {
133
+ const failDetail = (res.summary?.results || [])
134
+ .filter((r) => r.status === 'failed')
135
+ .map((r) => `${r.name}: ${r.error || 'failed'}`)
136
+ .slice(0, 3)
137
+ .join('; ');
138
+ setUpdateMessage(
139
+ `Update failed: ${res.message}${failDetail ? ` (${failDetail})` : ''}`,
140
+ );
141
+ }
142
+ const refreshed = await checkUpdate().catch(() => null);
143
+ if (refreshed) {
144
+ setUpdateInfo({
145
+ updateAvailable: !!refreshed.updateAvailable,
146
+ latestVersion: refreshed.latestVersion,
147
+ currentVersion: refreshed.currentVersion,
148
+ });
149
+ }
78
150
  } catch (err: unknown) {
79
151
  setUpdateMessage('Update error: ' + (err as Error).message);
80
152
  } finally {
@@ -86,7 +158,18 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
86
158
  if (!configData) return;
87
159
  setSaveStatus('saving');
88
160
  try {
89
- await saveConfigJson(configData);
161
+ // Do not persist masked secret placeholders back into the store.
162
+ const payload = { ...configData } as ConfigJson & { secrets?: Record<string, string> };
163
+ if (payload.secrets) {
164
+ const cleaned: Record<string, string> = {};
165
+ for (const [k, v] of Object.entries(payload.secrets)) {
166
+ if (typeof v === 'string' && v && !v.includes('•') && v !== '[redacted]') {
167
+ cleaned[k] = v;
168
+ }
169
+ }
170
+ payload.secrets = cleaned;
171
+ }
172
+ await saveConfigJson(payload);
90
173
  setSaveStatus('saved');
91
174
  setTimeout(() => setSaveStatus('idle'), 2500);
92
175
  } catch (err) {
@@ -96,18 +179,39 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
96
179
  }
97
180
  };
98
181
 
99
- const executeSandboxCommand = async (command: string, description: string) => {
100
- setSandboxLog(`[Running] ${description}...\n$ ${command}`);
101
- const res = await runSandbox(`
102
- const { execSync } = require('child_process');
103
- try {
104
- const out = execSync('${command}', { encoding: 'utf8' });
105
- console.log(out);
106
- } catch (e) {
107
- console.error(e.stdout || e.message);
108
- }
109
- `);
110
- setSandboxLog(`[Finished] ${description}\n\n` + res.output);
182
+ const handleRebuildIndex = async () => {
183
+ setRebuilding(true);
184
+ setSandboxLog('[Running] Rebuilding instruction surfaces…');
185
+ try {
186
+ const res = await triggerRecompile();
187
+ setSandboxLog(
188
+ `[Finished] Rebuilding instruction surfaces\n\n${res.message || (res.success ? 'Compile complete.' : 'Compile finished with warnings.')}`,
189
+ );
190
+ } catch (err) {
191
+ setSandboxLog(`[Failed] Rebuild\n\n${err instanceof Error ? err.message : String(err)}`);
192
+ } finally {
193
+ setRebuilding(false);
194
+ }
195
+ };
196
+
197
+ const handleDownloadExtension = async () => {
198
+ try {
199
+ const res = await apiFetch(`${getApiBase()}/api/extension/download`);
200
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
201
+ const blob = await res.blob();
202
+ const url = URL.createObjectURL(blob);
203
+ const a = document.createElement('a');
204
+ a.href = url;
205
+ a.download = 'total-recall-extension.zip';
206
+ document.body.appendChild(a);
207
+ a.click();
208
+ a.remove();
209
+ URL.revokeObjectURL(url);
210
+ } catch (err) {
211
+ setSandboxLog(
212
+ `[Failed] Extension download\n\n${err instanceof Error ? err.message : String(err)}`,
213
+ );
214
+ }
111
215
  };
112
216
 
113
217
  // State Update Helpers
@@ -153,12 +257,16 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
153
257
 
154
258
  // Instead of parsing it back and forth on every keystroke,
155
259
  // we just use simple helpers that don't aggressively trim trailing commas.
156
- const getCsv = (arr: unknown) => Array.isArray(arr) ? arr.join(', ') : '';
157
- const setCsv = (str: string) => str.split(',').map(s => s.trimStart());
158
-
159
- if (!configData) {
260
+ const getCsv = (arr: unknown) => Array.isArray(arr) ? arr.filter(Boolean).join(', ') : '';
261
+ const setCsv = (str: string) =>
262
+ str
263
+ .split(',')
264
+ .map((s) => s.trim())
265
+ .filter((s) => s.length > 0);
266
+
267
+ if (loading && !configData) {
160
268
  return (
161
- <div style={{ padding: 40, color: 'var(--text-secondary)' }}>
269
+ <div style={{ padding: 40, color: 'var(--text-secondary)' }} data-testid="settings-loading">
162
270
  <div className="glass-card" style={{ display: 'inline-block', padding: '12px 24px' }}>
163
271
  Loading Settings...
164
272
  </div>
@@ -166,8 +274,38 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
166
274
  );
167
275
  }
168
276
 
277
+ if (loadError && !configData) {
278
+ return (
279
+ <div style={{ padding: 40 }} data-testid="settings-error" role="alert">
280
+ <div
281
+ className="glass-card"
282
+ style={{
283
+ display: 'inline-block',
284
+ padding: '16px 24px',
285
+ border: '1px solid #ef4444',
286
+ color: '#ef4444',
287
+ }}
288
+ >
289
+ Failed to load settings: {loadError}
290
+ <button
291
+ type="button"
292
+ className="btn-primary"
293
+ style={{ marginLeft: 16 }}
294
+ onClick={() => loadConfig()}
295
+ >
296
+ Retry
297
+ </button>
298
+ </div>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ if (!configData) {
304
+ return null;
305
+ }
306
+
169
307
  return (
170
- <div className="settings-page-wrapper">
308
+ <div className="settings-page-wrapper" data-testid="settings-page">
171
309
  <style>{`
172
310
  .settings-page-wrapper {
173
311
  padding: 32px 40px 100px;
@@ -627,7 +765,12 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
627
765
  step="0.01"
628
766
  className="settings-input"
629
767
  value={configData.budget.budget?.daily_cap_usd ?? 5.0}
630
- onChange={(e) => updateBudgetProp('monthly_soft_cap', e.target.value === '' ? null : Number(e.target.value))}
768
+ onChange={(e) =>
769
+ updateBudgetProp(
770
+ 'daily_cap_usd',
771
+ e.target.value === '' ? null : Number(e.target.value),
772
+ )
773
+ }
631
774
  />
632
775
  </div>
633
776
  <div className="field-col" style={{ flex: 1 }}>
@@ -638,7 +781,12 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
638
781
  step="0.01"
639
782
  className="settings-input"
640
783
  value={configData.budget.budget?.weekly_cap_usd ?? 20.0}
641
- onChange={(e) => updateBudgetProp('weekly_cap_usd', parseFloat(e.target.value))}
784
+ onChange={(e) =>
785
+ updateBudgetProp(
786
+ 'weekly_cap_usd',
787
+ e.target.value === '' ? null : Number(e.target.value),
788
+ )
789
+ }
642
790
  />
643
791
  </div>
644
792
  </div>
@@ -654,28 +802,10 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
654
802
  </div>
655
803
  </div>
656
804
 
657
- <div className="field-col">
658
- <label className="field-label" style={{ color: 'var(--text-accent)', fontWeight: 600 }}>Select Active Brain</label>
659
- <select
660
- className="settings-select"
661
- style={{ border: '1px solid var(--border-accent)', background: 'rgba(59, 130, 246, 0.05)' }}
662
- value={activeBrain}
663
- onChange={(e) => {
664
- const val = e.target.value;
665
- if (val) {
666
- localStorage.setItem('total-recall-active-brain', val);
667
- } else {
668
- localStorage.removeItem('total-recall-active-brain');
669
- }
670
- window.location.reload();
671
- }}
672
- >
673
- <option value="">Global Brain (Root)</option>
674
- {brains.map((b: any) => (
675
- <option key={b.name} value={b.name}>{b.name} ({b.role} - {b.nodes} nodes)</option>
676
- ))}
677
- </select>
678
- </div>
805
+ <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '0 0 12px', lineHeight: 1.5 }}>
806
+ Active vault layer is chosen in the <strong>sidebar brain selector</strong> (bottom left).
807
+ Fields below edit this install’s <code>brain.json</code> identity — not the active layer.
808
+ </p>
679
809
 
680
810
  <div style={{ display: 'flex', gap: 16 }}>
681
811
  <div className="field-col" style={{ flex: 1 }}>
@@ -765,7 +895,7 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
765
895
  <option value="antigravity">Antigravity (Google Deepmind)</option>
766
896
  <option value="gemini">Gemini (Google)</option>
767
897
  <option value="claude">Claude (Anthropic)</option>
768
- <option value="openai">OpenAI (O-Series)</option>
898
+ <option value="codex">Codex (OpenAI)</option>
769
899
  <option value="grok">Grok (xAI)</option>
770
900
  </select>
771
901
  </div>
@@ -788,22 +918,30 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
788
918
  <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 16px', lineHeight: 1.5 }}>
789
919
  Capture context directly from your browser. Install from the Chrome Web Store or load unpacked from <code>/extension</code> directory.
790
920
  </p>
791
- <a href="/api/extension/download" download className="btn-primary" style={{ display: 'block', textAlign: 'center', textDecoration: 'none', width: '100%', boxSizing: 'border-box' }}>Download Extension</a>
921
+ <button
922
+ type="button"
923
+ className="btn-primary"
924
+ style={{ display: 'block', width: '100%', boxSizing: 'border-box' }}
925
+ onClick={handleDownloadExtension}
926
+ >
927
+ Download Extension
928
+ </button>
792
929
  </div>
793
930
 
794
931
  <div style={{ background: 'rgba(255,255,255,0.03)', padding: 20, borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)' }}>
795
932
  <div style={{ fontSize: 24, marginBottom: 12 }}>šŸ› ļø</div>
796
933
  <h4 style={{ margin: '0 0 8px', fontSize: 15 }}>Developer Utilities</h4>
797
934
  <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 16px', lineHeight: 1.5 }}>
798
- Run background maintenance tasks or re-index the local search tree.
935
+ Rebuild compiled instruction surfaces (vault compile) without a shell sandbox.
799
936
  </p>
800
937
  <div style={{ display: 'flex', gap: 12 }}>
801
938
  <button
802
939
  className="btn-secondary"
803
940
  style={{ flex: 1 }}
804
- onClick={() => executeSandboxCommand('npx total-recall compile', 'Rebuilding Search Indexes')}
941
+ disabled={rebuilding}
942
+ onClick={handleRebuildIndex}
805
943
  >
806
- Rebuild Index
944
+ {rebuilding ? 'Rebuilding…' : 'Rebuild Index'}
807
945
  </button>
808
946
  </div>
809
947
  </div>
@@ -826,13 +964,19 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
826
964
  </div>
827
965
  </div>
828
966
 
829
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 24, padding: '16px 20px', background: 'rgba(0,0,0,0.2)', borderRadius: 12, border: '1px solid rgba(255,255,255,0.05)' }}>
967
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 24, padding: '16px 20px', background: 'rgba(0,0,0,0.2)', borderRadius: 12, border: '1px solid rgba(255,255,255,0.05)', gap: 16, flexWrap: 'wrap' }}>
830
968
  <div>
831
969
  <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>
832
- {updateInfo?.updateAvailable ? `Update Available (v${updateInfo.latestVersion})` : 'System is up to date'}
970
+ {updateInfo?.updateAvailable
971
+ ? `Update available (latest v${updateInfo.latestVersion || '?'})`
972
+ : 'Host package is up to date'}
833
973
  </div>
834
974
  <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 4 }}>
835
- {updateInfo?.updateAvailable ? 'A new version of Total Recall is available for download.' : 'You are running the latest version.'}
975
+ Running v{updateInfo?.currentVersion || health?.version || '—'}
976
+ {updateInfo?.latestVersion ? ` Ā· npm latest v${updateInfo.latestVersion}` : ''}
977
+ {updateInfo?.updateAvailable
978
+ ? ' Ā· registered projects or host package can be upgraded'
979
+ : ''}
836
980
  </div>
837
981
  {updateMessage && (
838
982
  <div style={{ fontSize: 12, color: updateMessage.includes('error') || updateMessage.includes('failed') ? '#ef4444' : '#10b981', marginTop: 8 }}>
@@ -840,16 +984,14 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
840
984
  </div>
841
985
  )}
842
986
  </div>
843
- {updateInfo?.updateAvailable && (
844
- <button
845
- className="btn-primary"
846
- onClick={handleRunUpdate}
847
- disabled={updating}
848
- style={{ background: '#3fb950', border: 'none' }}
849
- >
850
- {updating ? 'Updating...' : 'Download & Restart'}
851
- </button>
852
- )}
987
+ <button
988
+ className="btn-primary"
989
+ onClick={handleRunUpdate}
990
+ disabled={updating}
991
+ style={{ background: updateInfo?.updateAvailable ? '#3fb950' : undefined, border: 'none' }}
992
+ >
993
+ {updating ? 'Updating…' : updateInfo?.updateAvailable ? 'Apply Updates' : 'Check & Sync Projects'}
994
+ </button>
853
995
  </div>
854
996
  </div>
855
997
 
@@ -868,7 +1010,7 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
868
1010
 
869
1011
  <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
870
1012
  {AGENTS_LIST.map((a) => {
871
- const activeAgent = health?.cli_agents?.includes(a.id);
1013
+ const activeAgent = availableAgents.includes(a.id);
872
1014
  return (
873
1015
  <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 8, border: '1px solid var(--border)' }}>
874
1016
  <div style={{ flex: 1 }}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.18.2",
3
+ "version": "3.19.0",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -56,7 +56,8 @@ export function detectProjectBrain(startDir = process.cwd()) {
56
56
  if (fs.existsSync(path.join(c.candidate, 'SKILL.md')) || fs.existsSync(path.join(c.candidate, 'memory-vault'))) {
57
57
  return {
58
58
  agentDir: c.dir,
59
- brainDir: getGlobalBrainDir(),
59
+ // Project brain lives at the detected candidate — never alias to global.
60
+ brainDir: c.candidate,
60
61
  projectRoot: dir,
61
62
  };
62
63
  }
@@ -90,7 +91,21 @@ export function resolveAgentDir(layer = 'auto', targetPath = process.cwd()) {
90
91
  * @param {'global' | 'project' | 'auto'} [layer='auto']
91
92
  */
92
93
  export function resolveBrainDir(layer = 'auto', targetPath = process.cwd()) {
93
- return getGlobalBrainDir();
94
+ // Match resolveAgentDir: AGENT_DIR override pins the agent root → brain under it.
95
+ if (process.env.AGENT_DIR) {
96
+ return path.join(process.env.AGENT_DIR, 'skills', 'total-recall');
97
+ }
98
+ if (layer === 'global') return getGlobalBrainDir();
99
+ if (layer === 'project') {
100
+ const project = detectProjectBrain(targetPath);
101
+ if (!project) {
102
+ throw new Error('No project brain found. Run `npx total-recall init --project` to create one.');
103
+ }
104
+ return project.brainDir;
105
+ }
106
+ // Auto: project-local wins if a project brain exists
107
+ const project = detectProjectBrain(targetPath);
108
+ return project ? project.brainDir : getGlobalBrainDir();
94
109
  }
95
110
 
96
111
  /**
package/src/cli/chat.mjs CHANGED
@@ -10,7 +10,7 @@ import { callLocalRuntime, loadRuntimeConfig, checkRuntimeHealth } from '../core
10
10
  export default async function runChat(args) {
11
11
  console.log('šŸ¤– Total Recall CLI Chat initialized. Type "exit" to quit.\n');
12
12
 
13
- const configDir = path.join(resolveBrainDir(), 'config');
13
+ const configDir = path.join(resolveBrainDir('global'), 'config');
14
14
 
15
15
  let config;
16
16
  let caller;
@@ -22,7 +22,8 @@ import { startDaemon, stopDaemon, readPid } from '../core/daemon-control.mjs';
22
22
 
23
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
24
  const ROOT = path.join(__dirname, '..', '..');
25
- const BRAIN_DIR = resolveBrainDir();
25
+ // System daemon always targets the global brain — never the cwd project vault.
26
+ const BRAIN_DIR = resolveBrainDir('global');
26
27
  const LOG_FILE = path.join(BRAIN_DIR, 'logs', 'daemon.log');
27
28
 
28
29
  function commandExists(cmd) {
package/src/cli/dream.mjs CHANGED
@@ -13,11 +13,9 @@
13
13
 
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
16
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
17
17
 
18
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
- const AGENT_DIR = resolveAgentDir();
20
- const BRAIN_DIR = resolveBrainDir();
21
19
 
22
20
  function parseArgs(args) {
23
21
  const opts = { vault: null, help: false };
@@ -42,20 +40,26 @@ function printHelp() {
42
40
  Usage: total-recall dream [options]
43
41
 
44
42
  Options:
45
- --vault <path> Override vault directory (default: ~/.agent/memory-vault)
43
+ --vault <path> Override vault directory
44
+ --global Dream the global brain (default)
45
+ --project Dream the project brain for the current repo
46
46
  --help, -h Show this help
47
47
  `);
48
48
  }
49
49
 
50
50
  export default async function dream(args) {
51
- const opts = parseArgs(args);
51
+ const { layer, remainingArgs } = parseLayerFlag(args);
52
+ const effectiveLayer = layer === 'auto' ? 'global' : layer;
53
+ const opts = parseArgs(remainingArgs);
52
54
  if (opts.help) { printHelp(); return; }
53
55
 
54
- const vaultDir = opts.vault || path.join(BRAIN_DIR, 'memory-vault');
55
- const skillsDir = path.join(AGENT_DIR, 'skills');
56
- const derivedDir = path.join(BRAIN_DIR, 'memory-derived');
57
- const conflictsDir = path.join(BRAIN_DIR, 'memory-inbox', 'conflicts');
58
- const instructionsFile = path.join(AGENT_DIR, 'INSTRUCTIONS.md');
56
+ const agentDir = resolveAgentDir(effectiveLayer);
57
+ const brainDir = resolveBrainDir(effectiveLayer);
58
+ const vaultDir = opts.vault || path.join(brainDir, 'memory-vault');
59
+ const skillsDir = path.join(agentDir, 'skills');
60
+ const derivedDir = path.join(brainDir, 'memory-derived');
61
+ const conflictsDir = path.join(brainDir, 'memory-inbox', 'conflicts');
62
+ const instructionsFile = path.join(agentDir, 'INSTRUCTIONS.md');
59
63
 
60
64
  console.error('\n šŸŒ™ Starting dream cycle...\n');
61
65
  const startTime = Date.now();
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains } from './agent-dir.mjs';
4
4
  import { compileSurface } from '../core/surface.mjs';
5
5
  import { deleteNode } from '../core/vault.mjs';
6
+ import { invalidate } from '../core/vault-cache.mjs';
6
7
 
7
8
  function printHelp() {
8
9
  console.log(`
@@ -65,12 +66,16 @@ export default async function forget(args) {
65
66
  }
66
67
 
67
68
  console.log(` āœ… Soft-deleted ${count} nodes for project "${projectName}" (moved to .trash/)`);
69
+
70
+ // Files moved under .trash still invalidate the vault cache (watcher may lag)
71
+ invalidate(globalVaultDir);
68
72
 
69
73
  if (!remainingArgs.includes('--no-compile')) {
70
74
  console.log(' ā³ Recompiling active memory surfaces and indexes in the background...');
71
75
  try {
72
76
  const { spawn } = await import('node:child_process');
73
- const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
77
+ // Soft-delete from global vault tags — recompile global surfaces
78
+ const child = spawn(process.argv[0], [process.argv[1], 'compile', '--global'], {
74
79
  detached: true,
75
80
  stdio: 'ignore'
76
81
  });
@@ -130,6 +135,9 @@ export default async function forget(args) {
130
135
  process.exit(1);
131
136
  }
132
137
 
138
+ // Drop cached nodes immediately (fs.watch may lag or miss same-process deletes)
139
+ invalidate(vaultDir);
140
+
133
141
  console.log(` āœ… Deleted memory node "${slug}" from ${layerLabel} vault.`);
134
142
 
135
143
  // Recompile unless --no-compile
@@ -137,7 +145,11 @@ export default async function forget(args) {
137
145
  console.log(' ā³ Recompiling active memory surfaces and indexes in the background...');
138
146
  try {
139
147
  const { spawn } = await import('node:child_process');
140
- const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
148
+ // Pass layer so project forget recompiles the project brain, not auto/wrong vault
149
+ const compileArgs = [process.argv[1], 'compile'];
150
+ if (layer === 'project') compileArgs.push('--project');
151
+ else if (layer === 'global') compileArgs.push('--global');
152
+ const child = spawn(process.argv[0], compileArgs, {
141
153
  detached: true,
142
154
  stdio: 'ignore'
143
155
  });
@@ -9,7 +9,7 @@ export default async function runFriction(args) {
9
9
  console.log(`[Friction] šŸ” Analyzing logs for workflow bottlenecks...`);
10
10
 
11
11
  // Default path for watchdog logs
12
- const logPath = args[0] || path.join(resolveBrainDir(), 'logs', 'watchdog.jsonl');
12
+ const logPath = args[0] || path.join(resolveBrainDir('global'), 'logs', 'watchdog.jsonl');
13
13
 
14
14
  const result = await detectFriction(logPath);
15
15
  if (!result) {
@@ -8,10 +8,11 @@
8
8
 
9
9
  import path from 'node:path';
10
10
  import { detectAndImport } from '../core/import-rules.mjs';
11
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
11
+ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
12
12
 
13
13
  export async function run(argv = []) {
14
- const args = argv.slice(3); // strip node, script, 'import'
14
+ const rawArgs = argv.slice(3); // strip node, script, 'import'
15
+ const { layer, remainingArgs: args } = parseLayerFlag(rawArgs);
15
16
 
16
17
  const dirs = [];
17
18
  let force = false;
@@ -33,7 +34,8 @@ export async function run(argv = []) {
33
34
  // Default: search cwd
34
35
  if (dirs.length === 0) dirs.push(process.cwd());
35
36
 
36
- const brainDir = resolveBrainDir();
37
+ // auto → project vault when present (import rules for this repo)
38
+ const brainDir = resolveBrainDir(layer);
37
39
  const vaultDir = path.join(brainDir, 'memory-vault');
38
40
 
39
41
  console.log(`\nšŸ” Scanning for rule files in: ${dirs.join(', ')}\n`);
@@ -90,12 +92,15 @@ all future AI sessions.
90
92
 
91
93
  Options:
92
94
  --dir <path> Directory to search (default: current directory)
95
+ --global Import into the global vault
96
+ --project Import into the project vault (default: auto when present)
93
97
  --force Re-import files that were previously imported
94
98
  --dry-run Show what would be imported without writing anything
95
99
  --help Show this help
96
100
 
97
101
  Examples:
98
102
  npx total-recall import
103
+ npx total-recall import --project
99
104
  npx total-recall import --dir ~/my-project
100
105
  npx total-recall import --force --dry-run
101
106
  `);