limbo-ai 1.20.4 → 1.22.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/README.md +7 -7
- package/cli.js +231 -22
- package/mcp-server/index.js +6 -1
- package/mcp-server/test/benchmark.js +365 -0
- package/mcp-server/tools/read.js +12 -67
- package/mcp-server/tools/search.js +5 -108
- package/mcp-server/tools/update-map.js +47 -11
- package/mcp-server/tools/write.js +7 -1
- package/mcp-server/vault-index.js +127 -0
- package/package.json +2 -2
- package/setup-server/public/index.html +234 -4
- package/setup-server/server.js +54 -11
- package/test/cli-auth.test.js +280 -174
- package/test/setup-server.test.js +287 -0
package/README.md
CHANGED
|
@@ -52,12 +52,12 @@ Headless mode skips Telegram setup. To add Telegram later, run `npx limbo-ai sta
|
|
|
52
52
|
### Available commands
|
|
53
53
|
|
|
54
54
|
```sh
|
|
55
|
-
npx limbo-ai start # Install and start (default if no command given)
|
|
56
|
-
npx limbo-ai stop # Stop the container
|
|
57
|
-
npx limbo-ai update # Pull latest image and restart
|
|
58
|
-
npx limbo-ai status # Show container status
|
|
59
|
-
npx limbo-ai logs # Tail container logs
|
|
60
|
-
npx limbo-ai start --reconfigure # Change API keys or settings
|
|
55
|
+
npx limbo-ai@latest start # Install and start (default if no command given)
|
|
56
|
+
npx limbo-ai@latest stop # Stop the container
|
|
57
|
+
npx limbo-ai@latest update # Pull latest image and restart
|
|
58
|
+
npx limbo-ai@latest status # Show container status
|
|
59
|
+
npx limbo-ai@latest logs # Tail container logs
|
|
60
|
+
npx limbo-ai@latest start --reconfigure # Change API keys or settings
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
---
|
|
@@ -65,7 +65,7 @@ npx limbo-ai start --reconfigure # Change API keys or settings
|
|
|
65
65
|
## Updating
|
|
66
66
|
|
|
67
67
|
```sh
|
|
68
|
-
npx limbo-ai update
|
|
68
|
+
npx limbo-ai@latest update
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Pulls the latest Limbo image and restarts the container. Your vault data is persisted in the `limbo-data` Docker volume and is not affected.
|
package/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const { execSync, spawn, spawnSync } = require('child_process');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const fs = require('fs');
|
|
9
|
+
const https = require('https');
|
|
9
10
|
const os = require('os');
|
|
10
11
|
const path = require('path');
|
|
11
12
|
const readline = require('readline');
|
|
@@ -162,6 +163,8 @@ function composeContent() {
|
|
|
162
163
|
- llm_api_key
|
|
163
164
|
- telegram_bot_token
|
|
164
165
|
- gateway_token
|
|
166
|
+
- groq_api_key
|
|
167
|
+
- brave_api_key
|
|
165
168
|
env_file:
|
|
166
169
|
- ${LIMBO_DIR}/.env
|
|
167
170
|
environment:
|
|
@@ -183,6 +186,10 @@ secrets:
|
|
|
183
186
|
file: ${SECRETS_DIR}/telegram_bot_token
|
|
184
187
|
gateway_token:
|
|
185
188
|
file: ${SECRETS_DIR}/gateway_token
|
|
189
|
+
groq_api_key:
|
|
190
|
+
file: ${SECRETS_DIR}/groq_api_key
|
|
191
|
+
brave_api_key:
|
|
192
|
+
file: ${SECRETS_DIR}/brave_api_key
|
|
186
193
|
|
|
187
194
|
volumes:
|
|
188
195
|
limbo-data:
|
|
@@ -218,6 +225,8 @@ function composeContentHardened() {
|
|
|
218
225
|
- llm_api_key
|
|
219
226
|
- telegram_bot_token
|
|
220
227
|
- gateway_token
|
|
228
|
+
- groq_api_key
|
|
229
|
+
- brave_api_key
|
|
221
230
|
env_file:
|
|
222
231
|
- ${LIMBO_DIR}/.env
|
|
223
232
|
environment:
|
|
@@ -270,6 +279,10 @@ secrets:
|
|
|
270
279
|
file: ${SECRETS_DIR}/telegram_bot_token
|
|
271
280
|
gateway_token:
|
|
272
281
|
file: ${SECRETS_DIR}/gateway_token
|
|
282
|
+
groq_api_key:
|
|
283
|
+
file: ${SECRETS_DIR}/groq_api_key
|
|
284
|
+
brave_api_key:
|
|
285
|
+
file: ${SECRETS_DIR}/brave_api_key
|
|
273
286
|
|
|
274
287
|
volumes:
|
|
275
288
|
limbo-data:
|
|
@@ -699,6 +712,8 @@ function normalizeConfig(cfg, existingEnv = {}) {
|
|
|
699
712
|
TELEGRAM_BOT_TOKEN: cfg.telegramToken || (cfg.keepExisting ? existingEnv.TELEGRAM_BOT_TOKEN || '' : ''),
|
|
700
713
|
TELEGRAM_AUTO_PAIR_FIRST_DM: cfg.telegramAutoPair || existingEnv.TELEGRAM_AUTO_PAIR_FIRST_DM || 'false',
|
|
701
714
|
GATEWAY_TOKEN: gatewayToken,
|
|
715
|
+
VOICE_ENABLED: cfg.voiceEnabled || existingEnv.VOICE_ENABLED || 'false',
|
|
716
|
+
WEB_SEARCH_ENABLED: cfg.webSearchEnabled || existingEnv.WEB_SEARCH_ENABLED || 'false',
|
|
702
717
|
};
|
|
703
718
|
|
|
704
719
|
return base;
|
|
@@ -718,6 +733,8 @@ function writeSecrets(cfg, existingEnv = {}) {
|
|
|
718
733
|
writeSecretFile('llm_api_key', normalized.LLM_API_KEY);
|
|
719
734
|
writeSecretFile('telegram_bot_token', normalized.TELEGRAM_BOT_TOKEN);
|
|
720
735
|
writeSecretFile('gateway_token', normalized.GATEWAY_TOKEN);
|
|
736
|
+
writeSecretFile('groq_api_key', cfg.groqApiKey || readSecretFile('groq_api_key'));
|
|
737
|
+
writeSecretFile('brave_api_key', cfg.braveApiKey || readSecretFile('brave_api_key'));
|
|
721
738
|
}
|
|
722
739
|
|
|
723
740
|
const SECRET_KEYS = new Set([
|
|
@@ -922,7 +939,7 @@ function ensureComposeFile(hardened = false) {
|
|
|
922
939
|
fs.mkdirSync(path.join(VAULT_DIR, 'maps'), { recursive: true });
|
|
923
940
|
fs.mkdirSync(SECRETS_DIR, { recursive: true, mode: 0o700 });
|
|
924
941
|
// Ensure secret files exist (Docker Compose secrets require the files to be present)
|
|
925
|
-
for (const name of ['llm_api_key', 'telegram_bot_token', 'gateway_token']) {
|
|
942
|
+
for (const name of ['llm_api_key', 'telegram_bot_token', 'gateway_token', 'groq_api_key', 'brave_api_key']) {
|
|
926
943
|
const fp = path.join(SECRETS_DIR, name);
|
|
927
944
|
if (!fs.existsSync(fp)) fs.writeFileSync(fp, '', { mode: 0o644 });
|
|
928
945
|
}
|
|
@@ -1683,6 +1700,98 @@ function cmdStatus() {
|
|
|
1683
1700
|
run('docker compose ps');
|
|
1684
1701
|
}
|
|
1685
1702
|
|
|
1703
|
+
function cmdConfig() {
|
|
1704
|
+
const args = process.argv.slice(3);
|
|
1705
|
+
const feature = args[0];
|
|
1706
|
+
|
|
1707
|
+
if (!feature || !['voice', 'web-search'].includes(feature)) {
|
|
1708
|
+
console.log(`
|
|
1709
|
+
${c.bold}Usage:${c.reset}
|
|
1710
|
+
limbo config voice --enable --api-key <key>
|
|
1711
|
+
limbo config voice --disable
|
|
1712
|
+
limbo config voice --status
|
|
1713
|
+
limbo config web-search --enable --api-key <key>
|
|
1714
|
+
limbo config web-search --disable
|
|
1715
|
+
limbo config web-search --status
|
|
1716
|
+
`);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (!fs.existsSync(ENV_FILE)) {
|
|
1721
|
+
die('Limbo is not configured. Run "limbo start" first.');
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const existingEnv = {};
|
|
1725
|
+
const envContent = fs.readFileSync(ENV_FILE, 'utf8');
|
|
1726
|
+
for (const line of envContent.split('\n')) {
|
|
1727
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
1728
|
+
if (match) existingEnv[match[1]] = match[2].replace(/^"|"$/g, '');
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const isVoice = feature === 'voice';
|
|
1732
|
+
const envKey = isVoice ? 'VOICE_ENABLED' : 'WEB_SEARCH_ENABLED';
|
|
1733
|
+
const secretName = isVoice ? 'groq_api_key' : 'brave_api_key';
|
|
1734
|
+
const featureLabel = isVoice ? 'Voice transcription' : 'Web search';
|
|
1735
|
+
|
|
1736
|
+
const hasEnable = args.includes('--enable');
|
|
1737
|
+
const hasDisable = args.includes('--disable');
|
|
1738
|
+
const hasStatus = args.includes('--status');
|
|
1739
|
+
const apiKeyIdx = args.indexOf('--api-key');
|
|
1740
|
+
const apiKey = apiKeyIdx !== -1 ? args[apiKeyIdx + 1] : null;
|
|
1741
|
+
|
|
1742
|
+
if (hasStatus) {
|
|
1743
|
+
const enabled = existingEnv[envKey] === 'true';
|
|
1744
|
+
const key = readSecretFile(secretName);
|
|
1745
|
+
console.log(`${featureLabel}: ${enabled ? `${c.green}enabled${c.reset}` : `${c.dim}disabled${c.reset}`}`);
|
|
1746
|
+
if (key) {
|
|
1747
|
+
const masked = key.length > 8 ? key.substring(0, 4) + '...' + key.slice(-4) : '***';
|
|
1748
|
+
console.log(`API Key: ${masked}`);
|
|
1749
|
+
}
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (!hasEnable && !hasDisable) {
|
|
1754
|
+
die('Specify --enable, --disable, or --status');
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (hasEnable) {
|
|
1758
|
+
if (apiKey) {
|
|
1759
|
+
if (isVoice && !apiKey.startsWith('gsk_')) {
|
|
1760
|
+
warn('Groq API keys typically start with gsk_');
|
|
1761
|
+
}
|
|
1762
|
+
if (!isVoice && !apiKey.startsWith('BSA')) {
|
|
1763
|
+
warn('Brave API keys typically start with BSA');
|
|
1764
|
+
}
|
|
1765
|
+
writeSecretFile(secretName, apiKey);
|
|
1766
|
+
ok(`${featureLabel} API key saved.`);
|
|
1767
|
+
} else {
|
|
1768
|
+
const existing = readSecretFile(secretName);
|
|
1769
|
+
if (!existing) {
|
|
1770
|
+
die(`No API key found. Use --api-key <key> to set one.`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
existingEnv[envKey] = 'true';
|
|
1774
|
+
} else {
|
|
1775
|
+
existingEnv[envKey] = 'false';
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
const newContent = Object.entries(existingEnv)
|
|
1779
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
1780
|
+
.join('\n') + '\n';
|
|
1781
|
+
fs.writeFileSync(ENV_FILE, newContent, { mode: 0o600 });
|
|
1782
|
+
ok(`${featureLabel} ${hasEnable ? 'enabled' : 'disabled'}.`);
|
|
1783
|
+
|
|
1784
|
+
if (fs.existsSync(COMPOSE_FILE)) {
|
|
1785
|
+
log('Restarting container...');
|
|
1786
|
+
try {
|
|
1787
|
+
execSync(`docker compose -f "${COMPOSE_FILE}" restart limbo`, { stdio: 'inherit' });
|
|
1788
|
+
ok('Container restarted.');
|
|
1789
|
+
} catch {
|
|
1790
|
+
warn('Could not restart container. Restart manually with: limbo stop && limbo start');
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1686
1795
|
function cmdHelp() {
|
|
1687
1796
|
console.log(`
|
|
1688
1797
|
${c.bold}limbo${c.reset} - personal AI memory agent
|
|
@@ -1696,6 +1805,7 @@ ${c.bold}Commands:${c.reset}
|
|
|
1696
1805
|
logs Tail container logs
|
|
1697
1806
|
update Pull latest image and restart
|
|
1698
1807
|
status Show container status
|
|
1808
|
+
config Configure optional features (voice, web-search)
|
|
1699
1809
|
help Show this help
|
|
1700
1810
|
|
|
1701
1811
|
${c.bold}Flags:${c.reset}
|
|
@@ -1708,30 +1818,129 @@ ${c.bold}Flags:${c.reset}
|
|
|
1708
1818
|
--language <code> Language: en, es (default: en)
|
|
1709
1819
|
--tunnel-domain <d> Admin: use branded subdomain for setup tunnel (e.g. limbo.tomasward.com)
|
|
1710
1820
|
|
|
1821
|
+
${c.bold}Config:${c.reset}
|
|
1822
|
+
limbo config voice --enable --api-key gsk_xxx Enable voice transcription
|
|
1823
|
+
limbo config voice --disable Disable voice transcription
|
|
1824
|
+
limbo config web-search --enable --api-key BSAxxx Enable web search
|
|
1825
|
+
limbo config web-search --disable Disable web search
|
|
1826
|
+
limbo config voice --status Show feature status
|
|
1827
|
+
|
|
1711
1828
|
${c.bold}Data directory:${c.reset} ${LIMBO_DIR}
|
|
1712
1829
|
`);
|
|
1713
1830
|
}
|
|
1714
1831
|
|
|
1832
|
+
// ─── Update Notifier ─────────────────────────────────────────────────────────
|
|
1833
|
+
|
|
1834
|
+
const UPDATE_CHECK_FILE = path.join(LIMBO_DIR, '.update-check');
|
|
1835
|
+
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
|
1836
|
+
|
|
1837
|
+
// Spawn a detached background process to check the npm registry.
|
|
1838
|
+
// Writes {latest, checkedAt} to UPDATE_CHECK_FILE and exits.
|
|
1839
|
+
function checkForUpdateInBackground() {
|
|
1840
|
+
try {
|
|
1841
|
+
let shouldCheck = true;
|
|
1842
|
+
if (fs.existsSync(UPDATE_CHECK_FILE)) {
|
|
1843
|
+
const cached = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf8'));
|
|
1844
|
+
if (Date.now() - cached.checkedAt < UPDATE_CHECK_INTERVAL) shouldCheck = false;
|
|
1845
|
+
}
|
|
1846
|
+
if (!shouldCheck) return;
|
|
1847
|
+
|
|
1848
|
+
// Spawn detached child that hits the registry and writes cache
|
|
1849
|
+
const child = spawn(process.execPath, ['-e', `
|
|
1850
|
+
const https = require('https');
|
|
1851
|
+
const fs = require('fs');
|
|
1852
|
+
const req = https.get('https://registry.npmjs.org/limbo-ai/latest', { timeout: 5000 }, (res) => {
|
|
1853
|
+
let data = '';
|
|
1854
|
+
res.on('data', (chunk) => data += chunk);
|
|
1855
|
+
res.on('end', () => {
|
|
1856
|
+
try {
|
|
1857
|
+
const { version } = JSON.parse(data);
|
|
1858
|
+
fs.mkdirSync('${LIMBO_DIR.replace(/\\/g, '\\\\')}', { recursive: true });
|
|
1859
|
+
fs.writeFileSync('${UPDATE_CHECK_FILE.replace(/\\/g, '\\\\')}', JSON.stringify({ latest: version, checkedAt: Date.now() }));
|
|
1860
|
+
} catch {}
|
|
1861
|
+
});
|
|
1862
|
+
});
|
|
1863
|
+
req.on('error', () => {});
|
|
1864
|
+
req.end();
|
|
1865
|
+
`], { detached: true, stdio: 'ignore' });
|
|
1866
|
+
child.unref();
|
|
1867
|
+
} catch {}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Read cache and print banner if a newer version is available.
|
|
1871
|
+
function notifyUpdate() {
|
|
1872
|
+
try {
|
|
1873
|
+
if (!fs.existsSync(UPDATE_CHECK_FILE)) return;
|
|
1874
|
+
const { latest } = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf8'));
|
|
1875
|
+
const pkg = require('./package.json');
|
|
1876
|
+
if (!latest || latest === pkg.version) return;
|
|
1877
|
+
|
|
1878
|
+
// Simple semver compare: split on dots, compare numerically
|
|
1879
|
+
const cur = pkg.version.split('.').map(Number);
|
|
1880
|
+
const lat = latest.split('.').map(Number);
|
|
1881
|
+
const isNewer = lat[0] > cur[0] || (lat[0] === cur[0] && lat[1] > cur[1]) ||
|
|
1882
|
+
(lat[0] === cur[0] && lat[1] === cur[1] && lat[2] > cur[2]);
|
|
1883
|
+
if (!isNewer) return;
|
|
1884
|
+
|
|
1885
|
+
// Strip ANSI escapes for visible-length padding
|
|
1886
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1887
|
+
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - strip(s).length));
|
|
1888
|
+
|
|
1889
|
+
const line = ` Update available: ${c.dim}${pkg.version}${c.reset} → ${c.green}${latest}${c.reset} `;
|
|
1890
|
+
const instruction = ` Run ${c.cyan}npx limbo-ai@latest update${c.reset} to update `;
|
|
1891
|
+
const inner = Math.max(strip(line).length, strip(instruction).length);
|
|
1892
|
+
const border = '─'.repeat(inner);
|
|
1893
|
+
console.error(`\n ${c.dim}╭${border}╮${c.reset}`);
|
|
1894
|
+
console.error(` ${c.dim}│${c.reset}${pad(line, inner)}${c.dim}│${c.reset}`);
|
|
1895
|
+
console.error(` ${c.dim}│${c.reset}${pad(instruction, inner)}${c.dim}│${c.reset}`);
|
|
1896
|
+
console.error(` ${c.dim}╰${border}╯${c.reset}\n`);
|
|
1897
|
+
} catch {}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// ─── Exports (for testing) ────────────────────────────────────────────────────
|
|
1901
|
+
|
|
1902
|
+
module.exports = {
|
|
1903
|
+
MODEL_CATALOG,
|
|
1904
|
+
normalizeConfig,
|
|
1905
|
+
parseEnvFile,
|
|
1906
|
+
deriveProviderFamily,
|
|
1907
|
+
getModelCatalog,
|
|
1908
|
+
parseCallbackInput,
|
|
1909
|
+
decodeJwtPayload,
|
|
1910
|
+
parseClaudeSetupToken,
|
|
1911
|
+
buildCodexAuthProfile,
|
|
1912
|
+
buildAnthropicAuthProfile,
|
|
1913
|
+
generatePKCE,
|
|
1914
|
+
buildOAuthUrl,
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1715
1917
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
1716
1918
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1919
|
+
if (require.main === module) {
|
|
1920
|
+
const [,, cmd = 'start'] = process.argv;
|
|
1921
|
+
|
|
1922
|
+
(async () => {
|
|
1923
|
+
checkForUpdateInBackground();
|
|
1924
|
+
|
|
1925
|
+
switch (cmd) {
|
|
1926
|
+
case 'start':
|
|
1927
|
+
case 'install': await cmdStart(); break;
|
|
1928
|
+
case 'stop': cmdStop(); break;
|
|
1929
|
+
case 'logs': cmdLogs(); break;
|
|
1930
|
+
case 'update': cmdUpdate(); break;
|
|
1931
|
+
case 'status': cmdStatus(); break;
|
|
1932
|
+
case 'config': cmdConfig(); break;
|
|
1933
|
+
case 'help':
|
|
1934
|
+
case '--help':
|
|
1935
|
+
case '-h': cmdHelp(); break;
|
|
1936
|
+
default:
|
|
1937
|
+
warn(t('en', 'unknownCommand', cmd));
|
|
1938
|
+
cmdHelp();
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
notifyUpdate();
|
|
1943
|
+
})().catch((err) => {
|
|
1944
|
+
die(err.message || String(err));
|
|
1945
|
+
});
|
|
1946
|
+
}
|
package/mcp-server/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
|
|
8
|
+
import { buildIndex } from "./vault-index.js";
|
|
8
9
|
import { vaultSearch } from "./tools/search.js";
|
|
9
10
|
import { vaultRead } from "./tools/read.js";
|
|
10
11
|
import { vaultWriteNote } from "./tools/write.js";
|
|
@@ -13,7 +14,7 @@ import { vaultUpdateMap } from "./tools/update-map.js";
|
|
|
13
14
|
const server = new Server(
|
|
14
15
|
{
|
|
15
16
|
name: "limbo-vault",
|
|
16
|
-
version: "1.
|
|
17
|
+
version: "1.2.0",
|
|
17
18
|
},
|
|
18
19
|
{
|
|
19
20
|
capabilities: {
|
|
@@ -172,5 +173,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
172
173
|
|
|
173
174
|
// ── Start ───────────────────────────────────────────────────────────────────
|
|
174
175
|
|
|
176
|
+
// Build in-memory index before accepting connections
|
|
177
|
+
const noteCount = await buildIndex();
|
|
178
|
+
process.stderr.write(`[limbo-vault] Index built: ${noteCount} notes indexed\n`);
|
|
179
|
+
|
|
175
180
|
const transport = new StdioServerTransport();
|
|
176
181
|
await server.connect(transport);
|