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.
- package/frontend/dist/assets/{index-D5xRw5uq.js → index-B6AfuKv9.js} +126 -134
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/config.ts +16 -2
- package/frontend/src/api/keys.ts +111 -0
- package/frontend/src/api/notifications.ts +54 -6
- package/frontend/src/api/update.ts +41 -5
- package/frontend/src/components/BrainSelector.spec.tsx +71 -47
- package/frontend/src/components/BrainSelector.tsx +112 -35
- package/frontend/src/components/MobilePairing.spec.tsx +44 -14
- package/frontend/src/components/MobilePairing.tsx +229 -22
- package/frontend/src/pages/HealthPage.tsx +12 -16
- package/frontend/src/pages/NotificationsPage.tsx +6 -3
- package/frontend/src/pages/RulesPage.spec.tsx +46 -15
- package/frontend/src/pages/RulesPage.tsx +41 -9
- package/frontend/src/pages/SecretsPage.tsx +316 -3
- package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
- package/frontend/src/pages/SettingsPage.tsx +228 -86
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +17 -2
- package/src/cli/chat.mjs +1 -1
- package/src/cli/daemon.mjs +2 -1
- package/src/cli/dream.mjs +14 -10
- package/src/cli/forget.mjs +14 -2
- package/src/cli/friction.mjs +1 -1
- package/src/cli/import-rules.mjs +8 -3
- package/src/cli/ingest.mjs +3 -2
- package/src/cli/lint.mjs +9 -6
- package/src/cli/rebuild.mjs +9 -4
- package/src/cli/remember.mjs +21 -15
- package/src/cli/secret.mjs +184 -4
- package/src/cli/share.mjs +5 -3
- package/src/cli/skill-projection.mjs +21 -2
- package/src/cli/snapshot.mjs +10 -8
- package/src/cli/update.mjs +2 -1
- package/src/core/clarity-rewriter.mjs +31 -12
- package/src/core/config.mjs +10 -4
- package/src/core/daemon-loop.mjs +33 -6
- package/src/core/embeddings.mjs +51 -3
- package/src/core/memory-title.mjs +70 -0
- package/src/core/notifications.mjs +204 -34
- package/src/core/package-auto-update.mjs +99 -3
- package/src/core/pairing.mjs +168 -0
- package/src/core/provider-account-sync.mjs +1145 -0
- package/src/core/provider-catalog.mjs +11 -4
- package/src/core/research.mjs +98 -24
- package/src/core/runtime.mjs +11 -1
- package/src/core/scheduler.mjs +31 -9
- package/src/core/schema.mjs +23 -0
- package/src/core/secret-integration-research.mjs +325 -0
- package/src/core/secrets-store.mjs +284 -14
- package/src/core/secrets-sync.mjs +1 -1
- package/src/core/skills-registry.mjs +12 -0
- package/src/core/source-adapters.mjs +247 -22
- package/src/core/surface.mjs +4 -4
- package/src/core/task-executors.mjs +161 -14
- package/src/core/throttled-fetch.mjs +5 -0
- package/src/core/vault.mjs +7 -1
- package/src/core/webhook-handlers.mjs +123 -14
- package/src/server/auth.mjs +6 -0
- package/src/server/index.mjs +56 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +40 -0
- package/src/server/routes/_shared.mjs +33 -1
- package/src/server/routes/config.mjs +64 -23
- package/src/server/routes/context.mjs +14 -9
- package/src/server/routes/dashboard.mjs +39 -18
- package/src/server/routes/docs.mjs +27 -13
- package/src/server/routes/dream.mjs +10 -11
- package/src/server/routes/export.mjs +43 -12
- package/src/server/routes/field.mjs +11 -9
- package/src/server/routes/graph.mjs +31 -13
- package/src/server/routes/import.mjs +35 -16
- package/src/server/routes/instructions.mjs +41 -5
- package/src/server/routes/memory.mjs +61 -21
- package/src/server/routes/mesh.mjs +2 -1
- package/src/server/routes/notifications.mjs +103 -0
- package/src/server/routes/rules.mjs +75 -20
- package/src/server/routes/secrets.mjs +110 -4
- package/src/server/routes/share.mjs +2 -1
- package/src/server/routes/ssss.mjs +11 -3
- package/src/server/routes/system.mjs +20 -1
- package/src/server/routes/update.mjs +33 -7
- 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
|
|
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
|
+
}
|