limbo-ai 1.10.0 → 1.12.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 (2) hide show
  1. package/cli.js +131 -40
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -50,6 +50,12 @@ const MODEL_CATALOG = {
50
50
  menuModels: ['claude-opus-4-6', 'claude-sonnet-4-6'],
51
51
  supportedModels: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-1', 'claude-sonnet-4'],
52
52
  },
53
+ 'openrouter:api-key': {
54
+ provider: 'openrouter',
55
+ defaultModel: 'auto',
56
+ menuModels: [],
57
+ supportedModels: [],
58
+ },
53
59
  };
54
60
 
55
61
  const ASCII_ART = String.raw`
@@ -211,6 +217,7 @@ const TEXT = {
211
217
  providerQuestion: 'AI Provider',
212
218
  providerOpenAI: 'Codex (OpenAI)',
213
219
  providerAnthropic: 'Claude (Anthropic)',
220
+ providerOpenRouter: 'OpenRouter (100+ models)',
214
221
  accessMethodQuestion: 'Access method',
215
222
  accessSubscriptionOpenAI: 'ChatGPT / Codex subscription',
216
223
  accessSubscriptionAnthropic: 'Claude Code subscription',
@@ -225,6 +232,11 @@ const TEXT = {
225
232
  requiredField: 'This field is required.',
226
233
  invalidOpenAIKey: 'OpenAI API keys usually start with "sk-".',
227
234
  invalidAnthropicKey: 'Anthropic API keys usually start with "sk-ant-".',
235
+ openRouterApiKeyPrompt: ' OpenRouter API key (sk-or-...): ',
236
+ openRouterKeyWarn: 'OpenRouter API keys usually start with "sk-or-". Proceeding anyway.',
237
+ openRouterKeyHint: 'Get your key at: https://openrouter.ai/keys',
238
+ openRouterModelPrompt: ' Model name (blank = auto-routing): ',
239
+ openRouterModelHint: 'Examples: anthropic/claude-sonnet-4-6, openai/gpt-4o, google/gemini-2.5-pro',
228
240
  telegramQuestion: 'Want to speak to Limbo through Telegram?',
229
241
  telegramBotFatherSteps: [
230
242
  'To create a Telegram bot:',
@@ -260,6 +272,9 @@ const TEXT = {
260
272
  subscriptionSetup: 'Provider authentication',
261
273
  openaiSubscriptionIntro: 'Limbo will authenticate with your OpenAI account. A URL will open in your browser — log in and authorize access.',
262
274
  anthropicSubscriptionIntro: 'Generate a Claude setup-token on any machine with `claude setup-token`, then paste it into the next step.',
275
+ claudeTokenPrompt: ' Setup token: ',
276
+ claudeTokenInvalid: 'Invalid token. It should start with "sk-ant-".',
277
+ claudeTokenWritten: 'Auth profile written.',
263
278
  authFlowStart: 'Starting authentication...',
264
279
  authFlowDone: 'Authentication complete.',
265
280
  authFlowFailed: 'Authentication did not complete successfully.',
@@ -309,6 +324,7 @@ const TEXT = {
309
324
  providerQuestion: 'AI Provider',
310
325
  providerOpenAI: 'Codex (OpenAI)',
311
326
  providerAnthropic: 'Claude (Anthropic)',
327
+ providerOpenRouter: 'OpenRouter (100+ modelos)',
312
328
  accessMethodQuestion: 'Metodo de acceso',
313
329
  accessSubscriptionOpenAI: 'Suscripcion ChatGPT / Codex',
314
330
  accessSubscriptionAnthropic: 'Suscripcion Claude Code',
@@ -323,6 +339,11 @@ const TEXT = {
323
339
  requiredField: 'Este campo es obligatorio.',
324
340
  invalidOpenAIKey: 'Las API keys de OpenAI normalmente empiezan con "sk-".',
325
341
  invalidAnthropicKey: 'Las API keys de Anthropic normalmente empiezan con "sk-ant-".',
342
+ openRouterApiKeyPrompt: ' OpenRouter API key (sk-or-...): ',
343
+ openRouterKeyWarn: 'Las API keys de OpenRouter normalmente empiezan con "sk-or-". Continuando igual.',
344
+ openRouterKeyHint: 'Consegui tu key en: https://openrouter.ai/keys',
345
+ openRouterModelPrompt: ' Nombre del modelo (vacio = auto-routing): ',
346
+ openRouterModelHint: 'Ejemplos: anthropic/claude-sonnet-4-6, openai/gpt-4o, google/gemini-2.5-pro',
326
347
  telegramQuestion: 'Quieres hablar con Limbo por Telegram?',
327
348
  telegramBotFatherSteps: [
328
349
  'Para crear un bot de Telegram:',
@@ -358,6 +379,9 @@ const TEXT = {
358
379
  subscriptionSetup: 'Autenticacion del provider',
359
380
  openaiSubscriptionIntro: 'Limbo va a autenticarse con tu cuenta de OpenAI. Se va a abrir una URL en tu navegador — inicia sesion y autoriza el acceso.',
360
381
  anthropicSubscriptionIntro: 'Genera un Claude setup-token en cualquier maquina con `claude setup-token` y pegalo en el siguiente paso.',
382
+ claudeTokenPrompt: ' Setup token: ',
383
+ claudeTokenInvalid: 'Token invalido. Deberia empezar con "sk-ant-".',
384
+ claudeTokenWritten: 'Perfil de auth guardado.',
361
385
  authFlowStart: 'Iniciando autenticacion...',
362
386
  authFlowDone: 'Autenticacion completada.',
363
387
  authFlowFailed: 'La autenticacion no termino correctamente.',
@@ -584,7 +608,7 @@ function writeSecrets(cfg, existingEnv = {}) {
584
608
  }
585
609
 
586
610
  const SECRET_KEYS = new Set([
587
- 'LLM_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
611
+ 'LLM_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY',
588
612
  'TELEGRAM_BOT_TOKEN', 'OPENCLAW_GATEWAY_TOKEN',
589
613
  ]);
590
614
 
@@ -609,12 +633,29 @@ function waitForHealthy(lang, maxAttempts = 12) {
609
633
  return false;
610
634
  }
611
635
 
636
+ function deriveProviderFamily(provider) {
637
+ if (!provider) return 'anthropic';
638
+ if (provider.startsWith('openai')) return 'openai';
639
+ if (provider === 'openrouter') return 'openrouter';
640
+ return 'anthropic';
641
+ }
642
+
612
643
  function getModelCatalog(providerFamily, authMode) {
613
644
  return MODEL_CATALOG[`${providerFamily}:${authMode}`];
614
645
  }
615
646
 
616
647
  async function chooseModel(lang, providerFamily, authMode) {
617
648
  const catalog = getModelCatalog(providerFamily, authMode);
649
+
650
+ if (!catalog.menuModels.length) {
651
+ console.log(` ${c.dim}${t(lang, 'openRouterModelHint')}${c.reset}`);
652
+ const modelName = await promptValidated(
653
+ t(lang, 'openRouterModelPrompt'),
654
+ (value) => ({ ok: true, value: value || catalog.defaultModel }),
655
+ );
656
+ return modelName;
657
+ }
658
+
618
659
  const options = catalog.menuModels.map((model) => ({ label: model, value: model }));
619
660
  options.push({ label: t(lang, 'customModel'), value: '__custom__' });
620
661
 
@@ -648,17 +689,23 @@ async function collectConfig(existingEnv = {}) {
648
689
  const providerFamily = (await selectMenu(t(language, 'providerQuestion'), [
649
690
  { label: t(language, 'providerOpenAI'), value: 'openai' },
650
691
  { label: t(language, 'providerAnthropic'), value: 'anthropic' },
692
+ { label: t(language, 'providerOpenRouter'), value: 'openrouter' },
651
693
  ], language)).value;
652
694
 
653
- const accessMethod = (await selectMenu(t(language, 'accessMethodQuestion'), [
654
- {
655
- label: providerFamily === 'openai'
656
- ? t(language, 'accessSubscriptionOpenAI')
657
- : t(language, 'accessSubscriptionAnthropic'),
658
- value: 'subscription',
659
- },
660
- { label: t(language, 'accessApiKey'), value: 'api-key' },
661
- ], language)).value;
695
+ let accessMethod;
696
+ if (providerFamily === 'openrouter') {
697
+ accessMethod = 'api-key';
698
+ } else {
699
+ accessMethod = (await selectMenu(t(language, 'accessMethodQuestion'), [
700
+ {
701
+ label: providerFamily === 'openai'
702
+ ? t(language, 'accessSubscriptionOpenAI')
703
+ : t(language, 'accessSubscriptionAnthropic'),
704
+ value: 'subscription',
705
+ },
706
+ { label: t(language, 'accessApiKey'), value: 'api-key' },
707
+ ], language)).value;
708
+ }
662
709
 
663
710
  const modelName = await chooseModel(language, providerFamily, accessMethod);
664
711
  const provider = getModelCatalog(providerFamily, accessMethod).provider;
@@ -674,6 +721,16 @@ async function collectConfig(existingEnv = {}) {
674
721
  return { ok: true, value };
675
722
  },
676
723
  );
724
+ } else if (providerFamily === 'openrouter') {
725
+ console.log(` ${c.dim}${t(language, 'openRouterKeyHint')}${c.reset}`);
726
+ apiKey = await promptValidated(
727
+ t(language, 'openRouterApiKeyPrompt'),
728
+ (value) => {
729
+ if (!value) return { ok: false, message: t(language, 'requiredField') };
730
+ if (!value.startsWith('sk-or-')) warn(t(language, 'openRouterKeyWarn'));
731
+ return { ok: true, value };
732
+ },
733
+ );
677
734
  } else {
678
735
  apiKey = await promptValidated(
679
736
  t(language, 'anthropicApiKeyPrompt'),
@@ -933,10 +990,24 @@ function decodeJwtPayload(token) {
933
990
  return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
934
991
  }
935
992
 
936
- function writeAuthProfilesToDocker(profile) {
937
- // Write auth-profiles.json into the OpenClaw Docker volume via a temp container
993
+ function writeAuthProfilesToDocker(store) {
994
+ const json = JSON.stringify(store, null, 2);
995
+ const destDir = '/home/limbo/.openclaw/agents/main/agent';
996
+ const destFile = `${destDir}/auth-profiles.json`;
997
+ spawnSync('docker', [
998
+ 'compose', 'run', '--rm', '--no-deps', '--entrypoint', 'sh', 'limbo',
999
+ '-c', `mkdir -p "${destDir}" && cat > "${destFile}"`,
1000
+ ], {
1001
+ cwd: LIMBO_DIR,
1002
+ stdio: ['pipe', 'pipe', 'pipe'],
1003
+ input: json,
1004
+ encoding: 'utf8',
1005
+ });
1006
+ }
1007
+
1008
+ function buildCodexAuthProfile(profile) {
938
1009
  const profileId = profile.email ? `openai-codex:${profile.email}` : 'openai-codex:default';
939
- const store = {
1010
+ return {
940
1011
  version: 1,
941
1012
  profiles: {
942
1013
  [profileId]: {
@@ -952,19 +1023,46 @@ function writeAuthProfilesToDocker(profile) {
952
1023
  lastGood: {},
953
1024
  usageStats: {},
954
1025
  };
955
- const json = JSON.stringify(store, null, 2);
956
- const destDir = '/home/limbo/.openclaw/agents/main/agent';
957
- const destFile = `${destDir}/auth-profiles.json`;
958
- // Use a one-shot container to write into the named volume
959
- spawnSync('docker', [
960
- 'compose', 'run', '--rm', '--no-deps', '--entrypoint', 'sh', 'limbo',
961
- '-c', `mkdir -p "${destDir}" && cat > "${destFile}"`,
962
- ], {
963
- cwd: LIMBO_DIR,
964
- stdio: ['pipe', 'pipe', 'pipe'],
965
- input: json,
966
- encoding: 'utf8',
967
- });
1026
+ }
1027
+
1028
+ function buildAnthropicAuthProfile(token) {
1029
+ return {
1030
+ version: 1,
1031
+ profiles: {
1032
+ 'anthropic:token': {
1033
+ type: 'token',
1034
+ provider: 'anthropic',
1035
+ token,
1036
+ },
1037
+ },
1038
+ order: { anthropic: ['anthropic:token'] },
1039
+ lastGood: {},
1040
+ usageStats: {},
1041
+ };
1042
+ }
1043
+
1044
+ function parseClaudeSetupToken(raw) {
1045
+ const trimmed = raw.trim();
1046
+ if (/^sk-ant-[a-zA-Z0-9_-]+$/.test(trimmed)) return trimmed;
1047
+ return null;
1048
+ }
1049
+
1050
+ async function runClaudeSetupTokenAuth(language) {
1051
+ const tokenRaw = await promptValidated(
1052
+ t(language, 'claudeTokenPrompt'),
1053
+ (value) => {
1054
+ if (!value) return { ok: false, message: t(language, 'requiredField') };
1055
+ const parsed = parseClaudeSetupToken(value);
1056
+ if (!parsed) return { ok: false, message: t(language, 'claudeTokenInvalid') };
1057
+ return { ok: true, value };
1058
+ },
1059
+ );
1060
+
1061
+ const token = parseClaudeSetupToken(tokenRaw);
1062
+ const store = buildAnthropicAuthProfile(token);
1063
+ writeAuthProfilesToDocker(store);
1064
+ ok(t(language, 'claudeTokenWritten'));
1065
+ return 0;
968
1066
  }
969
1067
 
970
1068
  async function runCodexOAuth(language) {
@@ -1004,13 +1102,14 @@ async function runCodexOAuth(language) {
1004
1102
  const accountId = authClaim.chatgpt_account_id || '';
1005
1103
  const email = jwt.email || '';
1006
1104
 
1007
- writeAuthProfilesToDocker({
1105
+ const store = buildCodexAuthProfile({
1008
1106
  access: tokens.access_token,
1009
1107
  refresh: tokens.refresh_token,
1010
1108
  expires: Date.now() + (tokens.expires_in * 1000),
1011
1109
  accountId,
1012
1110
  email,
1013
1111
  });
1112
+ writeAuthProfilesToDocker(store);
1014
1113
 
1015
1114
  return 0;
1016
1115
  }
@@ -1033,18 +1132,10 @@ async function runSubscriptionAuthFlow(cfg) {
1033
1132
  } else {
1034
1133
  log(t(cfg.language, 'anthropicSubscriptionIntro'));
1035
1134
  log(t(cfg.language, 'authFlowStart'));
1036
- // Anthropic paste-token is interactive (user pastes a token); keep stdio inherited
1037
- const authResult = runOpenClaw(['models', 'auth', 'paste-token', '--provider', 'anthropic']);
1038
- if (authResult.status !== 0) die(t(cfg.language, 'authFlowFailed'));
1039
-
1040
- const statusResult = runOpenClaw(
1041
- ['models', 'status', '--check', '--probe-provider', cfg.provider],
1042
- { stdio: 'pipe' },
1043
- );
1044
- if (statusResult.status !== 0) {
1045
- process.stdout.write(statusResult.stdout || '');
1046
- process.stderr.write(statusResult.stderr || '');
1047
- die(t(cfg.language, 'authStatusFailed'));
1135
+ try {
1136
+ await runClaudeSetupTokenAuth(cfg.language);
1137
+ } catch (err) {
1138
+ die(`${t(cfg.language, 'authFlowFailed')}: ${err.message}`);
1048
1139
  }
1049
1140
  ok(t(cfg.language, 'authFlowDone'));
1050
1141
  }
@@ -1098,7 +1189,7 @@ async function cmdStart() {
1098
1189
  cfg = {
1099
1190
  language: lang,
1100
1191
  provider: existingEnv.MODEL_PROVIDER || 'anthropic',
1101
- providerFamily: (existingEnv.MODEL_PROVIDER || 'anthropic').startsWith('openai') ? 'openai' : 'anthropic',
1192
+ providerFamily: deriveProviderFamily(existingEnv.MODEL_PROVIDER),
1102
1193
  authMode: existingEnv.AUTH_MODE || 'api-key',
1103
1194
  modelName: existingEnv.MODEL_NAME || 'claude-opus-4-6',
1104
1195
  telegramEnabled: existingEnv.TELEGRAM_ENABLED || 'false',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {