total-recall-brain 3.12.2 → 3.13.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.
@@ -806,6 +806,7 @@ router.get('/api/import/rules', requireAuth, requireScope('memory:read'), (req,
806
806
  if (dirs.length === 0) return badRequest(res, 'No permitted directories specified');
807
807
  const detected = detectRuleFiles(dirs);
808
808
  res.json({ dirs, detected });
809
+
809
810
  } catch (err) { serverError(res, err); }
810
811
  });
811
812
 
@@ -825,6 +826,7 @@ router.post('/api/import/rules', requireAuth, requireScope('memory:write'), (req
825
826
  if (dryRun) return res.json({ dryRun: true, detected, toImport, imported: [], skipped: [], failed: [] });
826
827
  const result = importRuleFiles(toImport, { force, vaultDir: VAULT_DIR });
827
828
  res.json({ detected, ...result });
829
+
828
830
  } catch (err) { serverError(res, err); }
829
831
  });
830
832
 
@@ -859,6 +861,7 @@ router.get('/api/brain/export', requireAuth, requireScope('brain:export'), async
859
861
  tar.stdout.pipe(res);
860
862
  tar.on('error', err => { if (!res.headersSent) serverError(res, err); });
861
863
  tar.on('close', code => { if (code !== 0 && !res.writableEnded) res.end(); });
864
+
862
865
  } catch (err) { serverError(res, err); }
863
866
  });
864
867
 
@@ -921,6 +924,7 @@ router.get('/api/extension/status', requireAuth, requireScope('config:read'), as
921
924
  const connected = fs.existsSync(markerPath);
922
925
 
923
926
  res.json({ available, connected, version });
927
+
924
928
  } catch (err) { serverError(res, err); }
925
929
  });
926
930
 
@@ -945,6 +949,7 @@ router.get('/api/graph', requireAuth, requireScope('ssss:read'), (req, res) => {
945
949
  ? fs.readFileSync(routesFile, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l))
946
950
  : [];
947
951
  res.json({ nodes, routes });
952
+
948
953
  } catch (err) { serverError(res, err); }
949
954
  });
950
955
 
@@ -1040,6 +1045,7 @@ router.get('/api/files', requireAuth, requireScope('files:read'), (req, res) =>
1040
1045
  };
1041
1046
  });
1042
1047
  res.json(files);
1048
+
1043
1049
  } catch (err) { serverError(res, err); }
1044
1050
  });
1045
1051
 
@@ -1170,6 +1176,7 @@ router.get('/api/logs/:type', requireAuth, requireScope('health:read'), (req, re
1170
1176
  const lines = content.split('\n');
1171
1177
  const lastLines = lines.slice(-200).join('\n');
1172
1178
  res.json({ content: lastLines });
1179
+
1173
1180
  } catch (err) { serverError(res, err); }
1174
1181
  });
1175
1182
 
@@ -1199,6 +1206,7 @@ router.get('/api/tasks', requireAuth, requireScope('tasks:read'), (req, res) =>
1199
1206
  }
1200
1207
  }
1201
1208
  res.json(tasks.sort((a, b) => (a.priority || 5) - (b.priority || 5)));
1209
+
1202
1210
  } catch (err) { serverError(res, err); }
1203
1211
  });
1204
1212
 
@@ -1224,6 +1232,7 @@ router.delete('/api/tasks/cleanup', requireAuth, requireScope('tasks:write'), (r
1224
1232
  }
1225
1233
  }
1226
1234
  res.json({ deleted });
1235
+
1227
1236
  } catch (err) { serverError(res, err); }
1228
1237
  });
1229
1238
 
@@ -1252,6 +1261,7 @@ router.post('/api/tasks', requireAuth, requireScope('tasks:write'), (req, res) =
1252
1261
  const raw = safeStringify(body || '', frontmatter);
1253
1262
  fs.writeFileSync(path.join(TASKS_DIR, `${slug}.md`), raw, 'utf8');
1254
1263
  res.json({ slug, ...frontmatter });
1264
+
1255
1265
  } catch (err) { serverError(res, err); }
