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
@@ -14,12 +14,16 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import os from 'node:os';
17
- import { spawnSync } from 'node:child_process';
17
+ import { spawn, spawnSync } from 'node:child_process';
18
18
  import { logger } from './logger.mjs';
19
19
 
20
20
  export const PACKAGE_NAME = 'total-recall-brain';
21
21
  export const DEFAULT_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
22
22
 
23
+ /** In-process cache so dashboard update checks never block the event loop on every load. */
24
+ let latestVersionCache = { value: null, fetchedAt: 0 };
25
+ const LATEST_VERSION_CACHE_MS = 5 * 60 * 1000;
26
+
23
27
  /**
24
28
  * @returns {boolean}
25
29
  */
@@ -183,21 +187,113 @@ export function inspectProjectPackage(projectRoot) {
183
187
  }
184
188
 
185
189
  /**
190
+ * Resolve latest npm version without blocking the Node event loop.
191
+ * Prefer this from HTTP handlers / daemons.
192
+ *
193
+ * @param {{ timeoutMs?: number, force?: boolean }} [opts]
194
+ * @returns {Promise<string|null>}
195
+ */
196
+ export function fetchLatestNpmVersionAsync(opts = {}) {
197
+ const timeout = opts.timeoutMs ?? 8_000;
198
+ const now = Date.now();
199
+ if (
200
+ !opts.force &&
201
+ latestVersionCache.value &&
202
+ now - latestVersionCache.fetchedAt < LATEST_VERSION_CACHE_MS
203
+ ) {
204
+ return Promise.resolve(latestVersionCache.value);
205
+ }
206
+
207
+ return new Promise((resolve) => {
208
+ let settled = false;
209
+ const finish = (value) => {
210
+ if (settled) return;
211
+ settled = true;
212
+ if (value) {
213
+ latestVersionCache = { value, fetchedAt: Date.now() };
214
+ }
215
+ resolve(value);
216
+ };
217
+
218
+ let child;
219
+ try {
220
+ child = spawn('npm', ['view', PACKAGE_NAME, 'version'], {
221
+ stdio: ['ignore', 'pipe', 'pipe'],
222
+ });
223
+ } catch (err) {
224
+ logger.debug('package-auto-update', 'npm view spawn failed', { error: err.message });
225
+ return finish(null);
226
+ }
227
+
228
+ let stdout = '';
229
+ let stderr = '';
230
+ child.stdout?.on('data', (chunk) => {
231
+ stdout += String(chunk);
232
+ });
233
+ child.stderr?.on('data', (chunk) => {
234
+ stderr += String(chunk);
235
+ });
236
+
237
+ const timer = setTimeout(() => {
238
+ try {
239
+ child.kill('SIGTERM');
240
+ } catch {
241
+ /* ignore */
242
+ }
243
+ logger.debug('package-auto-update', 'npm view timed out', { timeoutMs: timeout });
244
+ // Return stale cache if any
245
+ finish(latestVersionCache.value || null);
246
+ }, timeout);
247
+
248
+ child.on('error', (err) => {
249
+ clearTimeout(timer);
250
+ logger.debug('package-auto-update', 'npm view error', { error: err.message });
251
+ finish(latestVersionCache.value || null);
252
+ });
253
+
254
+ child.on('close', (code) => {
255
+ clearTimeout(timer);
256
+ if (code !== 0) {
257
+ if (stderr) {
258
+ logger.debug('package-auto-update', 'npm view failed', { code, stderr: stderr.slice(0, 200) });
259
+ }
260
+ return finish(latestVersionCache.value || null);
261
+ }
262
+ const v = stdout.trim();
263
+ finish(v || null);
264
+ });
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Sync npm view (CLI only). Blocks the process — do NOT call from Express handlers.
270
+ * Prefer fetchLatestNpmVersionAsync in server/daemon paths.
271
+ *
186
272
  * @param {{ timeoutMs?: number }} [opts]
187
273
  * @returns {string|null}
188
274
  */
189
275
  export function fetchLatestNpmVersion(opts = {}) {
190
- const timeout = opts.timeoutMs ?? 20_000;
276
+ const now = Date.now();
277
+ if (latestVersionCache.value && now - latestVersionCache.fetchedAt < LATEST_VERSION_CACHE_MS) {
278
+ return latestVersionCache.value;
279
+ }
280
+ const timeout = opts.timeoutMs ?? 8_000;
191
281
  const result = spawnSync('npm', ['view', PACKAGE_NAME, 'version'], {
192
282
  encoding: 'utf8',
193
283
  timeout,
194
284
  stdio: ['ignore', 'pipe', 'pipe'],
195
285
  });
196
- if (!result || result.status !== 0) return null;
286
+ if (!result || result.status !== 0) return latestVersionCache.value || null;
197
287
  const v = (result.stdout || '').trim();
288
+ if (v) latestVersionCache = { value: v, fetchedAt: Date.now() };
198
289
  return v || null;
199
290
  }
200
291
 
292
+ /** Test helper — clear version cache between specs. */
293
+ export function clearLatestNpmVersionCache() {
294
+ latestVersionCache = { value: null, fetchedAt: 0 };
295
+ }
296
+
201
297
  /**
202
298
  * Semver-ish compare: true if installed is missing or older than latest.
203
299
  * Handles simple x.y.z only (npm publish tags).
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Mobile pairing endpoints — reachable URLs for QR codes.
3
+ * Never hardcodes personal hostnames; derives from live interfaces + server port.
4
+ */
5
+ import { listLocalInterfaces, isLanIpv4, isOverlayIpv4 } from './network-interfaces.mjs';
6
+ import { getMeshIp } from './mesh.mjs';
7
+
8
+ /**
9
+ * @param {{
10
+ * port?: number,
11
+ * protocol?: 'http'|'https',
12
+ * listInterfaces?: () => Array,
13
+ * meshIp?: string|null,
14
+ * listenHosts?: string[],
15
+ * }} [opts]
16
+ */
17
+ export function buildPairingInfo(opts = {}) {
18
+ const port = Number(opts.port || process.env.TR_SERVER_PORT || process.env.PORT || 3000);
19
+ const protocol = opts.protocol === 'https' ? 'https' : 'http';
20
+ const interfaces = typeof opts.listInterfaces === 'function'
21
+ ? opts.listInterfaces()
22
+ : listLocalInterfaces();
23
+ const meshIp = opts.meshIp !== undefined ? opts.meshIp : getMeshIp();
24
+ const listenHosts = Array.isArray(opts.listenHosts)
25
+ ? opts.listenHosts
26
+ : deriveListenHosts(meshIp);
27
+
28
+ /** @type {Array<{ kind: string, label: string, ip: string, url: string, interface?: string, recommended: boolean, reachable_hint: string }>} */
29
+ const endpoints = [];
30
+ const seen = new Set();
31
+
32
+ const add = (kind, label, ip, ifaceName, recommended, hint) => {
33
+ if (!ip || seen.has(ip)) return;
34
+ seen.add(ip);
35
+ const hostPart = ip.includes(':') ? `[${ip}]` : ip;
36
+ endpoints.push({
37
+ kind,
38
+ label,
39
+ ip,
40
+ url: `${protocol}://${hostPart}:${port}`,
41
+ interface: ifaceName || null,
42
+ recommended: !!recommended,
43
+ reachable_hint: hint,
44
+ });
45
+ };
46
+
47
+ // LAN IPv4s first (phone on same Wi‑Fi)
48
+ for (const iface of interfaces) {
49
+ for (const addr of iface.addresses || []) {
50
+ if (addr.family !== 'IPv4' || addr.internal) continue;
51
+ if (addr.is_lan || isLanIpv4(addr.address)) {
52
+ add(
53
+ 'lan',
54
+ 'Local Wi‑Fi / LAN',
55
+ addr.address,
56
+ iface.name,
57
+ true,
58
+ 'Phone must be on the same Wi‑Fi. Brain must accept LAN connections (bind 0.0.0.0 or this IP).',
59
+ );
60
+ }
61
+ }
62
+ }
63
+
64
+ // Mesh / Tailscale overlay
65
+ if (meshIp && isOverlayIpv4(meshIp)) {
66
+ add(
67
+ 'mesh',
68
+ 'Tailscale / mesh',
69
+ meshIp,
70
+ null,
71
+ endpoints.length === 0,
72
+ 'Phone needs Tailscale (or mesh) installed and online on the same tailnet.',
73
+ );
74
+ }
75
+ for (const iface of interfaces) {
76
+ for (const addr of iface.addresses || []) {
77
+ if (addr.family !== 'IPv4' || addr.internal) continue;
78
+ if (addr.is_overlay || isOverlayIpv4(addr.address)) {
79
+ add(
80
+ 'mesh',
81
+ 'Tailscale / mesh',
82
+ addr.address,
83
+ iface.name,
84
+ endpoints.every((e) => e.kind !== 'lan'),
85
+ 'Phone needs Tailscale (or mesh) installed and online on the same tailnet.',
86
+ );
87
+ }
88
+ }
89
+ }
90
+
91
+ // Loopback last (only useful on the same machine)
92
+ add(
93
+ 'loopback',
94
+ 'This computer only',
95
+ '127.0.0.1',
96
+ 'lo',
97
+ false,
98
+ 'Only works on this machine — not for phone pairing.',
99
+ );
100
+
101
+ const lanBound = listenHosts.some((h) => h === '0.0.0.0' || isLanIpv4(h));
102
+ const meshBound = listenHosts.some((h) => h === meshIp || isOverlayIpv4(h) || h === '0.0.0.0');
103
+ const warnings = [];
104
+
105
+ if (!endpoints.some((e) => e.kind === 'lan')) {
106
+ warnings.push('No LAN IPv4 found — connect this machine to Wi‑Fi/Ethernet for same-network pairing.');
107
+ } else if (!lanBound) {
108
+ warnings.push(
109
+ 'Brain is not listening on LAN (only loopback/mesh). Phone on plain Wi‑Fi cannot use the LAN URL until the brain binds 0.0.0.0 (Allow Public Bind + restart) or you use Tailscale on the phone.',
110
+ );
111
+ }
112
+
113
+ // Prefer an address the server is actually listening on when possible.
114
+ const isListening = (ip) =>
115
+ listenHosts.includes(ip) ||
116
+ listenHosts.includes('0.0.0.0') ||
117
+ (ip === '127.0.0.1' && listenHosts.includes('127.0.0.1'));
118
+
119
+ for (const e of endpoints) {
120
+ e.recommended = false;
121
+ }
122
+
123
+ let preferred =
124
+ endpoints.find((e) => e.kind === 'lan' && lanBound && isListening(e.ip)) ||
125
+ endpoints.find((e) => e.kind === 'mesh' && (meshBound || isListening(e.ip))) ||
126
+ endpoints.find((e) => e.kind === 'lan') ||
127
+ endpoints.find((e) => e.kind === 'mesh') ||
128
+ endpoints.find((e) => e.kind === 'loopback') ||
129
+ endpoints[0] ||
130
+ null;
131
+
132
+ // When LAN exists but is not bound, prefer mesh if available (works today with Tailscale phones).
133
+ if (preferred?.kind === 'lan' && !lanBound) {
134
+ const meshEp = endpoints.find((e) => e.kind === 'mesh');
135
+ if (meshEp) preferred = meshEp;
136
+ }
137
+
138
+ if (preferred) {
139
+ for (const e of endpoints) e.recommended = e.ip === preferred.ip;
140
+ }
141
+
142
+ if (endpoints.some((e) => e.kind === 'mesh') && !meshBound && meshIp) {
143
+ warnings.push(`Mesh IP ${meshIp} is present but may not be the active listen address.`);
144
+ }
145
+
146
+ return {
147
+ port,
148
+ protocol,
149
+ listen_hosts: listenHosts,
150
+ preferred_url: preferred?.url || null,
151
+ endpoints,
152
+ warnings,
153
+ measured_at: new Date().toISOString(),
154
+ };
155
+ }
156
+
157
+ function deriveListenHosts(meshIp) {
158
+ const hosts = new Set(['127.0.0.1']);
159
+ if (meshIp) hosts.add(meshIp);
160
+ const envHost = process.env.HOST || process.env.TR_HOST;
161
+ if (envHost) hosts.add(envHost);
162
+ // Production dual-bind pattern in server/index.mjs
163
+ if (meshIp && meshIp !== '127.0.0.1') {
164
+ hosts.add(meshIp);
165
+ hosts.add('127.0.0.1');
166
+ }
167
+ return [...hosts];
168
+ }