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.
- package/cli.js +131 -40
- 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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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(
|
|
937
|
-
|
|
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
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
|
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',
|