1256
1266
  });
1257
1267
 
@@ -1313,6 +1323,7 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
1313
1323
  }
1314
1324
 
1315
1325
  res.json({ security, budget, brain: safeBrain, secrets: safeSecrets });
1326
+
1316
1327
  } catch (err) { serverError(res, err); }
1317
1328
  });
1318
1329
 
@@ -1369,12 +1380,18 @@ router.post('/api/config-json', requireAuth, requireScope('config:write'), (req,
1369
1380
  }
1370
1381
 
1371
1382
  res.json({ success: true });
1383
+
1372
1384
  } catch (err) { serverError(res, err); }
1373
1385
  });
1374
1386
 
1375
1387
  router.get('/api/usage', requireAuth, async (req, res) => {
1376
1388
  try {
1377
- const { calculateCurrentCost } = await import('../core/usage-tracker.mjs');
1389
+ const { syncUsageLedger, calculateCurrentCost } = await import('../core/usage-tracker.mjs');
1390
+
1391
+ // Sync the ledger first to capture new logs and lock in current prices
1392
+ const pricingMap = await getPricingMap();
1393
+ syncUsageLedger(pricingMap);
1394
+
1378
1395
  res.json(calculateCurrentCost());
1379
1396
  } catch (err) { serverError(res, err); }
1380
1397
  });
@@ -1391,6 +1408,7 @@ router.get('/api/config/:name', requireAuth, requireScope('config:read'), (req,
1391
1408
  }
1392
1409
  const content = fs.readFileSync(filePath, 'utf8');
1393
1410
  res.json({ content });
1411
+
1394
1412
  } catch (err) { serverError(res, err); }
1395
1413
  });
1396
1414
 
@@ -1403,6 +1421,7 @@ router.put('/api/config/:name', requireAuth, requireScope('config:write'), (req,
1403
1421
  }
1404
1422
  fs.writeFileSync(filePath, req.body.content, 'utf8');
1405
1423
  res.json({ success: true });
1424
+
1406
1425
  } catch (err) { serverError(res, err); }
1407
1426
  });
1408
1427
 
@@ -1427,6 +1446,41 @@ router.post('/api/capture/:source', requireAuth, requireScope('memory:write'), a
1427
1446
  const result = captureMessage({ text, author, channel, source });
1428
1447
  res.json({ ok: true, slug: result.slug });
1429
1448
  } catch (err) {
1449
+ console.error("API ERROR:", err);
1450
+ res.status(500).json({ error: err.message });
1451
+ }
1452
+ });
1453
+
1454
+ /**
1455
+ * GET /api/openrouter-models
1456
+ * Dynamically fetches the list of available OpenRouter models.
1457
+ */
1458
+ router.get('/api/openrouter-models', requireAuth, async (req, res) => {
1459
+ try {
1460
+ const response = await fetch('https://openrouter.ai/api/v1/models');
1461
+ if (response.ok) {
1462
+ const data = await response.json();
1463
+ const models = data.data.map(m => {
1464
+ return {
1465
+ id: m.id,
1466
+ displayName: m.name,
1467
+ pricing: m.pricing,
1468
+ created: m.created || 0
1469
+ };
1470
+ });
1471
+ // Sort first by provider (alphabetical), then by created (descending)
1472
+ models.sort((a, b) => {
1473
+ const provA = a.id.split('/')[0];
1474
+ const provB = b.id.split('/')[0];
1475
+ if (provA < provB) return -1;
1476
+ if (provA > provB) return 1;
1477
+ return b.created - a.created;
1478
+ });
1479
+ return res.json({ models });
1480
+ }
1481
+ throw new Error(`OpenRouter API responded with ${response.status}`);
1482
+ } catch (err) {
1483
+ console.error("API ERROR:", err);
1430
1484
  res.status(500).json({ error: err.message });
1431
1485
  }
1432
1486
  });
