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.
- package/frontend/package.json +2 -1
- package/frontend/src/App.tsx +44 -0
- package/frontend/src/api.ts +101 -1
- package/frontend/src/components/UsageChart.tsx +78 -0
- package/frontend/src/pages/DesignDocsPage.tsx +614 -0
- package/frontend/src/pages/InstructionsPage.tsx +583 -0
- package/frontend/src/pages/ModelsPage.tsx +280 -23
- package/frontend/src/pages/OkfPage.tsx +535 -0
- package/frontend/src/pages/OpenWikiPage.tsx +472 -0
- package/frontend/src/pages/SettingsPage.tsx +183 -0
- package/frontend/src/pages/SkillsPage.tsx +1054 -37
- package/frontend/src/pages/UsagePage.tsx +254 -1
- package/frontend/src/types.ts +160 -0
- package/package.json +1 -1
- package/src/server/api.mjs +2 -0
- package/src/server/auth.mjs +2 -2
- package/src/server/rest.mjs +207 -43
- package/src/server/route-manifest.json +12 -0
- package/src/server/routes/_shared.mjs +1 -1
package/src/server/rest.mjs
CHANGED
|
@@ -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
|
-
|
|
1794
|
-
|
|
1795
|
-
const
|
|
1796
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1907
|
+
const pricing = pricingMap[`google/${id}`] || pricingMap[id] || null;
|
|
1908
|
+
return { id, displayName, pricing };
|
|
1840
1909
|
})
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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:
|
|
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') {
|