@@ -1687,6 +1741,7 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) =>
1687
1741
  }
1688
1742
 
1689
1743
  res.json({ brains });
1744
+
1690
1745
  } catch (err) { serverError(res, err); }
1691
1746
  });
1692
1747
 
@@ -1740,6 +1795,35 @@ router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memo
1740
1795
  } catch (err) { serverError(res, err); }
1741
1796
  });
1742
1797
 
1798
+ let cachedPricingMap = null;
1799
+ let lastPricingFetch = 0;
1800
+
1801
+ async function getPricingMap() {
1802
+ if (cachedPricingMap && Date.now() - lastPricingFetch < 1000 * 60 * 60) {
1803
+ return cachedPricingMap;
1804
+ }
1805
+ try {
1806
+ const controller = new AbortController();
1807
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
1808
+ const response = await fetch('https://openrouter.ai/api/v1/models', { signal: controller.signal });
1809
+ clearTimeout(timeoutId);
1810
+ if (response.ok) {
1811
+ const data = await response.json();
1812
+ const map = {};
1813
+ for (const m of (data.data || [])) {
1814
+ const parts = m.id.split('/');
1815
+ const baseId = parts[parts.length - 1];
1816
+ map[baseId] = m.pricing;
1817
+ map[m.id] = m.pricing;
1818
+ }
1819
+ cachedPricingMap = map;
1820
+ lastPricingFetch = Date.now();
1821
+ return map;
1822
+ }
1823
+ } catch (e) {}
1824
+ return {};
1825
+ }
1826
+
1743
1827
  /**
1744
1828
  * GET /api/gemini-models
1745
1829
  * Dynamically fetches the list of available Gemini models using the user's GOOGLE_API_KEY.
@@ -1773,10 +1857,11 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
1773
1857
 
1774
1858
  const uniqueModels = [...new Set(discovered)];
1775
1859
  if (uniqueModels.length > 0) {
1860
+ const pricingMap = await getPricingMap();
1776
1861
  const cliModels = uniqueModels.map(modelId => {
1777
1862
  const parts = modelId.split('-');
1778
1863
  const displayName = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ');
1779
- return { id: modelId, displayName };
1864
+ return { id: modelId, displayName, pricing: pricingMap[modelId] || pricingMap[`google/${modelId}`] };
1780
1865
  });
1781
1866
  return res.json({ models: cliModels, source: 'cli' });
1782
1867
  }
@@ -1786,38 +1871,20 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
1786
1871
  // Fail silently and fall back
1787
1872
  }
1788
1873
 
1789
- let apiKey = process.env.GOOGLE_API_KEY;
1874
+ let apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
1790
1875
  if (!apiKey) {
1791
- // Find GOOGLE_API_KEY in upwards .env files
1792
1876
  try {
1793
- let dir = process.cwd();
1794
- while (dir !== path.dirname(dir)) {
1795
- const envPath = path.join(dir, '.env');
1796
- if (fs.existsSync(envPath)) {
1797
- const content = fs.readFileSync(envPath, 'utf8');
1798
- for (const line of content.split('\n')) {
1799
- const match = line.match(/^\s*GOOGLE_API_KEY\s*=\s*(["']?)(.*?)\1\s*$/);
1800
- if (match) {
1801
- apiKey = match[2];
1802
- break;
1803
- }
1804
- }
1805
- }
1806
- if (apiKey) break;
1807
- dir = path.dirname(dir);
1877
+ const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
1878
+ if (fs.existsSync(secretsPath)) {
1879
+ const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
1880
+ apiKey = secrets.gemini_api_key || secrets.google_api_key;
1808
1881
  }
1809
1882
  } catch {}
1810
1883
  }
1811
1884
 
1812
- const fallbackModels = [
1813
- { id: 'gemini-3.5-flash', displayName: 'Gemini 3.5 Flash' },
1814
- { id: 'gemini-3.1-pro-preview', displayName: 'Gemini 3.1 Pro Preview' },
1815
- { id: 'gemini-3.1-flash-lite', displayName: 'Gemini 3.1 Flash Lite' }
1816
- ];
1817
1885
 
1818
- if (!apiKey) {
1819
- return res.json({ models: fallbackModels, source: 'fallback' });
1820
- }
1886
+
1887
+ if (!apiKey) return res.json({ models: [], source: 'missing_key' });
1821
1888
 
1822
1889
  try {
1823
1890
  const controller = new AbortController();
@@ -1829,6 +1896,7 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
1829
1896
 
1830
1897
  if (response.ok) {
1831
1898
  const data = await response.json();
1899
+ const pricingMap = await getPricingMap();
1832
1900
  const models = (data.models || [])
1833
1901
  .filter(m => m.name.startsWith('models/gemini-') || m.name.startsWith('models/gemini'))
1834
1902
  .map(m => {
@@ -1836,30 +1904,126 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
1836
1904
  // Create nice display name e.g., "gemini-3.5-flash" -> "Gemini 3.5 Flash"
1837
1905
  const parts = id.split('-');
1838
1906
  const displayName = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ');
1839
- return { id, displayName };
1907
+ const pricing = pricingMap[`google/${id}`] || pricingMap[id] || null;
1908
+ return { id, displayName, pricing };
1840
1909
  })
1841
- .filter(m => {
1842
- // Only keep Gemini 3.1 and newer models
1843
- const match = m.id.match(/^gemini-(\d+)\.(\d+)/);
1844
- if (match) {
1845
- const major = parseInt(match[1], 10);
1846
- const minor = parseInt(match[2], 10);
1847
- return major > 3 || (major === 3 && minor >= 1);
1848
- }
1849
- const majorMatch = m.id.match(/^gemini-(\d+)/);
1850
- if (majorMatch) {
1851
- const major = parseInt(majorMatch[1], 10);
1852
- return major >= 3;
1853
- }
1854
- return false;
1910
+ ;
1911
+ if (models.length > 0) {
1912
+ return res.json({ models, source: 'dynamic' });
1913
+ }
1914
+ }
1915
+ } catch {}
1916
+
1917
+ res.json({ models: [], source: 'api_error' });
1918
+ } catch (err) {
1919
+ serverError(res, err);
1920
+ }
1921
+ });
1922
+
1923
+ /**
1924
+ * GET /api/claude-models
1925
+ * Dynamically fetches the list of available Anthropic models using the user's ANTHROPIC_API_KEY.
1926
+ */
1927
+ router.get('/api/claude-models', requireAuth, async (req, res) => {
1928
+ try {
1929
+ let apiKey = process.env.ANTHROPIC_API_KEY;
1930
+ if (!apiKey) {
1931
+ try {
1932
+ const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
1933
+ if (fs.existsSync(secretsPath)) {
1934
+ const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
1935
+ apiKey = secrets.anthropic_api_key;
1936
+ }
1937
+ } catch {}
1938
+ }
1939
+
1940
+
1941
+
1942
+ if (!apiKey) return res.json({ models: [], source: 'missing_key' });
1943
+
1944
+ try {
1945
+ const controller = new AbortController();
1946
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
1947
+ const response = await fetch('https://api.anthropic.com/v1/models', {
1948
+ headers: {
1949
+ 'x-api-key': apiKey,
1950
+ 'anthropic-version': '2023-06-01'
1951
+ },
1952
+ signal: controller.signal
1953
+ });
1954
+ clearTimeout(timeoutId);
1955
+
1956
+ if (response.ok) {
1957
+ const data = await response.json();
1958
+ const pricingMap = await getPricingMap();
1959
+ const models = (data.data || []).map(m => {
1960
+ const parts = m.id.split('-');
1961
+ const displayName = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ');
1962
+ const pricing = pricingMap[`openai/${m.id}`] || pricingMap[m.id] || null;
1963
+ return { id: m.id, displayName, pricing };
1964
+ });
1965
+ if (models.length > 0) {
1966
+ return res.json({ models, source: 'dynamic' });
1967
+ }
1968
+ }
1969
+ } catch {}
1970
+
1971
+ res.json({ models: [], source: 'api_error' });
1972
+ } catch (err) {
1973
+ serverError(res, err);
1974
+ }
1975
+ });
1976
+
1977
+ /**
1978
+ * GET /api/openai-models
1979
+ * Dynamically fetches the list of available OpenAI models using the user's OPENAI_API_KEY.
1980
+ */
1981
+ router.get('/api/openai-models', requireAuth, async (req, res) => {
1982
+ try {
1983
+ let apiKey = process.env.OPENAI_API_KEY;
1984
+ if (!apiKey) {
1985
+ try {
1986
+ const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
1987
+ if (fs.existsSync(secretsPath)) {
1988
+ const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
1989
+ apiKey = secrets.openai_api_key;
1990
+ }
1991
+ } catch {}
1992
+ }
1993
+
1994
+
1995
+
1996
+ if (!apiKey) return res.json({ models: [], source: 'missing_key' });
1997
+
1998
+ try {
1999
+ const controller = new AbortController();
2000
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
2001
+ const response = await fetch('https://api.openai.com/v1/models', {
2002
+ headers: {
2003
+ 'Authorization': `Bearer ${apiKey}`
2004
+ },
2005
+ signal: controller.signal
2006
+ });
2007
+ clearTimeout(timeoutId);
2008
+
2009
+ if (response.ok) {
2010
+ const data = await response.json();
2011
+ const pricingMap = await getPricingMap();
2012
+ const models = (data.data || [])
2013
+ .filter(m => m.id.startsWith('gpt-') || m.id.startsWith('o1') || m.id.startsWith('o3') || m.id.startsWith('o4') || m.id.startsWith('chatgpt-'))
2014
+ .map(m => {
2015
+ const pricing = pricingMap[`openai/${m.id}`] || pricingMap[m.id] || null;
2016
+ return { id: m.id, displayName: m.id, pricing };
1855
2017
  });
2018
+
1856
2019
  if (models.length > 0) {
2020
+ models.sort((a, b) => a.id.localeCompare(b.id));
1857
2021
  return res.json({ models, source: 'dynamic' });
1858
2022
  }
1859
2023
  }
1860
2024
  } catch {}
1861
2025
 
1862
- res.json({ models: fallbackModels, source: 'fallback_error' });
2026
+ res.json({ models: [], source: 'api_error' });
1863
2027
  } catch (err) {
1864
2028
  serverError(res, err);
1865
2029
  }
@@ -23,6 +23,10 @@
23
23
  "method": "POST",
24
24
  "path": "/api/capture/:source"
25
25
  },
26
+ {
27
+ "method": "GET",
28
+ "path": "/api/claude-models"
29
+ },
26
30
  {
27
31
  "method": "GET",
28
32
  "path": "/api/config"
@@ -183,6 +187,14 @@
183
187
  "method": "GET",
184
188
  "path": "/api/memory/stats"
185
189
  },
190
+ {
191
+ "method": "GET",
192
+ "path": "/api/openai-models"
193
+ },
194
+ {
195
+ "method": "GET",
196
+ "path": "/api/openrouter-models"
197
+ },
186
198
  {
187
199
  "method": "GET",
188
200
  "path": "/api/research"
@@ -40,7 +40,7 @@ export const MODEL_CATALOG_DIR = path.join(ROOT, 'models', 'catalog', 'total-rec
40
40
  * @returns {string} Absolute path to the memory-vault directory
41
41
  */
42
42
  export function resolveVaultFromQuery(req) {
43
- const brainId = req.query?.brain || req.body?.brainId;
43
+ const brainId = req.query?.brain || req.body?.brainId || req.headers?.['x-total-recall-brain'];
44
44
 
45
45
  // No brain specified or explicitly global → default vault
46
46
  if (!brainId || brainId === 'global') {